1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-09-05 20:56:22 +02:00

Compare commits

...

96 Commits

Author SHA1 Message Date
Laurent Cozic
feb7778fe4 Electron release v1.0.93 2018-05-14 12:18:40 +01:00
Laurent Cozic
b45185780f Fixed auto-update download url 2018-05-14 12:18:00 +01:00
Laurent Cozic
4e032c0c55 Electron release v1.0.92 2018-05-14 11:36:40 +01:00
Laurent Cozic
2e2b35dfeb Fixed import of tables and added tests 2018-05-14 11:36:02 +01:00
Laurent Cozic
526ef7e1d2 Don't add new table when processing a table 2018-05-14 11:24:19 +01:00
Laurent Cozic
a37005446a Fixed tests 2018-05-14 11:23:18 +01:00
Laurent Cozic
e012b927dc Merge branch 'clipper' 2018-05-14 11:09:57 +01:00
Laurent Cozic
359b8d5545 Various tweaks to build portable version 2018-05-14 11:08:33 +01:00
Laurent Cozic
23c592b322 Merge branch 'master' of github.com:laurent22/joplin 2018-05-14 10:31:32 +01:00
Laurent Cozic
9aeddf86f4 Updated PortableApps build files 2018-05-14 10:31:16 +01:00
FoxMaSk
0e1887988e add OneDrive
add OneDrive to the general description of the supported services
2018-05-13 14:42:36 +02:00
Laurent Cozic
394f2df664 All: More robust HTML to MD conversion and started adding test units for it 2018-05-12 11:48:39 +01:00
Laurent Cozic
2a04378a0d Merge branch 'clipper' 2018-05-11 15:32:45 +01:00
Laurent Cozic
bac68f2c42 All: Various changes to improve import of HTML 2018-05-11 15:29:44 +01:00
Laurent Cozic
0f0ff86ffa Merge pull request #409 from petrz12/better-evernote-import
Merge monospace text lines when importing from Evernote
2018-05-11 15:25:26 +01:00
Laurent Cozic
8b38752cbf Fix downloads 2018-05-11 14:36:52 +01:00
Laurent Cozic
3c24589450 Fixed deployment script 2018-05-11 14:36:22 +01:00
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
petrz12
9f2666aef9 Evernote code block conversion added, better handling of monospace empty lines 2018-05-04 00:23:19 +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
petrz12
f136664c11 codeblock detection added 2018-05-02 22:39:50 +02: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
petrz12
c8c9f80cc5 Making monospace merging permanent 2018-04-21 13:55:18 +02:00
petrz12
69ddcc6e30 Remove debug lines, fixing some comments 2018-04-18 14:13:19 +02:00
petrz12
16554b22c7 Merge monospace text lines when importing from Evernote 2018-04-10 22:59:45 +02:00
143 changed files with 5594 additions and 2766 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

@@ -14,6 +14,8 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.3\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
msgid "To delete a tag, untag the associated notes."
msgstr "Pour supprimer une vignette, enlever là des notes associées."
@@ -229,7 +231,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 +367,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>."
@@ -764,7 +768,7 @@ msgid ""
"how the notes or notebooks were originally encrypted."
msgstr ""
"Note : seule une clef maître va être utilisée pour le cryptage (celle "
"marquée comme \"actif\" ci-dessus). N'importe quel clef peut-être utilisée "
"marquée comme \"actif\" ci-dessus). N'importe quelle clef peut être utilisée "
"pour le décryptage, selon la façon dont les notes ou carnets étaient cryptés "
"à l'origine."
@@ -848,6 +852,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 +1070,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 +1126,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 +1409,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,7 +58,7 @@
"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",

View File

@@ -8,8 +8,16 @@ rsync -a "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
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)
else
if [[ $TEST_FILE != "" ]]; then
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
fi
exit
fi
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js)
(cd "$ROOT_DIR" && npm test tests-build/encryption.js
(cd "$ROOT_DIR" && npm test tests-build/ArrayUtils.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Setting.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Note.js)
(cd "$ROOT_DIR" && npm test tests-build/models_Folder.js)
(cd "$ROOT_DIR" && npm test tests-build/services_InteropService.js)
(cd "$ROOT_DIR" && npm test tests-build/HtmlToMd.js)

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,64 @@
require('app-module-path').addPath(__dirname);
const { time } = require('lib/time-utils.js');
const { filename } = require('lib/path-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');
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');
const stringToStream = require('string-to-stream')
jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit
process.on('unhandledRejection', (reason, p) => {
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});
describe('HtmlToMd', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await switchClient(1);
done();
});
it('should convert from HTML to Markdown', asyncTest(async () => {
const basePath = __dirname + '/html_to_md';
const files = await shim.fsDriver().readDirStats(basePath);
for (let i = 0; i < files.length; i++) {
const htmlFilename = files[i].path;
if (htmlFilename.indexOf('.html') < 0) continue;
const htmlPath = basePath + '/' + htmlFilename;
const mdPath = basePath + '/' + filename(htmlFilename) + '.md';
// if (htmlFilename !== 'tableWithNewLines.html') continue;
const html = await shim.fsDriver().readFile(htmlPath);
const expectedMd = await shim.fsDriver().readFile(mdPath);
const contentStream = stringToStream(html);
const actualMd = await enexXmlToMd(contentStream, []);
if (actualMd !== expectedMd) {
console.info('');
console.info('Error converting file: ' + htmlFilename);
console.info('--------------------------------- Got:');
console.info(actualMd.split('\n'));
console.info('--------------------------------- Expected:');
console.info(expectedMd.split('\n'));
console.info('--------------------------------------------');
console.info('');
expect(false).toBe(true);
return;
} else {
expect(true).toBe(true)
}
}
}));
});

View File

@@ -0,0 +1,16 @@
<div>
<p>For example, consider a web page like this:</p>
<pre class="brush: html line-numbers language-html"><code class=" language-html"><span class="token doctype">&lt;!DOCTYPE html&gt;</span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>html</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>head</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>meta</span> <span class="token attr-name">http-equiv</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>content-type<span class="token punctuation">"</span></span> <span class="token attr-name">content</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>text/html; charset<span class="token punctuation">=</span>utf-8<span class="token punctuation">"</span></span> <span class="token punctuation">/&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>head</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>body</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;</span>script</span> <span class="token attr-name">src</span><span class="token attr-value"><span class="token punctuation">=</span><span class="token punctuation">"</span>page-scripts/page-script.js<span class="token punctuation">"</span></span><span class="token punctuation">&gt;</span></span><span class="token script language-javascript"></span><span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>script</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>body</span><span class="token punctuation">&gt;</span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&lt;/</span>html</span><span class="token punctuation">&gt;</span></span><span class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre>
<p>The script "page-script.js" does this:</p>
</div>

View File

@@ -0,0 +1,14 @@
For example, consider a web page like this:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
</head>
<body>
<script src="page-scripts/page-script.js"></script>
</body>
</html>
The script "page-script.js" does this:

View File

@@ -0,0 +1,9 @@
<div>
<div class="note">
<p>Values added to the global scope of a content script with</p>
</div>
<h2 id="Loading_content_scripts">Loading content scripts</h2>
<p>You can load a content script into a web page in one of three ways:</p>
</div>

View File

@@ -0,0 +1,5 @@
Values added to the global scope of a content script with
## Loading content scripts
You can load a content script into a web page in one of three ways:

View File

@@ -0,0 +1,3 @@
<div>
<p>Similarly, I need another regex to match double newlines (<code>\n\n</code>) that are not part of a longer run of newline characters like <code>\n\n\n</code> or <code>\n\n\n\n\n\n</code> etc.</p>
</div>

View File

@@ -0,0 +1 @@
Similarly, I need another regex to match double newlines (`\n\n`) that are not part of a longer run of newline characters like `\n\n\n` or `\n\n\n\n\n\n` etc.

View File

@@ -0,0 +1,3 @@
<div>
<p>the&nbsp;<code><a href="/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onConnect">runtime.onConnect</a></code> listener gets passed its own <code><a href="/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port">runtime.Port</a></code> object.</p>
</div>

View File

@@ -0,0 +1 @@
the `[runtime.onConnect](/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onConnect)` listener gets passed its own `[runtime.Port](/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/Port)` object.

View File

@@ -0,0 +1,17 @@
<div>
<p>Liste de courses</p>
<div>
<div><en-todo checked="true"/>Pizzas</div>
<div><en-todo checked="true"/>Pain</div>
<div><en-todo checked="true"/>Jambon</div>
</div>
<div><br/></div>
<div>
<div><en-todo checked="true"/>On its own</div>
</div>
<p>End</p>
</div>

View File

@@ -0,0 +1,9 @@
Liste de courses
- [X] Pizzas
- [X] Pain
- [X] Jambon
- [X] On its own
End

View File

@@ -0,0 +1,5 @@
<div>
<p>Something something</p>
<p>Blablbla blabla lbla</p>
<p>Last line</p>
</div>

View File

@@ -0,0 +1,5 @@
Something something
Blablbla blabla lbla
Last line

View File

@@ -0,0 +1,3 @@
<div>
<table style="-evernote-table:true;border-collapse:collapse;width:100%;table-layout:fixed;margin-left:0px;"><tr><td style="border-style:solid;border-width:1px;border-color:rgb(211,211,211);padding:10px;margin:0px;width:50%;"><div>line 1</div><div>line 2</div></td><td style="border-style:solid;border-width:1px;border-color:rgb(211,211,211);padding:10px;margin:0px;width:50%;"><div><br/></div></td></tr><tr><td style="border-style:solid;border-width:1px;border-color:rgb(211,211,211);padding:10px;margin:0px;width:50%;"><div>aaaaaa</div></td><td style="border-style:solid;border-width:1px;border-color:rgb(211,211,211);padding:10px;margin:0px;width:50%;"><div>line 3</div><div>line 4</div></td></tr></table>
</div>

View File

@@ -0,0 +1,4 @@
| | |
| --- | --- |
| line 1<br>line 2 | |
| aaaaaa | line 3<br>line 4 |

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);
}));
});

File diff suppressed because it is too large Load Diff

View File

@@ -69,7 +69,7 @@ class ElectronAppWrapper {
}))
// Uncomment this to view errors if the application does not start
// if (this.env_ === 'dev') this.win_.webContents.openDevTools();
if (this.env_ === 'dev') this.win_.webContents.openDevTools();
this.win_.on('close', (event) => {
// If it's on macOS, the app is completely closed only if the user chooses to close the app (willQuitApp_ will be 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(30, { 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

@@ -1,7 +1,9 @@
const { dialog } = require('electron')
const { shim } = require('lib/shim');
const { Logger } = require('lib/logger.js');
const { _ } = require('lib/locale.js');
const fetch = require('node-fetch');
const { fileExtension } = require('lib/path-utils.js');
const packageInfo = require('./packageInfo.js');
const compareVersions = require('compare-versions');
@@ -43,11 +45,16 @@ 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 && asset.name.indexOf('Setup') >= 0) {
const ext = fileExtension(asset.name);
if (platform === 'win32' && ext === 'exe') {
if (shim.isPortable()) {
found = asset.name == 'JoplinPortable.exe';
} else {
found = !!asset.name.match(/^Joplin-Setup-[\d.]+\.exe$/);
}
} else if (platform === 'darwin' && ext === 'dmg') {
found = true;
} else if (platform === 'darwin' && asset.name.indexOf('.dmg') >= 0) {
found = true;
} else if (platform === 'linux' && asset.name.indexOf('.AppImage') >= 0) {
} else if (platform === 'linux' && ext === '.AppImage') {
found = true;
}
@@ -57,12 +64,11 @@ async function fetchLatestRelease() {
}
}
if (!downloadUrl) throw new Error('Cannot find download Url: ' + JSON.stringify(json).substr(0,500));
return {
version: version,
downloadUrl: downloadUrl,
notes: json.body,
pageUrl: json.html_url,
};
}
@@ -86,6 +92,9 @@ function checkForUpdates(inBackground, window, logFilePath) {
checkInBackground_ = inBackground;
fetchLatestRelease().then(release => {
autoUpdateLogger_.info('Current version: ' + packageInfo.version);
autoUpdateLogger_.info('Latest version: ' + release.version);
if (compareVersions(release.version, packageInfo.version) <= 0) {
if (!checkInBackground_) dialog.showMessageBox({ message: _('Current version is up-to-date.') })
} else {
@@ -97,7 +106,7 @@ function checkForUpdates(inBackground, window, logFilePath) {
buttons: [_('Yes'), _('No')]
});
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl);
if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl);
}
}).catch(error => {
autoUpdateLogger_.error(error);

View File

@@ -20,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
@@ -47,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;
@@ -55,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
@@ -72,6 +79,62 @@ 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() {
@@ -243,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) {
@@ -421,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;
@@ -428,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, () => {
@@ -439,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);
}
}
@@ -473,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;
@@ -509,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);
}
}
}
@@ -674,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 = [];
@@ -738,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"
@@ -793,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,
};
};

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.86",
"version": "1.0.93",
"description": "Joplin for Desktop",
"main": "main.js",
"scripts": {
@@ -48,6 +48,9 @@
"oneClick": false,
"allowToChangeInstallationDirectory": true
},
"portable": {
"artifactName": "${productName}Portable.${ext}"
},
"mac": {
"icon": "../../Assets/macOs.icns",
"asar": false
@@ -92,6 +95,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",

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 (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.85/Joplin-Setup-1.0.85.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.85/Joplin-1.0.85.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.85/Joplin-1.0.85-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-Setup-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.120/joplin-v1.0.120.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`.
@@ -124,7 +124,7 @@ Joplin can export to the JEX format (Joplin Export file), which is a tar file th
One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. Most of the synchronisation process is done at an abstract level and access to external services, such as Nextcloud or Dropbox, is done via lightweight drivers. It is easy to support new services by creating simple drivers that provide a filesystem-like interface, i.e. the ability to read, write, delete and list items. It is also simple to switch from one service to another or to even sync to multiple services at once. Each note, notebook, tags, as well as the relation between items is transmitted as plain text files during synchronisation, which means the data can also be moved to a different application, can be easily backed up, inspected, etc.
Currently, synchronisation is possible with Nextcloud, Dropbox (by default) or the local filesystem. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
Currently, synchronisation is possible with Nextcloud, Dropbox (by default), OneDrive or the local filesystem. To setup synchronisation please follow the instructions below. After that, the application will synchronise in the background whenever it is running, or you can click on "Synchronise" to start a synchronisation manually.
## Nextcloud synchronisation
@@ -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,6 +203,16 @@ 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:
@@ -241,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.
@@ -250,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).
@@ -275,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 2097298
versionName "1.0.120"
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;
}
@@ -361,6 +377,14 @@ class BaseApplication {
return flags.matched;
}
determineProfileDir(initArgs) {
if (initArgs.profileDir) return initArgs.profileDir;
if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) return process.env.PORTABLE_EXECUTABLE_DIR + '/JoplinProfile';
return os.homedir() + '/.config/' + Setting.value('appName');
}
async start(argv) {
let startFlags = await this.handleStartFlags_(argv);
@@ -368,17 +392,11 @@ class BaseApplication {
let initArgs = startFlags.matched;
if (argv.length) this.showPromptString_ = false;
// if (process.argv[1].indexOf('joplindev') >= 0) {
// if (!initArgs.profileDir) initArgs.profileDir = '/mnt/d/Temp/TestNotes2';
// initArgs.logLevel = Logger.LEVEL_DEBUG;
// initArgs.env = 'dev';
// }
let appName = initArgs.env == 'dev' ? 'joplindev' : 'joplin';
if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop';
Setting.setConstant('appName', appName);
const profileDir = initArgs.profileDir ? initArgs.profileDir : os.homedir() + '/.config/' + Setting.value('appName');
const profileDir = this.determineProfileDir(initArgs);
const resourceDir = profileDir + '/resources';
const tempDir = profileDir + '/tmp';

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

@@ -0,0 +1,16 @@
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');
const stringToStream = require('string-to-stream')
class HtmlToMarkdownParser {
async parse(html, options = {}) {
if (!options.baseUrl) options.baseUrl = '';
const contentStream = stringToStream(html);
const markdown = await enexXmlToMd(contentStream, [], options);
return markdown;
}
}
module.exports = HtmlToMarkdownParser;

View File

@@ -157,9 +157,12 @@ class MdToHtml {
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) {
@@ -389,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
@@ -435,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}

View File

@@ -5,15 +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, {
displayMode: tagType === 'block',
});
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>';
@@ -29,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
@@ -43,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

@@ -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,7 +122,7 @@ 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(':');

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,5 +1,5 @@
const React = require('react'); const Component = React.Component;
const { Platform, Clipboard, 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');
@@ -364,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;
}
@@ -403,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 });
}
@@ -462,6 +475,7 @@ 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(); } });
@@ -506,7 +520,13 @@ class NoteScreenComponent extends BaseScreenComponent {
this.saveOneProperty('body', newBody);
};
bodyComponent = <NoteBodyViewer onJoplinLinkClick={this.onJoplinLinkClick_} 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;

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 = {};
@@ -20,7 +21,9 @@ shared.saveNoteButton_press = async function(comp, folderId = null) {
if (folderId) {
note.parent_id = folderId;
} else if (!note.parent_id) {
let folder = await Folder.defaultFolder();
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

@@ -5,8 +5,219 @@ const BLOCK_CLOSE = "[[BLOCK_CLOSE]]";
const NEWLINE = "[[NEWLINE]]";
const NEWLINE_MERGED = "[[MERGED]]";
const SPACE = "[[SPACE]]";
// For monospace font detection (Courier, Menlo, Moncaco)
const MONOSPACE_OPEN = "[[MONOSPACE_OPEN]]";
const MONOSPACE_CLOSE = "[[MONOSPACE_CLOSE]]";
// This function will return a list of all monospace sections with a flag saying whether they can be merged or not
function findMonospaceSections(md) {
let temp = [];
let sections = [];
let section = null;
// This variable is used twice: to detected if a newline is between monospace sections and if a newline is inside monospace section
let mergeWithPrevious = true;
let last = "";
for (let i = 0; i < md.length; i++) {
let v = md[i];
if (v == MONOSPACE_OPEN) {
if (section != null) throw new Error('Monospace open tag detected while the previous was not closed'); // Sanity check, but normally not possible
let monospaceSection = {
openIndex: null,
closeIndex: null,
mergeAllowed: true,
mergeWithPrevious: mergeWithPrevious,
isEmptyLine: false,
}
section = monospaceSection;
// Remember where monospace section begins, later it will be replaced with appropriate markdown (` or ```)
section.openIndex = temp.push(v) - 1;
// Add an empty string, it can be later replaced with newline if necessary
temp.push("");
if (last != BLOCK_OPEN) {
// We cannot merge inline code
section.mergeAllowed = false;
}
// Reset to detect if monospace section contains a newline
mergeWithPrevious = true;
} else if (v == MONOSPACE_CLOSE) {
if (section == null) throw new Error('Monospace tag was closed without being open before'); // Sanity check, but normally not possible
if (section.closeIndex != null) throw new Error('Monospace tag is closed for the second time'); // Sanity check, but normally not possible
// Add an empty string, it can be later replaced with newline if necessary
temp.push("");
// Remember where monospace section ends, later it will be replaced with appropriate markdown (` or ```)
section.closeIndex = temp.push(v) - 1;
if (md[i+1] != BLOCK_CLOSE) {
// We cannot merge inline code
section.mergeAllowed = false;
}
section.isEmptyLine = mergeWithPrevious;
sections.push(section);
// Reset
section = null;
mergeWithPrevious = true;
} else {
// We can merge only if monospace sections are separated by newlines
if (v != NEWLINE && v != BLOCK_OPEN && v != BLOCK_CLOSE) {
mergeWithPrevious = false;
}
temp.push(v);
}
last = v;
}
return {
md: temp,
monospaceSections: sections,
};
}
// This function is looping over monospace sections and merging what it can merge
function mergeMonospaceSections(md, sections) {
const USE_BLOCK_TAG = 1;
const USE_INLINE_TAG = 2;
const USE_EMPTY_TAG = 3;
const toMonospace = (md, section, startTag, endTag) => {
// It looks better when empty lines are not inlined
if (startTag == USE_INLINE_TAG && section.isEmptyLine) {
startTag = USE_EMPTY_TAG;
endTag = USE_EMPTY_TAG;
}
switch (startTag) {
case USE_BLOCK_TAG:
md[section.openIndex] = "```";
md[section.openIndex + 1] = NEWLINE;
break;
case USE_INLINE_TAG:
md[section.openIndex] = "`";
break;
case USE_EMPTY_TAG:
md[section.openIndex] = "";
break;
}
switch (endTag) {
case USE_BLOCK_TAG:
// We don't add a NEWLINE if there already is a NEWLINE
if (md[section.closeIndex - 2] == NEWLINE) {
md[section.closeIndex - 1] = "";
} else {
md[section.closeIndex - 1] = NEWLINE;
}
md[section.closeIndex] = "```";
break;
case USE_INLINE_TAG:
md[section.closeIndex] = "`";
break;
case USE_EMPTY_TAG:
md[section.closeIndex] = "";
break;
}
}
const getSection = () => {
return sections.shift();
}
const getMergeableSection = (first = null) => {
if (first) {
sections.unshift(first);
}
while (sections.length) {
s = sections.shift();
if (s.mergeAllowed) {
return s;
}
// If cannot merge then convert into inline code
toMonospace(md, s, USE_INLINE_TAG, USE_INLINE_TAG);
}
return null;
}
let left = getMergeableSection();
let right = null;
while (left) {
let isFirst = true;
right = getSection();
while (right && right.mergeAllowed && right.mergeWithPrevious) {
// We can merge left and right
if (isFirst) {
isFirst = false;
// First section
toMonospace(md, left, USE_BLOCK_TAG, USE_EMPTY_TAG);
} else {
// Middle section
toMonospace(md, left, USE_EMPTY_TAG, USE_EMPTY_TAG);
}
left = right;
right = getSection();
}
if (isFirst) {
// Could not merge, convert to inline code
toMonospace(md, left, USE_INLINE_TAG, USE_INLINE_TAG);
} else {
// Was merged, add block end tag
toMonospace(md, left, USE_EMPTY_TAG, USE_BLOCK_TAG);
}
left = getMergeableSection(right);
}
}
// This function will try to merge monospace sections
// It works in two phases:
// 1) It will find all monospace sections
// 2) It will merge all monospace sections where merge is allowed
function mergeMonospaceSectionsWrapper(md, ignoreMonospace = false) {
if (!ignoreMonospace) {
const result = findMonospaceSections(md);
if (result.monospaceSections.length > 0) {
mergeMonospaceSections(result.md, result.monospaceSections);
}
md = result.md;
}
// Remove empty items, it is necessary for correct function of newline merging happening outside this function
let temp = []
for (let i = 0; i < md.length; i++) {
let v = md[i];
if (ignoreMonospace && (v == MONOSPACE_OPEN || v == MONOSPACE_CLOSE)) {
continue; // skip
}
if (v != "") {
temp.push(v);
}
}
return temp;
}
function processMdArrayNewLines(md, isTable = false) {
// console.info(md);
// Try to merge MONOSPACE sections, works good when when not parsing a table
// md = mergeMonospaceSectionsWrapper(md, isTable);
function processMdArrayNewLines(md) {
while (md.length && md[0] == BLOCK_OPEN) {
md.shift();
}
@@ -80,6 +291,8 @@ function processMdArrayNewLines(md) {
}
}
// console.info(md);
let output = '';
let previous = '';
let start = true;
@@ -103,7 +316,153 @@ function processMdArrayNewLines(md) {
if (!output.trim().length) return '';
return output;
let lines = output.replace(/\\r/g, '').split('\n');
return convertSingleLineCodeBlocksToInline(formatMdLayout(lines)).join('\n');
}
// While the processMdArrayNewLines() function adds newlines in a way that's technically correct, the resulting Markdown can look messy.
// This is because while a "block" element should be surrounded by newlines, in practice, some should be surrounded by TWO new lines, while
// others by only ONE.
//
// For instance, this:
//
// <li>one</li>
// <li>two</li>
// <li>three</li>
//
// should result in this:
//
// - one
// - two
// - three
//
// While this:
//
// <p>Some long paragraph</p><p>And another one</p><p>And the last paragraph</p>
//
// should result in this:
//
// Some long paragraph
//
// And another one
//
// And the last paragraph
//
// So in one case, one newline between tags, and in another two newlines. In HTML this would be done via CSS, but in Markdown we need
// to add new lines. It's also important to get these newlines right because two blocks of text next to each others might be renderered
// differently than if there's a newlines between them. So the function below parses the almost final MD and add new lines depending
// on various rules.
const isHeading = function(line) {
return !!line.match(/#+\s/);
}
const isListItem = function(line) {
return line && line.trim().indexOf('- ') === 0;
}
const isCodeLine = function(line) {
return line && line.indexOf('\t') === 0;
}
const isTableLine = function(line) {
return line.indexOf('|') === 0;
}
const isPlainParagraph = function(line) {
if (!line || !line.length) return false;
if (isListItem(line)) return false;
if (isHeading(line)) return false;
if (isCodeLine(line)) return false;
if (isTableLine(line)) return false;
return true;
}
function formatMdLayout(lines) {
let previous = '';
let newLines = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Add a new line at the end of a list of items
if (isListItem(previous) && line && !isListItem(line)) {
newLines.push('');
// Add a new line at the beginning of a list of items
} else if (isListItem(line) && previous && !isListItem(previous)) {
newLines.push('');
// Add a new line before a heading
} else if (isHeading(line) && previous) {
newLines.push('');
// Add a new line after a heading
} else if (isHeading(previous) && line) {
newLines.push('');
// Add a new line at beginning of paragraph
} else if (isPlainParagraph(line) && previous) {
newLines.push('');
// Add a new line at end of paragraph
} else if (isPlainParagraph(previous) && line) {
newLines.push('');
}
newLines.push(line);
previous = newLines[newLines.length - 1];
}
return newLines;
}
function lineStartsWithDelimiter(line) {
if (!line || !line.length) return false;
return ' ,.;:)]}'.indexOf(line[0]) >= 0;
}
function convertSingleLineCodeBlocksToInline(lines) {
let newLines = [];
let currentCodeLines = [];
let codeLineCount = 0;
const processCurrentCodeLines = (line) => {
if (codeLineCount === 1) {
const inlineCode = currentCodeLines.join('').trim();
newLines[newLines.length - 1] += '`' + inlineCode + '`';
if (line) newLines[newLines.length - 1] += (lineStartsWithDelimiter(line) ? '' : ' ') + line;
} else {
newLines = newLines.concat(currentCodeLines);
newLines.push(line);
}
currentCodeLines = [];
codeLineCount = 0;
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (isCodeLine(line)) {
currentCodeLines.push(line);
codeLineCount++;
} else if (!line.trim()) {
currentCodeLines.push(line);
} else {
if (currentCodeLines.length) {
processCurrentCodeLines(line);
} else {
newLines.push(line);
}
}
}
if (currentCodeLines.length) processCurrentCodeLines('');
return newLines;
}
function isWhiteSpace(c) {
@@ -134,8 +493,22 @@ function simplifyString(s) {
function collapseWhiteSpaceAndAppend(lines, state, text) {
if (state.inCode) {
text = "\t" + text;
lines.push(text);
let previous = lines.length ? lines[lines.length - 1] : '';
// If the preceding item is a block limit, then the current line should start with a TAB
if ([BLOCK_OPEN, BLOCK_CLOSE, NEWLINE, NEWLINE_MERGED, MONOSPACE_OPEN, MONOSPACE_CLOSE].indexOf(previous) >= 0 || !previous) {
//text = "\t" + text;
lines.push('\t');
lines.push(text);
} else {
// If the current text contains one or more \n, then the last one should be immediately followed by a TAB
const idx = text.lastIndexOf('\n');
if (idx >= 0) {
text = text.substr(0, idx+1) + '\t' + text.substr(idx+1);
}
lines.push(text);
}
} else {
// Remove all \n and \r from the left and right of the text
while (text.length && (text[0] == "\n" || text[0] == "\r")) text = text.substr(1);
@@ -194,7 +567,7 @@ function addResourceTag(lines, resource, alt = "") {
function isBlockTag(n) {
return ["div", "p", "dl", "dd", 'dt', "center", 'address'].indexOf(n) >= 0;
return ["div", "p", "dl", "dd", 'dt', "center", 'address', 'form', 'input', 'section', 'nav', 'header', 'article', 'textarea', 'footer', 'fieldset'].indexOf(n) >= 0;
}
function isStrongTag(n) {
@@ -214,7 +587,7 @@ function isAnchor(n) {
}
function isIgnoredEndTag(n) {
return ["en-note", "en-todo", "span", "body", "html", "font", "br", 'hr', 'tbody', 'sup', 'img', 'abbr', 'cite', 'thead', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0;
return ["en-note", "en-todo", "span", "body", "html", "font", "br", 'hr', 'tbody', 'sup', 'img', 'abbr', 'cite', 'thead', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area', 'label', 'legend'].indexOf(n) >= 0;
}
function isListTag(n) {
@@ -223,7 +596,12 @@ function isListTag(n) {
// Elements that don't require any special treatment beside adding a newline character
function isNewLineOnlyEndTag(n) {
return ["div", "p", "li", "h1", "h2", "h3", "h4", "h5", 'h6', "dl", "dd", 'dt', "center", 'address'].indexOf(n) >= 0;
return ["div", "p", "li", "h1", "h2", "h3", "h4", "h5", 'h6', "dl", "dd", 'dt', "center", 'address', 'form', 'input', 'section', 'nav', 'header', 'article', 'textarea', 'footer', 'fieldset'].indexOf(n) >= 0;
}
// Tags that must be ignored - both the tag and its content.
function isIgnoredContentTag(n) {
return ['script', 'style', 'iframe', 'select', 'option', 'button', 'video', 'source'].indexOf(n) >= 0
}
function isCodeTag(n) {
@@ -271,7 +649,36 @@ function attributeToLowerCase(node) {
return output;
}
function enexXmlToMdArray(stream, resources) {
function urlWithoutPath(url) {
const parsed = require('url').parse(url, true);
return parsed.protocol + '//' + parsed.host;
}
function urlProtocol(url) {
const parsed = require('url').parse(url, true);
return parsed.protocol;
}
const schemeRegex = /[a-zA-Z0-9\+\-\.]+:\/\//
// Make sure baseUrl doesn't end with a slash
function prependBaseUrl(url, baseUrl) {
if (!url) url = '';
if (!baseUrl) return url;
const matches = schemeRegex.exec(url);
if (matches) return url; // Don't prepend the base URL if the URL already has a scheme
if (url.length >= 2 && url.indexOf('//') === 0) { // If it starts with // it's a protcol-relative URL
return urlProtocol(baseUrl) + url;
} else if (url && url[0] === '/') { // If it starts with a slash, it's an absolute URL so it should be relative to the domain (and not to the full baseUrl)
return urlWithoutPath(baseUrl) + url;
} else {
return baseUrl + '/' + url;
}
}
function enexXmlToMdArray(stream, resources, options = {}) {
if (options.baseUrl) options.baseUrl = options.baseUrl.replace(/[\/]+$/, '');
let remainingResources = resources.slice();
const removeRemainingResource = (id) => {
@@ -287,13 +694,16 @@ function enexXmlToMdArray(stream, resources) {
let state = {
inCode: false,
inQuote: false,
inMonospaceFont: false,
inCodeblock: 0,
lists: [],
anchorAttributes: [],
ignoreContents: [],
};
let options = {};
let saxStreamOptions = {};
let strict = false;
var saxStream = require('sax').createStream(strict, options)
var saxStream = require('sax').createStream(strict, saxStreamOptions)
let section = {
type: 'text',
@@ -307,6 +717,7 @@ function enexXmlToMdArray(stream, resources) {
})
saxStream.on('text', function(text) {
if (state.ignoreContents.length) return;
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
})
@@ -315,10 +726,25 @@ function enexXmlToMdArray(stream, resources) {
const nodeAttributes = attributeToLowerCase(node);
let n = node.name.toLowerCase();
// if (n == "div") {
// // div tags are recursive, in order to find the end we have to count the depth
// if (state.inCodeblock > 0) {
// state.inCodeblock++;
// } else if (nodeAttributes && nodeAttributes.style && nodeAttributes.style.indexOf("box-sizing: border-box") >= 0) {
// // Evernote code block start
// state.inCodeblock = 1;
// section.lines.push("```");
// return; // skip further processing
// }
// }
if (n == 'en-note') {
// Start of note
} else if (isBlockTag(n)) {
section.lines.push(BLOCK_OPEN);
} else if (isIgnoredContentTag(n)) {
state.ignoreContents.push(true);
} else if (n == 'table') {
let newSection = {
type: 'table',
@@ -366,7 +792,7 @@ function enexXmlToMdArray(stream, resources) {
} else if (n == 'li') {
section.lines.push(BLOCK_OPEN);
if (!state.lists.length) {
reject("Found <li> tag without being inside a list"); // TODO: could be a warning, but nothing to handle warnings at the moment
console.warn("Found <li> tag without being inside a list");
return;
}
@@ -386,16 +812,17 @@ function enexXmlToMdArray(stream, resources) {
} else if (n == 'q') {
section.lines.push('"');
} else if (n == 'img') {
// TODO: TEST IMAGE
if (nodeAttributes.src) { // Many (most?) img tags don't have no source associated, especially when they were imported from HTML
let s = '![';
if (nodeAttributes.alt) s += nodeAttributes.alt;
s += '](' + nodeAttributes.src + ')';
s += '](' + prependBaseUrl(nodeAttributes.src, options.baseUrl) + ')';
section.lines.push(s);
}
} else if (isAnchor(n)) {
state.anchorAttributes.push(nodeAttributes);
section.lines.push('[');
// Need to add the '[' via this function to make sure that links within code blocks
// are handled correctly.
collapseWhiteSpaceAndAppend(section.lines, state, '[');
} else if (isEmTag(n)) {
section.lines.push("*");
} else if (n == "en-todo") {
@@ -502,7 +929,27 @@ function enexXmlToMdArray(stream, resources) {
if (resource && !!resource.id) {
section.lines = addResourceTag(section.lines, resource, nodeAttributes.alt);
}
} else if (["span", "font", 'sup', 'cite', 'abbr', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area'].indexOf(n) >= 0) {
// } else if (n == "span" || n == "font") {
// // Check for monospace font. It can come from being specified in either from
// // <span style="..."> or <font face="...">.
// // Monospace sections are already in monospace for Evernote code blocks
// if (state.inCodeblock == 0 && nodeAttributes) {
// let style = null;
// if (nodeAttributes.style) {
// style = nodeAttributes.style.toLowerCase();
// } else if (nodeAttributes.face) {
// style = nodeAttributes.face.toLowerCase();
// }
// monospace = style ? style.match(/monospace|courier|menlo|monaco/) != null : false;
// if (monospace) {
// state.inMonospaceFont = true;
// section.lines.push(MONOSPACE_OPEN);
// }
// }
} else if (["span", "font", 'sup', 'cite', 'abbr', 'small', 'tt', 'sub', 'colgroup', 'col', 'ins', 'caption', 'var', 'map', 'area', 'label', 'legend'].indexOf(n) >= 0) {
// Inline tags that can be ignored in Markdown
} else {
console.warn("Unsupported start tag: " + n);
@@ -512,16 +959,34 @@ function enexXmlToMdArray(stream, resources) {
saxStream.on('closetag', function(n) {
n = n ? n.toLowerCase() : n;
// if (n == "div") {
// if (state.inCodeblock >= 1) {
// state.inCodeblock--;
// if (state.inCodeblock == 0) {
// // Evernote code block end
// section.lines.push("```");
// return; // skip further processing
// }
// }
// }
if (n == 'en-note') {
// End of note
} else if (isNewLineOnlyEndTag(n)) {
section.lines.push(BLOCK_CLOSE);
} else if (isIgnoredContentTag(n)) {
state.ignoreContents.pop();
} else if (n == 'td' || n == 'th') {
if (section && section.parent) section = section.parent;
} else if (n == 'tr') {
if (section && section.parent) section = section.parent;
} else if (n == 'table') {
if (section && section.parent) section = section.parent;
// } else if (n == "span" || n == "font") {
// if (state.inCodeblock == 0 && state.inMonospaceFont) {
// state.inMonospaceFont = false;
// section.lines.push(MONOSPACE_CLOSE);
// }
} else if (isIgnoredEndTag(n)) {
// Skip
} else if (isListTag(n)) {
@@ -546,6 +1011,7 @@ function enexXmlToMdArray(stream, resources) {
} else if (isAnchor(n)) {
let attributes = state.anchorAttributes.pop();
let url = attributes && attributes.href ? attributes.href : '';
url = prependBaseUrl(url, options.baseUrl);
if (section.lines.length < 1) throw new Error('Invalid anchor tag closing'); // Sanity check, but normally not possible
@@ -553,10 +1019,47 @@ function enexXmlToMdArray(stream, resources) {
// put the URL as is (don't wrap it in [](url)). The markdown parser, using
// GitHub flavour, will turn this URL into a link. This is to generate slightly
// cleaner markdown.
let previous = section.lines[section.lines.length - 1];
// Need to loop on the previous tags so as to skip the special ones, which are not relevant for the below algorithm.
let previous = null;
for (let i = section.lines.length - 1; i >= 0; i--) {
previous = section.lines[i];
if ([BLOCK_OPEN, BLOCK_CLOSE, NEWLINE, NEWLINE_MERGED, SPACE].indexOf(previous) >= 0) {
continue;
} else {
break;
}
}
if (previous == '[') {
section.lines.pop();
section.lines.push(url);
// We have a link that had some content but, after parsing, nothing is left. The content was most likely
// something that shows up via CSS and which we cannot support. For example:
//
// <a onclick="return vote()" href="vote?id=17045576">
// <div class="votearrow" title="upvote"></div>
// </a>
//
// In the case above the arrow is displayed via CSS.
// It is useless to display the full URL since often it is not relevant for a note (for example
// it's interactive bits) and it's not user-generated content such as a URL that would appear in a comment.
// So in this case, we still want to preserve the information but display it in a discreet way as a simple [L].
// Need to pop everything inside the current [] because it can only be special chars that we don't want (they would create uncessary newlines)
for (let i = section.lines.length - 1; i >= 0; i--) {
if (section.lines[i] !== '[') {
section.lines.pop();
} else {
break;
}
}
if (!url) {
// If there's no URL and no content, pop the [ and don't save any content.
section.lines.pop();
} else {
section.lines.push('(L)');
section.lines.push('](' + url + ')');
}
} else if (!previous || previous == url) {
section.lines.pop();
section.lines.pop();
@@ -662,7 +1165,7 @@ function drawTable(table) {
const renderCurrentCells = () => {
if (!currentCells.length) return;
const cellText = processMdArrayNewLines(currentCells);
const cellText = processMdArrayNewLines(currentCells, true);
line.push(cellText);
currentCells = [];
}
@@ -685,7 +1188,7 @@ function drawTable(table) {
// A cell in a Markdown table cannot have actual new lines so replace
// them with <br>, which are supported by the markdown renderers.
let cellText = processMdArrayNewLines(td.lines).replace(/\n+/g, "<br>");
let cellText = processMdArrayNewLines(td.lines, true).replace(/\n+/g, "<br>");
// Inside tables cells, "|" needs to be escaped
cellText = cellText.replace(/\|/g, "\\|");
@@ -735,8 +1238,8 @@ function drawTable(table) {
return flatRender ? lines : lines.join('<<<<:D>>>>' + NEWLINE + '<<<<:D>>>>').split('<<<<:D>>>>');
}
async function enexXmlToMd(stream, resources) {
let result = await enexXmlToMdArray(stream, resources);
async function enexXmlToMd(stream, resources, options = {}) {
let result = await enexXmlToMdArray(stream, resources, options);
let mdLines = [];

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

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

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