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 Folder = require('lib/models/Folder.js');
const BaseItem = require('lib/models/BaseItem.js');
const Tag = require('lib/models/Tag.js');
const BaseModel = require('lib/BaseModel.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 { reg } = require('lib/registry.js');
const { _ } = require('lib/locale.js');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
const chalk = require('chalk');
const tk = require('terminal-kit');
@@ -638,12 +641,27 @@ class AppGui {
return true;
}
if (link.type === 'resource') {
const resourceId = link.id;
let resource = await Resource.load(resourceId);
if (!resource) throw new Error('No resource with ID ' + resourceId); // Should be nearly impossible
if (resource.mime) response.setHeader('Content-Type', resource.mime);
response.write(await Resource.content(resource));
if (link.type === 'item') {
const itemId = link.id;
let item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error('No item with ID ' + itemId); // Should be nearly impossible
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;
}
@@ -659,7 +677,7 @@ class AppGui {
if (resourceIdRegex.test(url)) {
noteLinks[index] = {
type: 'resource',
type: 'item',
id: url.substr(2),
};
} else if (hasProtocol(url, ['http', 'https', 'file', 'ftp'])) {

View File

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

View File

@@ -72,7 +72,7 @@ class Command extends BaseCommand {
this.stdout('');
this.stdout(commandNames.join(', '));
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(_('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).'));

View File

@@ -29,7 +29,7 @@ class Command extends BaseCommand {
const folder = await app().loadItem(BaseModel.TYPE_FOLDER, pattern);
if (!folder) throw new Error(_('Cannot find "%s".', pattern));
const ok = force ? true : await this.prompt(_('Delete notebook? 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;
await Folder.delete(folder.id);

View File

@@ -18,19 +18,20 @@ class FolderListWidget extends ListWidget {
this.notesParentType_ = 'Folder';
this.updateIndexFromSelectedFolderId_ = false;
this.updateItems_ = false;
this.trimItemTitle = false;
this.itemRenderer = (item) => {
let output = [];
if (item === '-') {
output.push('-'.repeat(this.innerWidth));
} 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()) {
output.push('[' + Folder.displayTitle(item) + ']');
} else if (item.type_ === BaseModel.TYPE_SEARCH) {
output.push(_('Search:'));
output.push(item.title);
}
}
// 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() {
return this.selectedFolderId_;
}

View File

@@ -229,7 +229,7 @@ msgid "The possible commands are:"
msgstr "Dostupné příkazy:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -364,7 +364,10 @@ msgstr "Smaže vybraný zápisník."
msgid "Deletes the notebook without asking for confirmation."
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é."
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"
msgstr "Přepnout mezi poznámkou a to-do"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete"
msgstr "Smazat"
@@ -1035,6 +1042,10 @@ msgstr "Nelze editovat zašifrovanou položku"
msgid "Conflicts"
msgstr "Konflikty"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Poznámku nelze přesunout do zápisníku \"%s\""
#, javascript-format
msgid "A notebook with this title already exists: \"%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"
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"
msgstr "Řadit poznámky podle"
@@ -1366,6 +1381,14 @@ msgstr "Uložit změny"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Nepodporovaný formát obrázku: %s"

View File

@@ -2,7 +2,7 @@
# Copyright (C) YEAR Laurent Cozic
# This file is distributed under the same license as the Joplin-CLI package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n"
@@ -229,7 +229,7 @@ msgid "The possible commands are:"
msgstr "Mulige kommandoer er:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -367,7 +367,10 @@ msgstr "Sletter aktuelle notesbog."
msgid "Deletes the notebook without asking for confirmation."
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."
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"
msgstr "Skift mellem note- og opgave type"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete"
msgstr "Slet"
@@ -1044,6 +1051,10 @@ msgstr "Krypteret emner kan ikke rettes"
msgid "Conflicts"
msgstr "Konflikter"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Kan ikke flytte note til \"%s\" notesbog"
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "En notesbog bruger allerede dette navn: \"%s\""
@@ -1097,6 +1108,10 @@ msgstr "Mørkt"
msgid "Uncompleted to-dos on top"
msgstr "Ufærdige opgaver øverst"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Ufærdige opgaver øverst"
msgid "Sort notes by"
msgstr "Sorter noter efter"
@@ -1375,6 +1390,14 @@ msgstr "Gem ændringer"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Ulovlig billedtype: %s"

View File

@@ -7,13 +7,13 @@ msgid ""
msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\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: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.6\n"
"X-Generator: Poedit 2.0.7\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "To delete a tag, untag the associated notes."
@@ -238,7 +238,7 @@ msgid "The possible commands are:"
msgstr "Mögliche Befehle lauten:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -259,9 +259,8 @@ msgstr ""
"Benutze die Pfeiltasten und Bild hoch/runter um durch Listen und Texte zu "
"scrollen (inklusive diesem Terminal)."
#, fuzzy
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 \":\""
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."
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 ""
"Notizbuch wirklich löschen? Alle Notizen darin werden ebenfalls gelöscht."
@@ -433,12 +435,14 @@ msgstr ""
msgid ""
"To allow Joplin to synchronise with Dropbox, please follow the steps below:"
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:"
msgstr ""
msgstr "Schritt 1: URL im Browser öffnen um die Anwendung zu autorisieren:"
msgid "Step 2: Enter the code provided by Dropbox:"
msgstr ""
msgstr "Schritt 2: Den von Dropbox bereitgestellten Code eingeben:"
#, javascript-format
msgid "Not authentified with %s. Please provide any missing credentials."
@@ -531,9 +535,8 @@ msgstr "Standard: %s"
msgid "Possible keys/values:"
msgstr "Mögliche Werte:"
#, fuzzy
msgid "Type `joplin help` for usage information."
msgstr "Zeigt die Nutzungsstatistik an."
msgstr "Gib `joplin help` ein um die Nutzungsstatistik anzuzeigen."
msgid "Fatal error:"
msgstr "Schwerwiegender Fehler:"
@@ -541,7 +544,7 @@ msgstr "Schwerwiegender Fehler:"
msgid ""
"The application has been authorised - you may now close this browser tab."
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."
msgstr "Das Programm wurde erfolgreich autorisiert."
@@ -647,7 +650,7 @@ msgid "View"
msgstr "Ansicht"
msgid "Toggle sidebar"
msgstr ""
msgstr "Seitenleiste ein/aus"
msgid "Toggle editor layout"
msgstr "Editor Layout umschalten"
@@ -719,7 +722,7 @@ msgid "Save"
msgstr "Speichern"
msgid "Submit"
msgstr ""
msgstr "Absenden"
msgid ""
"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 "
"to enable it please check the documentation:"
msgstr ""
"Weitere Informationen zur Ende-zu-Ende-Verschlüsselung (E2EE) und Hinweise "
"zur Aktivierung findest du in der Dokumentation (auf Englisch):"
msgid "Status"
msgstr "Status"
@@ -836,9 +841,8 @@ msgstr "Alarm erstellen:"
msgid "Layout"
msgstr "Layout"
#, fuzzy
msgid "Search..."
msgstr "Suchen"
msgstr "Suchen..."
msgid "Some items cannot be synchronised."
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"
msgstr "Zwischen Notiz und To-Do Typ wechseln"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete"
msgstr "Löschen"
@@ -872,8 +880,8 @@ msgstr ""
msgid ""
"There is currently no notebook. Create one by clicking on \"New notebook\"."
msgstr ""
"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf den "
"(+) Knopf drückst."
"Momentan existieren noch keine Notizbücher. Erstelle eines, indem du auf "
"\"Neues Notizbuch\" drückst."
msgid "Open..."
msgstr "Öffne..."
@@ -922,7 +930,7 @@ msgid "OneDrive Login"
msgstr "OneDrive Login"
msgid "Dropbox Login"
msgstr ""
msgstr "Dropbox Anmeldung"
msgid "Options"
msgstr "Optionen"
@@ -961,7 +969,7 @@ msgid "Unknown flag: %s"
msgstr "Unbekanntes Argument: %s"
msgid "Dropbox"
msgstr ""
msgstr "Dropbox"
msgid "File system"
msgstr "Dateisystem"
@@ -1040,9 +1048,9 @@ msgstr "Remote Objekte gelöscht: %d."
msgid "Fetched items: %d/%d."
msgstr "Geladene Objekte: %d/%d."
#, fuzzy, javascript-format
#, javascript-format
msgid "State: %s."
msgstr "Status: \"%s\"."
msgstr "Status: %s."
msgid "Cancelling..."
msgstr "Abbrechen..."
@@ -1074,6 +1082,10 @@ msgstr "Verschlüsselte Objekte können nicht verändert werden"
msgid "Conflicts"
msgstr "Konflikte"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Kann Notiz nicht zu Notizbuch \"%s\" verschieben"
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Ein Notizbuch mit diesem Titel existiert bereits : \"%s\""
@@ -1129,6 +1141,10 @@ msgstr "Dunkel"
msgid "Uncompleted to-dos on top"
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"
msgstr "Sortiere Notizen nach"
@@ -1154,7 +1170,7 @@ msgid "Show tray icon"
msgstr "Zeige Tray Icon"
msgid "Note: Does not work in all desktop environments."
msgstr ""
msgstr "Hinweis: Funktioniert nicht in allen Desktopumgebungen."
msgid "Global zoom percentage"
msgstr "Zoomstufe der Benutzeroberfläche"
@@ -1284,7 +1300,7 @@ msgid ""
"(which is displayed in brackets above)."
msgstr ""
"Diese Objekte verbleiben auf dem Gerät, werden aber nicht zum "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchen Sie "
"Synchronisationsziel hochgeladen. Um diese Objekte zu finden, suchst du "
"entweder nach dem Titel oder der ID (die oben in Klammern angezeigt wird)."
msgid "Sync status (synced items / total items)"
@@ -1360,17 +1376,16 @@ msgid "Cancel synchronisation"
msgstr "Synchronisation abbrechen"
msgid "New tags:"
msgstr ""
msgstr "Neue Markierungen:"
msgid "Type new tags or select from list"
msgstr ""
msgstr "Neue Markierungen eingeben oder aus der Liste auswählen"
msgid "Joplin website"
msgstr "Website von Joplin"
#, fuzzy
msgid "Login with Dropbox"
msgstr "Mit OneDrive anmelden"
msgstr "Mit Dropbox anmelden"
#, javascript-format
msgid "Master Key %s"
@@ -1411,6 +1426,14 @@ msgstr "Änderungen speichern"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Nicht unterstütztes Fotoformat: %s"

View File

@@ -210,7 +210,7 @@ msgid "The possible commands are:"
msgstr ""
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -332,7 +332,9 @@ msgstr ""
msgid "Deletes the notebook without asking for confirmation."
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 ""
msgid "Deletes the notes matching <note-pattern>."
@@ -753,6 +755,9 @@ msgstr ""
msgid "Switch between note and to-do type"
msgstr ""
msgid "Copy Markdown link"
msgstr ""
msgid "Delete"
msgstr ""
@@ -954,6 +959,9 @@ msgstr ""
msgid "Conflicts"
msgstr ""
msgid "Cannot move notebook to this location"
msgstr ""
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr ""
@@ -1005,6 +1013,9 @@ msgstr ""
msgid "Uncompleted to-dos on top"
msgstr ""
msgid "Show completed to-dos"
msgstr ""
msgid "Sort notes by"
msgstr ""
@@ -1271,6 +1282,14 @@ msgstr ""
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr ""

View File

@@ -230,7 +230,7 @@ msgid "The possible commands are:"
msgstr "Los posibles comandos son:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -250,7 +250,6 @@ msgstr ""
"Para desplazar en las listas y areas de texto (incluyendo la consola) "
"utilice las flechas y re pág/av pág."
#, fuzzy
msgid "To maximise/minimise the console, press \"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."
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 ""
"¿Desea eliminar la libreta? Todas las notas dentro de esta libreta también "
"serán eliminadas."
"¿Desea eliminar la libreta? Todas las notas y sublibretas dentro de esta "
"libreta también serán eliminadas."
msgid "Deletes the notes matching <note-pattern>."
msgstr "Elimina las notas que coinciden con <note-pattern>."
@@ -634,7 +635,7 @@ msgid "View"
msgstr "Ver"
msgid "Toggle sidebar"
msgstr ""
msgstr "Cambia la barra lateral"
msgid "Toggle editor layout"
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"
msgstr "Cambiar entre nota y lista de tareas"
msgid "Copy Markdown link"
msgstr "Copiar el enlace de Markdown"
msgid "Delete"
msgstr "Eliminar"
@@ -1056,6 +1060,9 @@ msgstr "Los elementos cifrados no pueden ser modificados"
msgid "Conflicts"
msgstr "Conflictos"
msgid "Cannot move notebook to this location"
msgstr "No se puede mover la libreta a este lugar"
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Ya existe una libreta con este nombre: «%s»"
@@ -1110,6 +1117,10 @@ msgstr "Oscuro"
msgid "Uncompleted to-dos on top"
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"
msgstr "Ordenar notas por"
@@ -1135,7 +1146,7 @@ msgid "Show tray icon"
msgstr "Mostrar icono en la bandeja"
msgid "Note: Does not work in all desktop environments."
msgstr ""
msgstr "Nota: No funciona en todos los entornos de escritorio."
msgid "Global zoom percentage"
msgstr "Establecer el porcentaje de aumento de la aplicación"
@@ -1388,6 +1399,15 @@ msgstr "Guardar cambios"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Tipo de imagen no soportado: %s"

View File

@@ -227,7 +227,7 @@ msgid "The possible commands are:"
msgstr "Litezkeen komandoak hauek dira:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -368,7 +368,10 @@ msgstr "Ezabatu emandako koadernoak."
msgid "Deletes the notebook without asking for confirmation."
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."
msgid "Deletes the notes matching <note-pattern>."
@@ -845,6 +848,9 @@ msgstr "Gehitu edo ezabatu etiketak"
msgid "Switch between note and to-do type"
msgstr "Aldatu oharra eta zeregin eren artean."
msgid "Copy Markdown link"
msgstr ""
msgid "Delete"
msgstr "Ezabatu"
@@ -1060,6 +1066,10 @@ msgstr "Zifratutako itemak ezin aldatu daitezke"
msgid "Conflicts"
msgstr "Gatazkak"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Ezin eraman daiteke oharra \"%s\" koadernora"
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Dagoeneko bada koaderno bat izen horrekin: \"%s\""
@@ -1115,6 +1125,10 @@ msgstr "Iluna"
msgid "Uncompleted to-dos on top"
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"
msgstr ""
@@ -1398,6 +1412,14 @@ msgstr "Gorde aldaketak"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Irudi formatua ez onartua: %s"

View File

@@ -229,7 +229,7 @@ msgid "The possible commands are:"
msgstr "Les commandes possibles sont :"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -365,10 +365,12 @@ msgstr "Supprimer le carnet."
msgid "Deletes the notebook without asking for 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 ""
"Effacer le carnet ? Toutes les notes dans ce carnet seront également "
"effacées."
"Effacer le carnet ? Toutes les notes et sous-carnets dans ce carnet seront "
"également effacés."
msgid "Deletes the notes matching <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"
msgstr "Alterner entre note et tâche"
msgid "Copy Markdown link"
msgstr "Copier lien Markdown"
msgid "Delete"
msgstr "Supprimer"
@@ -1063,6 +1068,9 @@ msgstr "Les objets cryptés ne peuvent être modifiés"
msgid "Conflicts"
msgstr "Conflits"
msgid "Cannot move notebook to this location"
msgstr "Impossible de déplacer le carnet vers le carnet \"%s\""
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Un carnet avec ce titre existe déjà : \"%s\""
@@ -1116,6 +1124,9 @@ msgstr "Sombre"
msgid "Uncompleted to-dos on top"
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"
msgstr "Trier les notes par"
@@ -1396,6 +1407,15 @@ msgstr "Enregistrer les changements"
msgid "Discard changes"
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
msgid "Unsupported image type: %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:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -364,7 +364,10 @@ msgstr "Eliminar o carderno indicado."
msgid "Deletes the notebook without asking for confirmation."
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 ""
"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"
msgstr "Cambiar entre notas e tarefas"
#, fuzzy
msgid "Copy Markdown link"
msgstr "Markdown"
msgid "Delete"
msgstr "Eliminar"
@@ -1043,6 +1050,10 @@ msgstr "Non é posíbel modificar elementos cifrados"
msgid "Conflicts"
msgstr "Conflitos"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Non é posíbel mover a nota ao caderno «%s»"
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Xa existe un caderno con ese título: «%s»"
@@ -1096,6 +1107,10 @@ msgstr "Escuro"
msgid "Uncompleted to-dos on top"
msgstr "Tarefas sen completar arriba"
#, fuzzy
msgid "Show completed to-dos"
msgstr "Tarefas sen completar arriba"
msgid "Sort notes by"
msgstr "Ordenar notas por"
@@ -1374,6 +1389,14 @@ msgstr "Gardar cambios"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Tipo de imaxe incompatíbel: %s"

View File

@@ -229,15 +229,11 @@ msgstr ""
msgid "The possible commands are:"
msgstr "Moguće naredbe su:"
#, fuzzy
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
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."
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."
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 ""
"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"
msgstr "Zamijeni bilješku i zadatak"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete"
msgstr "Obriši"
@@ -1048,6 +1050,10 @@ msgstr "Neke stavke se ne mogu sinkronizirati."
msgid "Conflicts"
msgstr "Sukobi"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Ne mogu premjestiti bilješku u bilježnicu %s"
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Bilježnica s ovim naslovom već postoji: \"%s\""
@@ -1102,6 +1108,10 @@ msgstr "Tamna"
msgid "Uncompleted to-dos on top"
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"
msgstr ""
@@ -1379,6 +1389,14 @@ msgstr "Spremi promjene"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Nepodržana vrsta slike: %s"

View File

@@ -226,7 +226,7 @@ msgid "The possible commands are:"
msgstr "I possibili comandi sono:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -364,7 +364,9 @@ msgstr "Elimina il seguente blocco note."
msgid "Deletes the notebook without asking for confirmation."
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 ""
msgid "Deletes the notes matching <note-pattern>."
@@ -819,6 +821,9 @@ msgstr "Aggiungi o rimuovi etichetta"
msgid "Switch between note and to-do type"
msgstr "Passa da un tipo di nota a un elenco di attività"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete"
msgstr "Elimina"
@@ -1034,6 +1039,10 @@ msgstr "Alcuni elementi non possono essere sincronizzati."
msgid "Conflicts"
msgstr "Conflitti"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Non posso spostare la nota nel blocco note \"%s\""
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Esiste già un blocco note col titolo \"%s\""
@@ -1088,6 +1097,10 @@ msgstr "Scuro"
msgid "Uncompleted to-dos on top"
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"
msgstr ""
@@ -1365,6 +1378,14 @@ msgstr "Salva i cambiamenti"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Tipo di immagine non supportata: %s"

View File

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

View File

@@ -210,7 +210,7 @@ msgid "The possible commands are:"
msgstr ""
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -332,7 +332,9 @@ msgstr ""
msgid "Deletes the notebook without asking for confirmation."
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 ""
msgid "Deletes the notes matching <note-pattern>."
@@ -753,6 +755,9 @@ msgstr ""
msgid "Switch between note and to-do type"
msgstr ""
msgid "Copy Markdown link"
msgstr ""
msgid "Delete"
msgstr ""
@@ -954,6 +959,9 @@ msgstr ""
msgid "Conflicts"
msgstr ""
msgid "Cannot move notebook to this location"
msgstr ""
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr ""
@@ -1005,6 +1013,9 @@ msgstr ""
msgid "Uncompleted to-dos on top"
msgstr ""
msgid "Show completed to-dos"
msgstr ""
msgid "Sort notes by"
msgstr ""
@@ -1271,6 +1282,14 @@ msgstr ""
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr ""

View File

@@ -230,7 +230,7 @@ msgid "The possible commands are:"
msgstr "Mogelijke commando's zijn:"
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 "
"note or notebook. `$c` can be used to refer to the currently selected item."
msgstr ""
@@ -369,7 +369,10 @@ msgstr "Verwijdert het opgegeven notitieboek."
msgid "Deletes the notebook without asking for confirmation."
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 ""
"Notitieboek verwijderen? Alle notities in dit notitieboek zullen ook "
"verwijderd worden."
@@ -847,6 +850,9 @@ msgstr "Voeg tag toe of verwijder tag"
msgid "Switch between note and to-do type"
msgstr "Wissel tussen notitie en to-do type"
msgid "Copy Markdown link"
msgstr ""
msgid "Delete"
msgstr "Verwijderen"
@@ -1062,6 +1068,10 @@ msgstr "Versleutelde items kunnen niet aangepast worden"
msgid "Conflicts"
msgstr "Conflicten"
#, fuzzy
msgid "Cannot move notebook to this location"
msgstr "Kan notitie niet naar notitieboek \"%s\" verplaatsen."
#, javascript-format
msgid "A notebook with this title already exists: \"%s\""
msgstr "Er bestaat al een notitieboek met \"%s\" als titel"
@@ -1119,6 +1129,10 @@ msgstr "Donker"
msgid "Uncompleted to-dos on top"
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"
msgstr ""
@@ -1400,6 +1414,14 @@ msgstr "Sla wijzigingen op"
msgid "Discard changes"
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
msgid "Unsupported image type: %s"
msgstr "Afbeeldingstype %s wordt niet ondersteund"

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.0.106",
"version": "1.0.107",
"bin": {
"joplin": "./main.js"
},
@@ -58,9 +58,10 @@
"strip-ansi": "^4.0.0",
"tar": "^4.4.0",
"tcp-port-used": "^0.1.2",
"tkwidgets": "^0.5.25",
"tkwidgets": "^0.5.26",
"url-parse": "^1.2.0",
"uuid": "^3.0.1",
"valid-url": "^1.0.9",
"word-wrap": "^1.2.3",
"xml2js": "^0.4.19",
"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"
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
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi

View File

@@ -44,4 +44,13 @@ describe('ArrayUtils', function() {
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 });
await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
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]);
await service.export({ path: filePath });
@@ -193,7 +193,7 @@ describe('services_InteropService', function() {
let note2 = (await Note.all())[0];
expect(note2.body).not.toBe(note1.body);
resourceIds = Note.linkedResourceIds(note2.body);
resourceIds = await Note.linkedResourceIds(note2.body);
expect(resourceIds.length).toBe(1);
let resource2 = await Resource.load(resourceIds[0]);
expect(resource2.id).not.toBe(resource1.id);
@@ -249,4 +249,28 @@ describe('services_InteropService', function() {
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 (!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) {
@@ -437,6 +437,14 @@ class Application extends BaseApplication {
click: () => {
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'),
@@ -615,6 +623,11 @@ class Application extends BaseApplication {
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
// but then doesn't install it on exit.
if (shim.isWindows() || shim.isMac()) {

View File

@@ -43,7 +43,7 @@ async function fetchLatestRelease() {
for (let i = 0; i < json.assets.length; i++) {
const asset = json.assets[i];
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;
} else if (platform === 'darwin' && asset.name.indexOf('.dmg') >= 0) {
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 ioService = new InteropService();

View File

@@ -1,5 +1,6 @@
const React = require('react');
const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
const BaseModel = require('lib/BaseModel.js');
const Search = require('lib/models/Search.js');
const { time } = require('lib/time-utils.js');
@@ -19,6 +20,10 @@ const MenuItem = bridge().MenuItem;
const { shim } = require('lib/shim.js');
const eventManager = require('../eventManager');
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');
// 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
// to automatically set the title.
newAndNoTitleChangeNoteId: null,
bodyHtml: '',
};
this.lastLoadedNoteId_ = null;
@@ -54,6 +60,8 @@ class NoteTextComponent extends React.Component {
this.ignoreNextEditorScroll_ = false;
this.scheduleSaveTimeout_ = null;
this.restoreScrollTop_ = null;
this.lastSetHtml_ = '';
this.lastSetMarkers_ = [];
// Complicated but reliable method to get editor content height
// 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.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.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() {
if (this.mdToHtml_) return this.mdToHtml_;
this.mdToHtml_ = new MdToHtml({
supportsResourceLinks: true,
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
});
return this.mdToHtml_;
@@ -210,10 +273,22 @@ class NoteTextComponent extends React.Component {
}
if (this.editor_) {
const session = this.editor_.editor.getSession();
const undoManager = session.getUndoManager();
undoManager.reset();
session.setUndoManager(undoManager);
// Calling setValue here does two things:
// 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
// phase to set the value, the initial value would still be "", which means pressing "undo" on a note that has just loaded
// 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.moveCursorTo(0,0);
}
@@ -231,7 +306,12 @@ class NoteTextComponent extends React.Component {
newState.newAndNoTitleChangeNoteId = null;
}
this.lastSetHtml_ = '';
this.lastSetMarkers_ = [];
this.setState(newState);
this.updateHtml(newState.note ? newState.note.body : '');
}
async componentWillReceiveProps(nextProps) {
@@ -322,11 +402,29 @@ class NoteTextComponent extends React.Component {
menu.popup(bridge().window());
} else if (msg.indexOf('joplin://') === 0) {
const resourceId = msg.substr('joplin://'.length);
Resource.load(resourceId).then((resource) => {
const filePath = Resource.fullPath(resource);
const itemId = msg.substr('joplin://'.length);
const item = await BaseItem.loadItemById(itemId);
if (!item) throw new Error('No item with ID ' + itemId);
if (item.type_ === BaseModel.TYPE_RESOURCE) {
const filePath = Resource.fullPath(item);
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 {
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
}
@@ -391,6 +489,7 @@ class NoteTextComponent extends React.Component {
if (this.editor_) {
this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_);
document.querySelector('#note-editor').removeEventListener('paste', this.onEditorPaste_, true);
}
this.editor_ = element;
@@ -398,7 +497,14 @@ class NoteTextComponent extends React.Component {
if (this.editor_) {
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++) {
const k = cancelledKeys[i];
this.editor_.editor.commands.bindKey(k, () => {
@@ -409,6 +515,8 @@ class NoteTextComponent extends React.Component {
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) {
shared.noteComponent_change(this, 'body', body);
this.scheduleHtmlUpdate();
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) {
if (!command) return;
@@ -479,27 +630,34 @@ class NoteTextComponent extends React.Component {
}
}
async commandAttachFile() {
const filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory', 'multiSelections'],
});
if (!filePaths || !filePaths.length) return;
async commandAttachFile(filePaths = null) {
if (!filePaths) {
filePaths = bridge().showOpenDialog({
properties: ['openFile', 'createDirectory', 'multiSelections'],
});
if (!filePaths || !filePaths.length) return;
}
await this.saveIfNeeded(true);
let note = await Note.load(this.state.note.id);
const position = this.cursorPosition();
for (let i = 0; i < filePaths.length; i++) {
const filePath = filePaths[i];
try {
reg.logger().info('Attaching ' + filePath);
note = await shim.attachFileToNote(note, filePath);
note = await shim.attachFileToNote(note, filePath, position);
reg.logger().info('File was attached.');
this.setState({
note: Object.assign({}, note),
lastSavedNote: Object.assign({}, note),
});
this.updateHtml(note.body);
} catch (error) {
reg.logger().error(error);
bridge().showErrorMessageBox(error.message);
}
}
}
@@ -644,25 +802,21 @@ class NoteTextComponent extends React.Component {
}
if (this.state.webviewReady) {
const mdOptions = {
onResourceLoaded: () => {
this.forceUpdate();
},
postMessageSyntax: 'ipcRenderer.sendToHost',
};
let html = this.state.bodyHtml;
let bodyToRender = body;
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')) + '*';
const htmlHasChanged = this.lastSetHtml_ !== html;
if (htmlHasChanged) {
this.webview_.send('setHtml', html);
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 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 = [];
@@ -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 viewer = <webview
const viewer = <webview
style={viewerStyle}
nodeintegration="1"
src="gui/note-viewer/index.html"
@@ -763,7 +917,7 @@ class NoteTextComponent extends React.Component {
/>
return (
<div style={rootStyle}>
<div style={rootStyle} onDrop={this.onDrop_}>
<div style={titleBarStyle}>
{ titleEditor }
{ titleBarDate }

View File

@@ -14,6 +14,57 @@ const MenuItem = bridge().MenuItem;
const InteropServiceHelper = require("../InteropServiceHelper.js");
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() {
const theme = themeStyle(this.props.theme);
@@ -23,23 +74,39 @@ class SideBarComponent extends React.Component {
root: {
backgroundColor: theme.backgroundColor2,
},
listItem: {
listItemContainer: {
boxSizing: "border-box",
height: itemHeight,
// paddingLeft: 14,
display: "flex",
alignItems: "stretch",
},
listItem: {
fontFamily: theme.fontFamily,
fontSize: theme.fontSize,
textDecoration: "none",
boxSizing: "border-box",
color: theme.color2,
paddingLeft: 14,
display: "flex",
alignItems: "center",
cursor: "default",
opacity: 0.8,
whiteSpace: "nowrap",
display: "flex",
flex: 1,
alignItems: 'center',
},
listItemSelected: {
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: {
color: theme.colorError2,
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;
}
@@ -101,7 +172,7 @@ class SideBarComponent extends React.Component {
let deleteMessage = "";
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) {
deleteMessage = _("Remove this tag from all the notes?");
} 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" }));
const InteropService = require("lib/services/InteropService.js");
@@ -194,53 +278,51 @@ class SideBarComponent extends React.Component {
await shared.synchronize_press(this);
}
folderItem(folder, selected) {
folderItem(folder, selected, hasChildren, depth) {
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);
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);
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 (
<a
className="list-item"
onDragOver={event => {
onDragOver(event, folder);
}}
onDrop={event => {
onDrop(event, folder);
}}
href="#"
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={event => this.itemContextMenu(event)}
key={folder.id}
style={style}
onClick={() => {
this.folderItem_click(folder);
}}
>
{itemTitle}
</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}>
{ expandLink }
<a
className="list-item"
href="#"
data-id={folder.id}
data-type={BaseModel.TYPE_FOLDER}
onContextMenu={event => this.itemContextMenu(event)}
style={style}
folderid={folder.id}
onClick={() => {
this.folderItem_click(folder);
}}
onDoubleClick={this.onFolderToggleClick_}
>
{itemTitle}
</a>
</div>
);
}
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);
return (
<a
@@ -285,11 +367,11 @@ class SideBarComponent extends React.Component {
return <div style={{ height: 2, backgroundColor: "blue" }} key={key} />;
}
makeHeader(key, label, iconName) {
makeHeader(key, label, iconName, extraProps = {}) {
const style = this.style().header;
const icon = <i style={{ fontSize: style.fontSize * 1.2, marginRight: 5 }} className={"fa " + iconName} />;
return (
<div style={style} key={key}>
<div style={style} key={key} {...extraProps}>
{icon}
{label}
</div>
@@ -326,7 +408,10 @@ class SideBarComponent extends React.Component {
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) {
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);
const syncReportText = [];
for (let i = 0; i < lines.length; i++) {
@@ -396,6 +469,7 @@ const mapStateToProps = state => {
notesParentType: state.notesParentType,
locale: state.settings.locale,
theme: state.settings.theme,
collapsedFolderIds: state.collapsedFolderIds,
};
};

View File

@@ -124,17 +124,17 @@
loadAndApplyHljs();
// Remove the bullet from "ul" for checkbox lists and extra padding
const checkboxes = document.getElementsByClassName('checkbox');
for (let i = 0; i < checkboxes.length; i++) {
const cb = checkboxes[i];
const ul = cb.parentElement.parentElement;
if (!ul) {
console.warn('Unexpected layout for checkbox');
continue;
}
ul.style.listStyleType = 'none';
ul.style.paddingLeft = 0;
}
// const checkboxes = document.getElementsByClassName('checkbox');
// for (let i = 0; i < checkboxes.length; i++) {
// const cb = checkboxes[i];
// const ul = cb.parentElement.parentElement;
// if (!ul) {
// console.warn('Unexpected layout for checkbox');
// continue;
// }
// ul.style.listStyleType = 'none';
// ul.style.paddingLeft = 0;
// }
let previousContentHeight = contentElement.scrollHeight;
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",
"version": "1.0.83",
"version": "1.0.91",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -92,6 +92,7 @@
"markdown-it": "^8.4.0",
"markdown-it-katex": "^2.0.3",
"md5": "^2.2.1",
"mermaid": "^8.0.0-rc.8",
"mime": "^2.0.3",
"moment": "^2.19.1",
"node-fetch": "^1.7.3",
@@ -113,6 +114,7 @@
"tcp-port-used": "^0.1.2",
"url-parse": "^1.2.0",
"uuid": "^3.1.0",
"valid-url": "^1.0.9",
"xml2js": "^0.4.19"
}
}

View File

@@ -34,17 +34,15 @@ table td, table th {
background-color: rgba(0,160,255,0.1) !important;
}
.side-bar .list-item:hover,
/*.side-bar .list-item:hover,
.side-bar .synchronize-button:hover {
/*background-color: #453E53;*/
background-color: #01427B;
}
.side-bar .list-item:active,
.side-bar .synchronize-button:active {
/*background-color: #564B6C;*/
background-color: #0465BB;
}
}*/
.editor-toolbar .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
-----------------|--------|-------------------
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> |
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> |
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).
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.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.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
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> | -
## Terminal application
@@ -37,7 +37,7 @@ Operating system | Method
-----------------|----------------
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.
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`.
@@ -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.
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
@@ -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).
# 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
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 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.
## 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 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
- 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).
- The latest news are often posted [on this Twitter account](https://twitter.com/laurent2233).
@@ -267,25 +294,26 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&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/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/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/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/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/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) | 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) | 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. | 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) | 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/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/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/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/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/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/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/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/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) | 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) | 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) | | 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) | | 61%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# 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.
- 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 {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 22
versionCode 2097296
versionName "1.0.118"
targetSdkVersion 26
versionCode 2097303
versionName "1.0.125"
ndk {
abiFilters "armeabi-v7a", "x86"
}

View File

@@ -26,7 +26,7 @@
<uses-sdk
android:minSdkVersion="16"
android:targetSdkVersion="22" />
android:targetSdkVersion="26" />
<application
android:name=".MainApplication"
@@ -38,15 +38,6 @@
<!-- ==================================== -->
<!-- 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.RNPushNotificationBootEventReceiver">
@@ -55,13 +46,6 @@
</intent-filter>
</receiver>
<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 -->
<!-- ================================== -->

View File

@@ -306,6 +306,34 @@
remoteGlobalIDString = 139D7E881E25C6D100323FB7;
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 */ = {
isa = PBXContainerItemProxy;
containerPortal = A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */;
@@ -540,10 +568,14 @@
4D2AFF8D1FDA002000599716 /* libcxxreact.a */,
3DAD3EAD1DF850E9000B6D8A /* libjschelpers.a */,
4D2AFF8F1FDA002000599716 /* libjschelpers.a */,
4D7F8DA120A32BA0008B757D /* libjsinspector.a */,
4D7F8DA320A32BA0008B757D /* libjsinspector-tvOS.a */,
4D3A19271FBDDA9400457703 /* libthird-party.a */,
4D2AFF911FDA002000599716 /* libthird-party.a */,
4D3A192B1FBDDA9400457703 /* libdouble-conversion.a */,
4D2AFF931FDA002000599716 /* libdouble-conversion.a */,
4D7F8DA520A32BA0008B757D /* libprivatedata.a */,
4D7F8DA720A32BA0008B757D /* libprivatedata-tvOS.a */,
);
name = Products;
sourceTree = "<group>";
@@ -1150,6 +1182,34 @@
remoteRef = 4D3A192A1FBDDA9400457703 /* PBXContainerItemProxy */;
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 */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;

View File

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

View File

@@ -45,4 +45,17 @@ ArrayUtils.findByKey = function(array, key, value) {
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;

View File

@@ -19,6 +19,7 @@ const BaseSyncTarget = require('lib/BaseSyncTarget.js');
const { fileExtension } = require('lib/path-utils.js');
const { shim } = require('lib/shim.js');
const { _, setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const reduxSharedMiddleware = require('lib/components/shared/reduxSharedMiddleware');
const os = require('os');
const fs = require('fs-extra');
const JoplinError = require('lib/JoplinError');
@@ -143,6 +144,14 @@ class BaseApplication {
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] == '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else {
@@ -190,6 +199,7 @@ class BaseApplication {
let options = {
order: stateUtils.notesOrder(state.settings),
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
showCompletedTodos: Setting.value('showCompletedTodos'),
caseInsensitive: true,
};
@@ -262,6 +272,8 @@ class BaseApplication {
const newState = store.getState();
let refreshNotes = false;
reduxSharedMiddleware(store, next, action);
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
Setting.setValue('activeFolderId', newState.selectedFolderId);
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
@@ -272,6 +284,10 @@ class BaseApplication {
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')) {
refreshNotes = true;
}

View File

@@ -44,6 +44,15 @@ class BaseModel {
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) {
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
const e = BaseModel.typeEnum_[i];

View File

@@ -14,7 +14,6 @@ class MdToHtml {
constructor(options = null) {
if (!options) options = {};
this.supportsResourceLinks_ = !!options.supportsResourceLinks;
this.loadedResources_ = {};
this.cachedContent_ = null;
this.cachedContentKey_ = null;
@@ -132,43 +131,38 @@ class MdToHtml {
const isResourceUrl = Resource.isResourceUrl(href);
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
if (isResourceUrl && !this.supportsResourceLinks_) {
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
// Ideally they should be opened in the user's browser.
return '<span style="opacity: 0.5">(Resource not yet supported: '; //+ htmlentities(text) + ']';
let resourceIdAttr = "";
let icon = "";
let hrefAttr = '#';
if (isResourceUrl) {
const resourceId = Resource.pathToId(href);
href = "joplin://" + resourceId;
resourceIdAttr = "data-resource-id='" + resourceId + "'";
icon = '<span class="resource-icon"></span>';
} else {
let resourceIdAttr = "";
let icon = "";
if (isResourceUrl) {
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;
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
// link. This allows the link to be exported too when exporting to PDF.
hrefAttr = href;
}
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) {
const href = this.getAttr_(attrs, 'href');
const isResourceUrl = Resource.isResourceUrl(href);
if (isResourceUrl && !this.supportsResourceLinks_) {
return ')</span>';
} else {
return '</a>';
}
return '</a>';
}
rendererPlugin_(language) {
if (!language) return null;
const handlers = {};
handlers['katex'] = new MdToHtml_Katex();
return language in handlers ? handlers[language] : null;
if (!this.rendererPlugins_) {
this.rendererPlugins_ = {};
this.rendererPlugins_['katex'] = new MdToHtml_Katex();
}
return language in this.rendererPlugins_ ? this.rendererPlugins_[language] : null;
}
parseInlineCodeLanguage_(content) {
@@ -220,12 +214,7 @@ class MdToHtml {
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
if (previousToken && previousToken.tag === 'li' && tag === 'p') {
// Markdown-it render list items as <li><p>Text<p></li> which makes it
// complicated to style and layout the HTML, so we remove this extra
// <p> here and below in closeTag.
openTag = null;
} else if (isInlineCode) {
if (isInlineCode) {
openTag = null;
} else if (tag && t.type.indexOf('html_inline') >= 0) {
openTag = null;
@@ -403,7 +392,7 @@ class MdToHtml {
const md = new MarkdownIt({
breaks: 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
@@ -420,8 +409,8 @@ class MdToHtml {
if (HORRIBLE_HACK) {
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
body = body.replace(/- \[(X| )\]/, function(v, p1) {
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
counter++;
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/
const normalizeCss = `
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 {
padding-left: 1.3em;
}
li p {
margin-bottom: 0;
}
.resource-icon {
display: inline-block;
position: relative;
@@ -577,10 +572,10 @@ class MdToHtml {
toggleTickAt(body, index) {
let counter = -1;
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
counter++;
body = body.replace(/- \[(X| )\]/, function(v, p1) {
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
if (index == counter) {
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';

View File

@@ -5,13 +5,28 @@ const Setting = require('lib/models/Setting');
class MdToHtml_Katex {
constructor() {
this.cache_ = {};
this.assetsLoaded_ = false;
}
name() {
return 'katex';
}
processContent(renderedTokens, content, tagType) {
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>';
@@ -27,6 +42,8 @@ class MdToHtml_Katex {
}
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 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_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 { Platform, WebView, View, Linking } = require('react-native');
const { Platform, WebView, View } = require('react-native');
const { globalStyle } = require('lib/components/global-style.js');
const Resource = require('lib/models/Resource.js');
const Setting = require('lib/models/Setting.js');
@@ -19,7 +19,7 @@ class NoteBodyViewer extends Component {
}
UNSAFE_componentWillMount() {
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: false });
this.mdToHtml_ = new MdToHtml();
this.isMounted_ = true;
}
@@ -39,6 +39,19 @@ class NoteBodyViewer extends Component {
}, 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() {
const note = this.props.note;
const style = this.props.style;
@@ -109,13 +122,13 @@ class NoteBodyViewer extends Component {
let msg = event.nativeEvent.data;
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);
} else if (msg.indexOf('bodyscroll:') === 0) {
//msg = msg.split(':');
//this.bodyScrollTop_ = Number(msg[1]);
} else {
Linking.openURL(msg);
this.props.onJoplinLinkClick(msg);
}
}}
/>

View File

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

View File

@@ -1,5 +1,5 @@
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 { ScreenHeader } = require('lib/components/screen-header.js');
const { _ } = require('lib/locale.js');
@@ -53,7 +53,7 @@ class DropboxLoginScreenComponent extends BaseScreenComponent {
<View style={this.styles().screen}>
<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}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text>
<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}/>
<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 }}/>
</View>

View File

@@ -1,9 +1,10 @@
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 { uuid } = require('lib/uuid.js');
const RNFS = require('react-native-fs');
const Note = require('lib/models/Note.js');
const BaseItem = require('lib/models/BaseItem.js');
const Setting = require('lib/models/Setting.js');
const Resource = require('lib/models/Resource.js');
const Folder = require('lib/models/Folder.js');
@@ -22,8 +23,8 @@ const { _ } = require('lib/locale.js');
const { reg } = require('lib/registry.js');
const { shim } = require('lib/shim.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 { dialogs } = require('lib/dialogs.js');
const DialogBox = require('react-native-dialogbox').default;
const { NoteBodyViewer } = require('lib/components/note-body-viewer.js');
const RNFetchBlob = require('react-native-fetch-blob').default;
@@ -107,6 +108,39 @@ class NoteScreenComponent extends BaseScreenComponent {
this.noteTagDialog_closeRequested = () => {
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() {
@@ -186,8 +220,8 @@ class NoteScreenComponent extends BaseScreenComponent {
shared.noteComponent_change(this, 'body', text);
}
async saveNoteButton_press() {
await shared.saveNoteButton_press(this);
async saveNoteButton_press(folderId = null) {
await shared.saveNoteButton_press(this, folderId);
Keyboard.dismiss();
}
@@ -330,10 +364,16 @@ class NoteScreenComponent extends BaseScreenComponent {
return;
} else {
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) {
reg.logger().warn('Could not attach file:', error);
await dialogs.error(this, error.message);
return;
}
@@ -369,6 +409,13 @@ class NoteScreenComponent extends BaseScreenComponent {
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() {
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() {
const note = this.state.note;
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: _('Share'), onPress: () => { this.share_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(); } });
if (isSaved) output.push({ title: _('Copy Markdown link'), onPress: () => { this.copyMarkdownLink_onPress(); } });
output.push({ isDivider: true });
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(); } });
@@ -466,7 +520,13 @@ class NoteScreenComponent extends BaseScreenComponent {
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 {
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} />
}
@@ -560,7 +620,12 @@ class NoteScreenComponent extends BaseScreenComponent {
enabled: true,
selectedFolderId: folder ? folder.id : null,
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;
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') },
});
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);
if (!r) return;
@@ -79,6 +84,7 @@ class NotesScreenComponent extends BaseScreenComponent {
let options = {
order: props.notesOrder,
uncompletedTodosOnTop: props.uncompletedTodosOnTop,
showCompletedTodos: props.showCompletedTodos,
caseInsensitive: true,
};
@@ -107,7 +113,7 @@ class NotesScreenComponent extends BaseScreenComponent {
}
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;
Folder.delete(folderId).then(() => {
@@ -219,6 +225,7 @@ const NotesScreen = connect(
notes: state.notes,
notesSource: state.notesSource,
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
showCompletedTodos: state.settings.showCompletedTodos,
theme: state.settings.theme,
noteSelectionEnabled: state.noteSelectionEnabled,
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 BaseModel = require('lib/BaseModel.js');
const Note = require('lib/models/Note.js');
const Setting = require('lib/models/Setting.js');
const shared = {};
@@ -10,15 +11,19 @@ shared.noteExists = async function(noteId) {
return !!existingNote;
}
shared.saveNoteButton_press = async function(comp) {
shared.saveNoteButton_press = async function(comp, folderId = null) {
let note = Object.assign({}, comp.state.note);
// Note has been deleted while user was modifying it. In that case, we
// just save a new note by clearing the note ID.
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
if (!note.parent_id) {
let folder = await Folder.defaultFolder();
if (folderId) {
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;
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 = {};
shared.renderFolders = function(props, renderItem) {
let items = [];
for (let i = 0; i < props.folders.length; i++) {
let folder = props.folders[i];
items.push(renderItem(folder, props.selectedFolderId == folder.id && props.notesParentType == 'Folder'));
function folderHasChildren_(folders, folderId) {
for (let i = 0; i < folders.length; i++) {
let folder = folders[i];
if (folder.parent_id === folderId) return true;
}
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;
}
shared.renderFolders = function(props, renderItem) {
return renderFoldersRecursive_(props, renderItem, [], '', 0);
}
shared.renderTags = function(props, renderItem) {
let tags = props.tags.slice();
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.paddingLeft = 0;
styles.folderButtonText = Object.assign({}, styles.buttonText);
styles.folderButtonSelected = Object.assign({}, styles.folderButton);
styles.folderButtonSelected.backgroundColor = theme.selectedColor;
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.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) {
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' });
}
folderItem(folder, selected) {
const iconComp = selected ? <Icon name='md-folder-open' style={this.styles().folderIcon} /> : <Icon name='md-folder' style={this.styles().folderIcon} />;
const folderButtonStyle = selected ? this.styles().folderButtonSelected : this.styles().folderButton;
folderItem(folder, selected, hasChildren, depth) {
const theme = themeStyle(this.props.theme);
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 (
<TouchableOpacity key={folder.id} onPress={() => { this.folder_press(folder) }}>
<View style={folderButtonStyle}>
{ iconComp }
<Text numberOfLines={1} style={this.styles().folderButtonText}>{Folder.displayTitle(folder)}</Text>
</View>
</TouchableOpacity>
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
<TouchableOpacity style={{ flex: 1 }} onPress={() => { this.folder_press(folder) }}>
<View style={folderButtonStyle}>
<Text numberOfLines={1} style={this.styles().folderButtonText}>{Folder.displayTitle(folder)}</Text>
</View>
</TouchableOpacity>
{ iconWrapper }
</View>
);
}
@@ -204,9 +237,6 @@ class SideMenuContentComponent extends Component {
return (
<View style={style}>
<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}>
{ items }
</ScrollView>
@@ -229,6 +259,7 @@ const SideMenuContent = connect(
locale: state.settings.locale,
theme: state.settings.theme,
opacity: state.sideMenuOpenPercent,
collapsedFolderIds: state.collapsedFolderIds,
};
}
)(SideMenuContentComponent)

View File

@@ -38,7 +38,7 @@ class FileApiDriverWebDav {
}
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.
const propStat = this.api().arrayFromJson(resource, ['d:propstat']);
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
// so that they are processed again on the next sync.
newContext.statsCache = null;
newContext.statIdsCache = null;
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
// 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);
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
// 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;
while (currentVersionIndex < existingDatabaseVersions.length - 1) {
@@ -344,6 +344,10 @@ class JoplinDatabase extends Database {
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] });
await this.transactionExecBatch(queries);

View File

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

View File

@@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js');
const { _ } = require('lib/locale.js');
const moment = require('moment');
const { markdownUtils } = require('lib/markdown-utils.js');
class BaseItem extends BaseModel {
@@ -649,6 +650,15 @@ class BaseItem extends BaseModel {
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;

View File

@@ -18,7 +18,7 @@ class Folder extends BaseItem {
static async serialize(folder) {
let fieldNames = this.fieldNames();
fieldNames.push('type_');
lodash.pull(fieldNames, 'parent_id');
//lodash.pull(fieldNames, 'parent_id');
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) {
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;
@@ -79,6 +84,11 @@ class Folder extends BaseItem {
for (let i = 0; i < noteIds.length; 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);
@@ -101,6 +111,7 @@ class Folder extends BaseItem {
return {
type_: this.TYPE_FOLDER,
id: this.conflictFolderId(),
parent_id: '',
title: this.conflictFolderTitle(),
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');
}
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
// 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

View File

@@ -107,7 +107,7 @@ class Note extends BaseItem {
return BaseModel.TYPE_NOTE;
}
static linkedResourceIds(body) {
static linkedItemIds(body) {
// For example: ![](:/fcca2938a96a22570e8eae2565bc6b0b)
if (!body || body.length <= 32) return [];
const matches = body.match(/\(:\/.{32}\)/g);
@@ -115,6 +115,35 @@ class Note extends BaseItem {
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 = '') {
let output = super.new();
output.parent_id = parentId;
@@ -208,6 +237,7 @@ class Note extends BaseItem {
if (!options.conditionsParams) options.conditionsParams = [];
if (!options.fields) options.fields = this.previewFields();
if (!options.uncompletedTodosOnTop) options.uncompletedTodosOnTop = false;
if (!('showCompletedTodos' in options)) options.showCompletedTodos = true;
if (parentId == BaseItem.getClass('Folder').conflictFolderId()) {
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) {
let cond = options.conditions.slice();
cond.push('is_todo = 1');

View File

@@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
const { sprintf } = require('sprintf-js');
const ObjectUtils = require('lib/ObjectUtils');
const { toTitleCase } = require('lib/string-utils.js');
const { rtrimSlashes } = require('lib/path-utils.js');
const { _, supportedLocalesToLanguages, defaultLocale } = require('lib/locale.js');
const { shim } = require('lib/shim');
@@ -60,6 +61,7 @@ class Setting extends BaseModel {
return output;
}},
'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: () => {
const Note = require('lib/models/Note');
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: () => {
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.activeMasterKeyId': { value: '', type: Setting.TYPE_STRING, public: false },
@@ -122,6 +126,8 @@ class Setting extends BaseModel {
} catch (error) {
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`.'); } },
'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;
c.value = this.formatValue(c.key, c.value);
c.value = this.filterValue(c.key, c.value);
this.cache_.push(c);
}
@@ -237,6 +244,7 @@ class Setting extends BaseModel {
if (!this.cache_) throw new Error('Settings have not been initialized!');
value = this.formatValue(key, value);
value = this.filterValue(key, value);
for (let i = 0; i < this.cache_.length; i++) {
let c = this.cache_[i];
@@ -307,6 +315,11 @@ class Setting extends BaseModel {
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) {
const md = this.settingMetadata(key);

View File

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

View File

@@ -26,6 +26,7 @@ const defaultState = {
appState: 'starting',
hasDisabledSyncItems: false,
newNote: null,
collapsedFolderIds: [],
};
const stateUtils = {};
@@ -51,6 +52,23 @@ function stateHasEncryptedItems(state) {
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
function handleItemDelete(state, action) {
let newState = Object.assign({}, state);
@@ -339,6 +357,26 @@ const reducer = (state = defaultState, action) => {
newState.folders = action.items;
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':
newState = Object.assign({}, state);

View File

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

View File

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

View File

@@ -18,22 +18,19 @@ const { uuid } = require('lib/uuid.js');
class InteropService_Importer_Raw extends InteropService_Importer_Base {
async exec(result) {
const noteIdMap = {};
const folderIdMap = {};
const resourceIdMap = {};
const tagIdMap = {};
const itemIdMap = {};
const createdResources = {};
const noteTagsToCreate = [];
const destinationFolderId = this.options_.destinationFolderId;
const replaceResourceNoteIds = (noteBody) => {
const replaceLinkedItemIds = async (noteBody) => {
let output = noteBody;
const resourceIds = Note.linkedResourceIds(noteBody);
const itemIds = Note.linkedItemIds(noteBody);
for (let i = 0; i < resourceIds.length; i++) {
const id = resourceIds[i];
if (!resourceIdMap[id]) resourceIdMap[id] = uuid.create();
output = output.replace(new RegExp(id, 'gi'), resourceIdMap[id]);
for (let i = 0; i < itemIds.length; i++) {
const id = itemIds[i];
if (!itemIdMap[id]) itemIdMap[id] = uuid.create();
output = output.replace(new RegExp(id, 'gi'), itemIdMap[id]);
}
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.
// - 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 (!folderIdMap[item.parent_id]) {
if (!itemIdMap[item.parent_id]) {
if (destinationFolderId) {
folderIdMap[item.parent_id] = destinationFolderId;
itemIdMap[item.parent_id] = destinationFolderId;
} else if (!folderExists(stats, item.parent_id)) {
const parentFolder = await defaultFolder();
folderIdMap[item.parent_id] = parentFolder.id;
itemIdMap[item.parent_id] = parentFolder.id;
} else {
folderIdMap[item.parent_id] = uuid.create();
itemIdMap[item.parent_id] = uuid.create();
}
}
const noteId = uuid.create();
noteIdMap[item.id] = noteId;
item.id = noteId;
item.parent_id = folderIdMap[item.parent_id];
item.body = replaceResourceNoteIds(item.body);
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id]; //noteId;
item.parent_id = itemIdMap[item.parent_id];
item.body = await replaceLinkedItemIds(item.body);
} else if (itemType === BaseModel.TYPE_FOLDER) {
if (destinationFolderId) continue;
if (!folderIdMap[item.id]) folderIdMap[item.id] = uuid.create();
item.id = folderIdMap[item.id];
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
item.title = await Folder.findUniqueFolderTitle(item.title);
} else if (itemType === BaseModel.TYPE_RESOURCE) {
if (!resourceIdMap[item.id]) resourceIdMap[item.id] = uuid.create();
item.id = resourceIdMap[item.id];
if (!itemIdMap[item.id]) itemIdMap[item.id] = uuid.create();
item.id = itemIdMap[item.id];
createdResources[item.id] = item;
} else if (itemType === BaseModel.TYPE_TAG) {
const tag = await Tag.loadByTitle(item.title);
if (tag) {
tagIdMap[item.id] = tag.id;
itemIdMap[item.id] = tag.id;
continue;
}
const tagId = uuid.create();
tagIdMap[item.id] = tagId;
itemIdMap[item.id] = tagId;
item.id = tagId;
} else if (itemType === BaseModel.TYPE_NOTE_TAG) {
noteTagsToCreate.push(item);
@@ -123,8 +119,8 @@ class InteropService_Importer_Raw extends InteropService_Importer_Base {
for (let i = 0; i < noteTagsToCreate.length; i++) {
const noteTag = noteTagsToCreate[i];
const newNoteId = noteIdMap[noteTag.note_id];
const newTagId = tagIdMap[noteTag.tag_id];
const newNoteId = itemIdMap[noteTag.note_id];
const newTagId = itemIdMap[noteTag.tag_id];
if (!newNoteId) {
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++) {
const resourceFilePath = this.sourcePath_ + '/resources/' + resourceStats[i].path;
const oldId = Resource.pathToId(resourceFilePath);
const newId = resourceIdMap[oldId];
const newId = itemIdMap[oldId];
if (!newId) {
result.warnings.push(sprintf('Resource file is not referenced in any note and so was not imported: %s', oldId));
continue;

View File

@@ -44,7 +44,7 @@ class ResourceService extends BaseService {
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
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);
} else if (change.type === ItemChange.TYPE_DELETE) {
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 { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
const { FsDriverNode } = require('lib/fs-driver-node.js');
const urlValidator = require('valid-url');
function shimInit() {
shim.fsDriver = () => { throw new Error('Not implemented') }
@@ -34,6 +35,23 @@ function shimInit() {
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) {
if (shim.isElectron()) { // For Electron
const nativeImage = require('electron').nativeImage;
@@ -57,17 +75,7 @@ function shimInit() {
image = image.resize(options);
let buffer = null;
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');
await shim.writeImageToFile(image, mime, targetPath);
} else { // For the CLI tool
const sharp = require('sharp');
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 { uuid } = require('lib/uuid.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') {
const result = await resizeImage_(filePath, targetPath, resource.mime);
} 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 Resource.save(resource, { isNew: true });
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(note.body.substr(position));
const newNote = Object.assign({}, note, {
body: newBody.join('\n\n'),
@@ -133,6 +150,9 @@ function shimInit() {
}
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 nodeFetch(url, 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
// 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
// a few seconds ahead of what it was set with setTimestamp()
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
// 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
// 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.
if (!hasCancelled) {
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