You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
99 Commits
android-v1
...
cli-v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
|
65065a62d8 | ||
|
482e9340bc | ||
|
69d490996e | ||
|
3494937e34 | ||
|
41ba1043be | ||
|
cc57de60c0 | ||
|
60a2b9e5c6 | ||
|
8e1fb666a5 | ||
|
f4ad777bbf | ||
|
2eacf6146a | ||
|
fe2ba34cb4 | ||
|
84daa0db61 | ||
|
b9118a90be | ||
|
ef2ffd4e52 | ||
|
5e3063abe0 | ||
|
f460b2497a | ||
|
c080d7054f | ||
|
61dd4cefbc | ||
|
63d99b2d70 | ||
|
55332d7671 | ||
|
16635defcd | ||
|
595cf3fcad | ||
|
c9b9f82130 | ||
|
f5bca733d7 | ||
|
494e235e18 | ||
|
85219a6004 | ||
|
e4a7851e57 | ||
|
b7529b40b5 | ||
|
74827e5324 | ||
|
2e16cc5433 | ||
|
7f41bc5703 | ||
|
a2380fb752 | ||
|
f6a902809d | ||
|
33a853397d | ||
|
4f02481899 | ||
|
b18076565f | ||
|
853ddc5840 | ||
|
7930ab66c6 | ||
|
c7716c0d59 | ||
|
49cbb254d0 | ||
|
cf9246796d | ||
|
e1dee546dc | ||
|
da6fdad2de | ||
|
567596643c | ||
|
cb617e1b14 | ||
|
facf8afa8b | ||
|
f0dd61a711 | ||
|
e958211a13 | ||
|
0ed170b5bc | ||
|
473d3453a2 | ||
|
fa9d7b0408 | ||
|
d4a28f48c9 | ||
|
ead6fff861 | ||
|
c7d06b35cd | ||
|
fa939e5c76 | ||
|
1bf2601f4f | ||
|
feb0c02c9a | ||
|
40a34a7c05 | ||
|
c62dcd96b0 | ||
|
1364d6786d | ||
|
a6a351e68d | ||
|
1db38a9699 | ||
|
c57db1834f | ||
|
3aeb49b469 | ||
|
80b467eead | ||
|
61572f287a | ||
|
0e545baf10 | ||
|
e65e647359 | ||
|
238268884e | ||
|
4c210d0956 | ||
|
5f32c6466a | ||
|
71bd39a8a3 | ||
|
ffb660f0f4 | ||
|
dde23632c1 | ||
|
9d26f13db0 | ||
|
2a4c9c4427 | ||
|
3bfde26b74 | ||
|
a419bc7253 | ||
|
89e0dad88b | ||
|
ff1ee1249b | ||
|
ba9cfd8041 | ||
|
80a51e02a4 | ||
|
a2e2a9a2f5 | ||
|
49e4c37cac | ||
|
11d323d8b7 | ||
|
784ba45f1f | ||
|
e534414874 | ||
|
01f4faf8f1 | ||
|
b33d30ca47 | ||
|
1ba3fae101 | ||
|
9550347e04 | ||
|
398946d39a | ||
|
05faf55e8d | ||
|
4cf5525e20 | ||
|
62e91c44d7 | ||
|
e4ec4ae92b | ||
|
c1f5dfd9cc | ||
|
0c0efeac1f | ||
|
577bef5704 |
@@ -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'])) {
|
||||
|
@@ -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);
|
||||
|
@@ -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).'));
|
||||
|
@@ -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);
|
||||
|
@@ -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_;
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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 ""
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -229,7 +229,7 @@ msgid "The possible commands are:"
|
||||
msgstr "Les commandes possibles sont :"
|
||||
|
||||
msgid ""
|
||||
"In any command, a note or notebook can be refered to by title or ID, or "
|
||||
"In any command, a note or notebook can be referred to by title or ID, or "
|
||||
"using the shortcuts `$n` or `$b` for, respectively, the currently selected "
|
||||
"note or notebook. `$c` can be used to refer to the currently selected item."
|
||||
msgstr ""
|
||||
@@ -365,10 +365,12 @@ msgstr "Supprimer le carnet."
|
||||
msgid "Deletes the notebook without asking for confirmation."
|
||||
msgstr "Supprimer le carnet sans demander la confirmation."
|
||||
|
||||
msgid "Delete notebook? All notes within this notebook will also be deleted."
|
||||
msgid ""
|
||||
"Delete notebook? All notes and sub-notebooks within this notebook will also "
|
||||
"be deleted."
|
||||
msgstr ""
|
||||
"Effacer le carnet ? Toutes les notes dans ce carnet seront également "
|
||||
"effacées."
|
||||
"Effacer le carnet ? Toutes les notes et sous-carnets dans ce carnet seront "
|
||||
"également effacés."
|
||||
|
||||
msgid "Deletes the notes matching <note-pattern>."
|
||||
msgstr "Supprimer les notes correspondants à <note-pattern>."
|
||||
@@ -848,6 +850,9 @@ msgstr "Gérer les étiquettes"
|
||||
msgid "Switch between note and to-do type"
|
||||
msgstr "Alterner entre note et tâche"
|
||||
|
||||
msgid "Copy Markdown link"
|
||||
msgstr "Copier lien Markdown"
|
||||
|
||||
msgid "Delete"
|
||||
msgstr "Supprimer"
|
||||
|
||||
@@ -1063,6 +1068,9 @@ msgstr "Les objets cryptés ne peuvent être modifiés"
|
||||
msgid "Conflicts"
|
||||
msgstr "Conflits"
|
||||
|
||||
msgid "Cannot move notebook to this location"
|
||||
msgstr "Impossible de déplacer le carnet vers le carnet \"%s\""
|
||||
|
||||
#, javascript-format
|
||||
msgid "A notebook with this title already exists: \"%s\""
|
||||
msgstr "Un carnet avec ce titre existe déjà : \"%s\""
|
||||
@@ -1116,6 +1124,9 @@ msgstr "Sombre"
|
||||
msgid "Uncompleted to-dos on top"
|
||||
msgstr "Tâches non-terminées en haut"
|
||||
|
||||
msgid "Show completed to-dos"
|
||||
msgstr "Afficher les tâches complétées"
|
||||
|
||||
msgid "Sort notes by"
|
||||
msgstr "Trier les notes par"
|
||||
|
||||
@@ -1396,6 +1407,15 @@ msgstr "Enregistrer les changements"
|
||||
msgid "Discard changes"
|
||||
msgstr "Ignorer les changements"
|
||||
|
||||
#, javascript-format
|
||||
msgid "No item with ID %s"
|
||||
msgstr "Aucun objet avec identifiant %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "The Joplin mobile app does not currently support this type of link: %s"
|
||||
msgstr ""
|
||||
"L'application mobile Joplin ne gère pas pour l'instant ce type de lien : %s"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Unsupported image type: %s"
|
||||
msgstr "Type d'image non géré : %s"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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."
|
||||
|
@@ -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 ""
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
@@ -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"
|
||||
|
814
CliClient/package-lock.json
generated
814
CliClient/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.0.106",
|
||||
"version": "1.0.107",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -58,9 +58,10 @@
|
||||
"strip-ansi": "^4.0.0",
|
||||
"tar": "^4.4.0",
|
||||
"tcp-port-used": "^0.1.2",
|
||||
"tkwidgets": "^0.5.25",
|
||||
"tkwidgets": "^0.5.26",
|
||||
"url-parse": "^1.2.0",
|
||||
"uuid": "^3.0.1",
|
||||
"valid-url": "^1.0.9",
|
||||
"word-wrap": "^1.2.3",
|
||||
"xml2js": "^0.4.19",
|
||||
"yargs-parser": "^7.0.0"
|
||||
|
@@ -9,7 +9,7 @@ rsync -a "$ROOT_DIR/build/locales/" "$BUILD_DIR/locales/"
|
||||
mkdir -p "$BUILD_DIR/data"
|
||||
|
||||
if [[ $TEST_FILE == "" ]]; then
|
||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js tests-build/services_InteropService.js)
|
||||
(cd "$ROOT_DIR" && npm test tests-build/synchronizer.js tests-build/encryption.js tests-build/ArrayUtils.js tests-build/models_Setting.js tests-build/models_Note.js tests-build/models_Folder.js tests-build/services_InteropService.js)
|
||||
else
|
||||
(cd "$ROOT_DIR" && npm test tests-build/$TEST_FILE.js)
|
||||
fi
|
@@ -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();
|
||||
});
|
||||
|
||||
});
|
55
CliClient/tests/models_Folder.js
Normal file
55
CliClient/tests/models_Folder.js
Normal 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);
|
||||
}));
|
||||
|
||||
});
|
39
CliClient/tests/models_Note.js
Normal file
39
CliClient/tests/models_Note.js
Normal 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);
|
||||
}));
|
||||
|
||||
});
|
@@ -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);
|
||||
}));
|
||||
|
||||
});
|
@@ -164,7 +164,7 @@ class Application extends BaseApplication {
|
||||
}
|
||||
|
||||
if (["NOTE_UPDATE_ONE", "NOTE_DELETE", "FOLDER_UPDATE_ONE", "FOLDER_DELETE"].indexOf(action.type) >= 0) {
|
||||
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(5, { syncSteps: ["update_remote", "delete_remote"] });
|
||||
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(30 * 1000, { syncSteps: ["update_remote", "delete_remote"] });
|
||||
}
|
||||
|
||||
if (['EVENT_NOTE_ALARM_FIELD_CHANGE', 'NOTE_DELETE'].indexOf(action.type) >= 0) {
|
||||
@@ -437,6 +437,14 @@ class Application extends BaseApplication {
|
||||
click: () => {
|
||||
Setting.setValue('uncompletedTodosOnTop', !Setting.value('uncompletedTodosOnTop'));
|
||||
},
|
||||
}, {
|
||||
label: Setting.settingMetadata('showCompletedTodos').label(),
|
||||
type: 'checkbox',
|
||||
checked: Setting.value('showCompletedTodos'),
|
||||
screens: ['Main'],
|
||||
click: () => {
|
||||
Setting.setValue('showCompletedTodos', !Setting.value('showCompletedTodos'));
|
||||
},
|
||||
}],
|
||||
}, {
|
||||
label: _('Tools'),
|
||||
@@ -615,6 +623,11 @@ class Application extends BaseApplication {
|
||||
id: Setting.value('activeFolderId'),
|
||||
});
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'FOLDER_SET_COLLAPSED_ALL',
|
||||
ids: Setting.value('collapsedFolderIds'),
|
||||
});
|
||||
|
||||
// Note: Auto-update currently doesn't work in Linux: it downloads the update
|
||||
// but then doesn't install it on exit.
|
||||
if (shim.isWindows() || shim.isMac()) {
|
||||
|
@@ -43,7 +43,7 @@ async function fetchLatestRelease() {
|
||||
for (let i = 0; i < json.assets.length; i++) {
|
||||
const asset = json.assets[i];
|
||||
let found = false;
|
||||
if (platform === 'win32' && asset.name.indexOf('.exe') >= 0) {
|
||||
if (platform === 'win32' && asset.name.indexOf('.exe') >= 0 && asset.name.indexOf('Setup') >= 0) {
|
||||
found = true;
|
||||
} else if (platform === 'darwin' && asset.name.indexOf('.dmg') >= 0) {
|
||||
found = true;
|
||||
|
@@ -96,6 +96,16 @@ class NoteListComponent extends React.Component {
|
||||
}
|
||||
}}));
|
||||
|
||||
menu.append(new MenuItem({label: _('Copy Markdown link'), click: async () => {
|
||||
const { clipboard } = require('electron');
|
||||
const links = [];
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
const note = await Note.load(noteIds[i]);
|
||||
links.push(Note.markdownTag(note));
|
||||
}
|
||||
clipboard.writeText(links.join(' '));
|
||||
}}));
|
||||
|
||||
const exportMenu = new Menu();
|
||||
|
||||
const ioService = new InteropService();
|
||||
|
@@ -1,5 +1,6 @@
|
||||
const React = require('react');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Search = require('lib/models/Search.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
@@ -19,6 +20,10 @@ const MenuItem = bridge().MenuItem;
|
||||
const { shim } = require('lib/shim.js');
|
||||
const eventManager = require('../eventManager');
|
||||
const fs = require('fs-extra');
|
||||
const {clipboard} = require('electron')
|
||||
const md5 = require('md5');
|
||||
const mimeUtils = require('lib/mime-utils.js').mime;
|
||||
const ArrayUtils = require('lib/ArrayUtils');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
@@ -46,6 +51,7 @@ class NoteTextComponent extends React.Component {
|
||||
// changed by the user, this variable contains that note ID. Used
|
||||
// to automatically set the title.
|
||||
newAndNoTitleChangeNoteId: null,
|
||||
bodyHtml: '',
|
||||
};
|
||||
|
||||
this.lastLoadedNoteId_ = null;
|
||||
@@ -54,6 +60,8 @@ class NoteTextComponent extends React.Component {
|
||||
this.ignoreNextEditorScroll_ = false;
|
||||
this.scheduleSaveTimeout_ = null;
|
||||
this.restoreScrollTop_ = null;
|
||||
this.lastSetHtml_ = '';
|
||||
this.lastSetMarkers_ = [];
|
||||
|
||||
// Complicated but reliable method to get editor content height
|
||||
// https://github.com/ajaxorg/ace/issues/2046
|
||||
@@ -71,12 +79,67 @@ class NoteTextComponent extends React.Component {
|
||||
this.onAlarmChange_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
|
||||
this.onNoteTypeToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
|
||||
this.onTodoToggle_ = (event) => { if (event.noteId === this.props.noteId) this.reloadNote(this.props); }
|
||||
|
||||
this.onEditorPaste_ = async (event) => {
|
||||
const formats = clipboard.availableFormats();
|
||||
for (let i = 0; i < formats.length; i++) {
|
||||
const format = formats[i].toLowerCase();
|
||||
const formatType = format.split('/')[0]
|
||||
if (formatType === 'image') {
|
||||
event.preventDefault();
|
||||
|
||||
const image = clipboard.readImage();
|
||||
|
||||
const fileExt = mimeUtils.toFileExtension(format);
|
||||
const filePath = Setting.value('tempDir') + '/' + md5(Date.now()) + '.' + fileExt;
|
||||
|
||||
await shim.writeImageToFile(image, format, filePath);
|
||||
await this.commandAttachFile([filePath]);
|
||||
await shim.fsDriver().remove(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.onDrop_ = async (event) => {
|
||||
const files = event.dataTransfer.files;
|
||||
if (!files || !files.length) return;
|
||||
|
||||
const filesToAttach = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files[i];
|
||||
if (!file.path) continue;
|
||||
filesToAttach.push(file.path);
|
||||
}
|
||||
|
||||
await this.commandAttachFile(filesToAttach);
|
||||
}
|
||||
}
|
||||
|
||||
cursorPosition() {
|
||||
if (!this.editor_ || !this.editor_.editor || !this.state.note || !this.state.note.body) return 0;
|
||||
|
||||
const cursorPos = this.editor_.editor.getCursorPosition();
|
||||
const noteLines = this.state.note.body.split('\n');
|
||||
|
||||
let pos = 0;
|
||||
for (let i = 0; i < noteLines.length; i++) {
|
||||
if (i > 0) pos++; // Need to add the newline that's been removed in the split() call above
|
||||
|
||||
if (i === cursorPos.row) {
|
||||
pos += cursorPos.column;
|
||||
break;
|
||||
} else {
|
||||
pos += noteLines[i].length;
|
||||
}
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
mdToHtml() {
|
||||
if (this.mdToHtml_) return this.mdToHtml_;
|
||||
this.mdToHtml_ = new MdToHtml({
|
||||
supportsResourceLinks: true,
|
||||
resourceBaseUrl: 'file://' + Setting.value('resourceDir') + '/',
|
||||
});
|
||||
return this.mdToHtml_;
|
||||
@@ -210,10 +273,22 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
if (this.editor_) {
|
||||
const session = this.editor_.editor.getSession();
|
||||
const undoManager = session.getUndoManager();
|
||||
undoManager.reset();
|
||||
session.setUndoManager(undoManager);
|
||||
// Calling setValue here does two things:
|
||||
// 1. It sets the initial value as recorded by the undo manager. If we were to set it instead to "" and wait for the render
|
||||
// phase to set the value, the initial value would still be "", which means pressing "undo" on a note that has just loaded
|
||||
// would clear it.
|
||||
// 2. It resets the undo manager - fixes https://github.com/laurent22/joplin/issues/355
|
||||
// Note: calling undoManager.reset() doesn't work
|
||||
try {
|
||||
this.editor_.editor.getSession().setValue(note ? note.body : '');
|
||||
} catch (error) {
|
||||
if (error.message === "Cannot read property 'match' of undefined") {
|
||||
// The internals of Ace Editor throws an exception when creating a new note,
|
||||
// but that can be ignored.
|
||||
} else {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
this.editor_.editor.clearSelection();
|
||||
this.editor_.editor.moveCursorTo(0,0);
|
||||
}
|
||||
@@ -231,7 +306,12 @@ class NoteTextComponent extends React.Component {
|
||||
newState.newAndNoTitleChangeNoteId = null;
|
||||
}
|
||||
|
||||
this.lastSetHtml_ = '';
|
||||
this.lastSetMarkers_ = [];
|
||||
|
||||
this.setState(newState);
|
||||
|
||||
this.updateHtml(newState.note ? newState.note.body : '');
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
@@ -322,11 +402,29 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
menu.popup(bridge().window());
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceId = msg.substr('joplin://'.length);
|
||||
Resource.load(resourceId).then((resource) => {
|
||||
const filePath = Resource.fullPath(resource);
|
||||
const itemId = msg.substr('joplin://'.length);
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
|
||||
if (!item) throw new Error('No item with ID ' + itemId);
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
const filePath = Resource.fullPath(item);
|
||||
bridge().openItem(filePath);
|
||||
});
|
||||
} else if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
this.props.dispatch({
|
||||
type: "FOLDER_SELECT",
|
||||
id: item.parent_id,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: item.id,
|
||||
});
|
||||
}, 10);
|
||||
} else {
|
||||
throw new Error('Unsupported item type: ' + item.type_);
|
||||
}
|
||||
} else {
|
||||
bridge().showErrorMessageBox(_('Unsupported link or message: %s', msg));
|
||||
}
|
||||
@@ -391,6 +489,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
if (this.editor_) {
|
||||
this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_);
|
||||
document.querySelector('#note-editor').removeEventListener('paste', this.onEditorPaste_, true);
|
||||
}
|
||||
|
||||
this.editor_ = element;
|
||||
@@ -398,7 +497,14 @@ class NoteTextComponent extends React.Component {
|
||||
if (this.editor_) {
|
||||
this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_);
|
||||
|
||||
const cancelledKeys = ['Ctrl+F', 'Ctrl+T', 'Ctrl+P', 'Ctrl+Q', 'Ctrl+L', 'Ctrl+,'];
|
||||
const cancelledKeys = [];
|
||||
const letters = ['F', 'T', 'P', 'Q', 'L', ','];
|
||||
for (let i = 0; i < letters.length; i++) {
|
||||
const l = letters[i];
|
||||
cancelledKeys.push('Ctrl+' + l);
|
||||
cancelledKeys.push('Command+' + l);
|
||||
}
|
||||
|
||||
for (let i = 0; i < cancelledKeys.length; i++) {
|
||||
const k = cancelledKeys[i];
|
||||
this.editor_.editor.commands.bindKey(k, () => {
|
||||
@@ -409,6 +515,8 @@ class NoteTextComponent extends React.Component {
|
||||
throw new Error('HACK: Overriding Ace Editor shortcut: ' + k);
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelector('#note-editor').addEventListener('paste', this.onEditorPaste_, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,9 +551,52 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
aceEditor_change(body) {
|
||||
shared.noteComponent_change(this, 'body', body);
|
||||
this.scheduleHtmlUpdate();
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
scheduleHtmlUpdate(timeout = 500) {
|
||||
if (this.scheduleHtmlUpdateIID_) {
|
||||
clearTimeout(this.scheduleHtmlUpdateIID_);
|
||||
this.scheduleHtmlUpdateIID_ = null;
|
||||
}
|
||||
|
||||
if (timeout) {
|
||||
this.scheduleHtmlUpdateIID_ = setTimeout(() => {
|
||||
this.updateHtml();
|
||||
}, timeout);
|
||||
} else {
|
||||
this.updateHtml();
|
||||
}
|
||||
}
|
||||
|
||||
updateHtml(body = null) {
|
||||
const mdOptions = {
|
||||
onResourceLoaded: () => {
|
||||
this.updateHtml();
|
||||
this.forceUpdate();
|
||||
},
|
||||
postMessageSyntax: 'ipcRenderer.sendToHost',
|
||||
};
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
let bodyToRender = body;
|
||||
if (bodyToRender === null) bodyToRender = this.state.note && this.state.note.body ? this.state.note.body : '';
|
||||
let bodyHtml = '';
|
||||
|
||||
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
|
||||
|
||||
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*';
|
||||
}
|
||||
|
||||
bodyHtml = this.mdToHtml().render(bodyToRender, theme, mdOptions);
|
||||
|
||||
this.setState({ bodyHtml: bodyHtml });
|
||||
}
|
||||
|
||||
async doCommand(command) {
|
||||
if (!command) return;
|
||||
|
||||
@@ -479,27 +630,34 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
async commandAttachFile() {
|
||||
const filePaths = bridge().showOpenDialog({
|
||||
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
||||
});
|
||||
if (!filePaths || !filePaths.length) return;
|
||||
async commandAttachFile(filePaths = null) {
|
||||
if (!filePaths) {
|
||||
filePaths = bridge().showOpenDialog({
|
||||
properties: ['openFile', 'createDirectory', 'multiSelections'],
|
||||
});
|
||||
if (!filePaths || !filePaths.length) return;
|
||||
}
|
||||
|
||||
await this.saveIfNeeded(true);
|
||||
let note = await Note.load(this.state.note.id);
|
||||
|
||||
const position = this.cursorPosition();
|
||||
|
||||
for (let i = 0; i < filePaths.length; i++) {
|
||||
const filePath = filePaths[i];
|
||||
try {
|
||||
reg.logger().info('Attaching ' + filePath);
|
||||
note = await shim.attachFileToNote(note, filePath);
|
||||
note = await shim.attachFileToNote(note, filePath, position);
|
||||
reg.logger().info('File was attached.');
|
||||
this.setState({
|
||||
note: Object.assign({}, note),
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
});
|
||||
|
||||
this.updateHtml(note.body);
|
||||
} catch (error) {
|
||||
reg.logger().error(error);
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,25 +802,21 @@ class NoteTextComponent extends React.Component {
|
||||
}
|
||||
|
||||
if (this.state.webviewReady) {
|
||||
const mdOptions = {
|
||||
onResourceLoaded: () => {
|
||||
this.forceUpdate();
|
||||
},
|
||||
postMessageSyntax: 'ipcRenderer.sendToHost',
|
||||
};
|
||||
let html = this.state.bodyHtml;
|
||||
|
||||
let bodyToRender = body;
|
||||
if (!bodyToRender.trim() && visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') < 0) {
|
||||
// Fixes https://github.com/laurent22/joplin/issues/217
|
||||
bodyToRender = '*' + _('This note has no content. Click on "%s" to toggle the editor and edit the note.', _('Layout')) + '*';
|
||||
const htmlHasChanged = this.lastSetHtml_ !== html;
|
||||
if (htmlHasChanged) {
|
||||
this.webview_.send('setHtml', html);
|
||||
this.lastSetHtml_ = html;
|
||||
}
|
||||
|
||||
const html = this.mdToHtml().render(bodyToRender, theme, mdOptions);
|
||||
this.webview_.send('setHtml', html);
|
||||
|
||||
const search = BaseModel.byId(this.props.searches, this.props.selectedSearchId);
|
||||
const keywords = search ? Search.keywords(search.query_pattern) : [];
|
||||
this.webview_.send('setMarkers', keywords);
|
||||
|
||||
if (htmlHasChanged || !ArrayUtils.contentEquals(this.lastSetMarkers_, keywords)) {
|
||||
this.lastSetMarkers_ = [];
|
||||
this.webview_.send('setMarkers', keywords);
|
||||
}
|
||||
}
|
||||
|
||||
const toolbarItems = [];
|
||||
@@ -708,7 +862,7 @@ class NoteTextComponent extends React.Component {
|
||||
|
||||
const titleBarDate = <span style={Object.assign({}, theme.textStyle, {color: theme.colorFaded})}>{time.formatMsToLocal(note.user_updated_time)}</span>
|
||||
|
||||
const viewer = <webview
|
||||
const viewer = <webview
|
||||
style={viewerStyle}
|
||||
nodeintegration="1"
|
||||
src="gui/note-viewer/index.html"
|
||||
@@ -763,7 +917,7 @@ class NoteTextComponent extends React.Component {
|
||||
/>
|
||||
|
||||
return (
|
||||
<div style={rootStyle}>
|
||||
<div style={rootStyle} onDrop={this.onDrop_}>
|
||||
<div style={titleBarStyle}>
|
||||
{ titleEditor }
|
||||
{ titleBarDate }
|
||||
|
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -124,17 +124,17 @@
|
||||
loadAndApplyHljs();
|
||||
|
||||
// Remove the bullet from "ul" for checkbox lists and extra padding
|
||||
const checkboxes = document.getElementsByClassName('checkbox');
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
const cb = checkboxes[i];
|
||||
const ul = cb.parentElement.parentElement;
|
||||
if (!ul) {
|
||||
console.warn('Unexpected layout for checkbox');
|
||||
continue;
|
||||
}
|
||||
ul.style.listStyleType = 'none';
|
||||
ul.style.paddingLeft = 0;
|
||||
}
|
||||
// const checkboxes = document.getElementsByClassName('checkbox');
|
||||
// for (let i = 0; i < checkboxes.length; i++) {
|
||||
// const cb = checkboxes[i];
|
||||
// const ul = cb.parentElement.parentElement;
|
||||
// if (!ul) {
|
||||
// console.warn('Unexpected layout for checkbox');
|
||||
// continue;
|
||||
// }
|
||||
// ul.style.listStyleType = 'none';
|
||||
// ul.style.paddingLeft = 0;
|
||||
// }
|
||||
|
||||
let previousContentHeight = contentElement.scrollHeight;
|
||||
let startTime = Date.now();
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2362
ElectronClient/app/package-lock.json
generated
2362
ElectronClient/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.83",
|
||||
"version": "1.0.91",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -92,6 +92,7 @@
|
||||
"markdown-it": "^8.4.0",
|
||||
"markdown-it-katex": "^2.0.3",
|
||||
"md5": "^2.2.1",
|
||||
"mermaid": "^8.0.0-rc.8",
|
||||
"mime": "^2.0.3",
|
||||
"moment": "^2.19.1",
|
||||
"node-fetch": "^1.7.3",
|
||||
@@ -113,6 +114,7 @@
|
||||
"tcp-port-used": "^0.1.2",
|
||||
"url-parse": "^1.2.0",
|
||||
"uuid": "^3.1.0",
|
||||
"valid-url": "^1.0.9",
|
||||
"xml2js": "^0.4.19"
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
64
README.md
64
README.md
@@ -20,15 +20,15 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
|
||||
|
||||
Operating System | Download | Alternative
|
||||
-----------------|--------|-------------------
|
||||
Windows (64-bit only) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.83/Joplin-Setup-1.0.83.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a> |
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.83/Joplin-1.0.83.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a> |
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.83/Joplin-1.0.83-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a> | An Arch Linux package [is also available](#terminal-application).
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.91/Joplin-1.0.91.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a> |
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.91/Joplin-1.0.91.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a> |
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.91/Joplin-1.0.91-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a> | An Arch Linux package [is also available](#terminal-application).
|
||||
|
||||
## Mobile applications
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|----------|----------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplin.cozic.net/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.118/joplin-v1.0.118.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplin.cozic.net/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.125/joplin-v1.0.125.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplin.cozic.net/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -37,7 +37,7 @@ Operating system | Method
|
||||
-----------------|----------------
|
||||
macOS | `brew install joplin`
|
||||
Linux or Windows (via [WSL](https://msdn.microsoft.com/en-us/commandline/wsl/faq?f=255&MSPPError=-2147217396)) | **Important:** First, [install Node 8+](https://nodejs.org/en/download/package-manager/). Node 8 is LTS but not yet available everywhere so you might need to manually install it.<br/><br/>`NPM_CONFIG_PREFIX=~/.joplin-bin npm install -g joplin`<br/>`sudo ln -s ~/.joplin-bin/bin/joplin /usr/bin/joplin`<br><br>By default, the application binary will be installed under `~/.joplin-bin`. You may change this directory if needed. Alternatively, if your npm permissions are setup as described [here](https://docs.npmjs.com/getting-started/fixing-npm-permissions#option-2-change-npms-default-directory-to-another-directory) (Option 2) then simply running `npm -g install joplin` would work.
|
||||
Arch Linux | An Arch Linux pakage is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild).
|
||||
Arch Linux | An Arch Linux package is available [here](https://aur.archlinux.org/packages/joplin/). To install it, use an AUR wrapper such as yay: `yay -S joplin`. Both the CLI tool (type `joplin`) and desktop app (type `joplin-desktop`) are packaged. For support, please go to the [GitHub repo](https://github.com/masterkorp/joplin-pkgbuild).
|
||||
|
||||
To start it, type `joplin`.
|
||||
|
||||
@@ -183,7 +183,11 @@ For a more technical description, mostly relevant for development or to review t
|
||||
|
||||
Any kind of file can be attached to a note. In Markdown, links to these files are represented as a simple ID to the resource. In the note viewer, these files, if they are images, will be displayed or, if they are other files (PDF, text files, etc.) they will be displayed as links. Clicking on this link will open the file in the default application.
|
||||
|
||||
Resources that are not attached to any note will be automatically deleted after a day or two.
|
||||
On the **desktop application**, images can be attached either by clicking on "Attach file" or by pasting (with Ctrl+V) an image directly in the editor, or by drag and dropping an image.
|
||||
|
||||
Resources that are not attached to any note will be automatically deleted after a day or two (see [rationale](https://github.com/laurent22/joplin/issues/154#issuecomment-356582366)).
|
||||
|
||||
**Important:** Resources larger than 10 MB are not currently supported on mobile. They will crash the application when synchronising so it is recommended not to attach such resources at the moment. The issue is being looked at.
|
||||
|
||||
# Notifications
|
||||
|
||||
@@ -199,10 +203,28 @@ On mobile, the alarms will be displayed using the built-in notification system.
|
||||
|
||||
If for any reason the notifications do not work, please [open an issue](https://github.com/laurent22/joplin/issues).
|
||||
|
||||
# Sub-notebooks
|
||||
|
||||
Sub-notebooks allow organising multiple notebooks into a tree of notebooks. For example it can be used to regroup all the notebooks related to work, to family or to a particular project under a parent notebook.
|
||||
|
||||

|
||||
|
||||
- On the **desktop application**, to create a subnotebook, drag and drop it onto another notebook. To move it back to the root, drag and drop it on the "Notebooks" header. Currently only the desktop app can be used to organise the notebooks.
|
||||
- The **mobile application** supports displaying and collapsing/expanding the tree of notebooks, however it does not currently support moving the subnotebooks to different notebooks.
|
||||
- The **terminal app** supports displaying the tree of subnotebooks but it does not support collapsing/expanding them or moving the subnotebooks around.
|
||||
|
||||
# Markdown
|
||||
|
||||
Joplin uses and renders [Github-flavoured Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet) with a few variations and additions. In particular:
|
||||
|
||||
## Links to other notes
|
||||
|
||||
You can create a link to a note by specifying its ID in the URL. For example:
|
||||
|
||||
[Link to my note](:/0b0d62d15e60409dac34f354b6e9e839)
|
||||
|
||||
Since getting the ID of a note is not straightforward, each app provides a way to create such link. In the **desktop app**, right click on a note an select "Copy Markdown link". In the **mobile app**, open a note and, in the top right menu, select "Copy Markdown link". You can then paste this link anywhere in another note.
|
||||
|
||||
## Math notation
|
||||
|
||||
Math expressions can be added using the [Katex notation](https://khan.github.io/KaTeX/). To add an inline equation, wrap the expression in `$EXPRESSION$`, eg. `$\sqrt{3x-1}+(1+x)^2$`. To create an expression block, wrap it as follow:
|
||||
@@ -233,6 +255,10 @@ Checkboxes can be added like so:
|
||||
|
||||
The checkboxes can then be ticked in the mobile and desktop applications.
|
||||
|
||||
## HTML support
|
||||
|
||||
Only the `<br>` tag is supported - it can be used to force a new line, which is convenient to insert new lines inside table cells. For security reasons, other HTML tags are not supported.
|
||||
|
||||
# Donations
|
||||
|
||||
Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.
|
||||
@@ -242,6 +268,7 @@ Please see the [donation page](https://joplin.cozic.net/donate/) for information
|
||||
# Community
|
||||
|
||||
- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplin.cozic.net/). It is possible to login with your GitHub account.
|
||||
- Also see here for information about [the latest releases and general news](https://discourse.joplin.cozic.net/c/news).
|
||||
- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).
|
||||
- The latest news are often posted [on this Twitter account](https://twitter.com/laurent2233).
|
||||
|
||||
@@ -267,25 +294,26 @@ Current translations:
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
| Language | Po File | Last translator | Percent done
|
||||
---|---|---|---|---
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 76%
|
||||
 | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić <trbuhom@net.hr> | 62%
|
||||
 | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt <lukas@aiya.cz> | 96%
|
||||
 | 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%
|
||||
 | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Tobias Grasse <mail@tobias-grasse.net> | 95%
|
||||
 | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 75%
|
||||
 | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 61%
|
||||
 | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 95%
|
||||
 | 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%
|
||||
 | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Philipp Zumstein (zuphilip@gmail.com) | 98%
|
||||
 | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
|
||||
 | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín <f@mrtn.es> | 99%
|
||||
 | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín (f@mrtn.es) | 99%
|
||||
 | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
 | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans <marcoslansgarza@gmail.com> | 97%
|
||||
 | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 95%
|
||||
 | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 63%
|
||||
 | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 77%
|
||||
 | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos <rnbastos@gmail.com> | 99%
|
||||
 | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov <artyom.karlov@gmail.com> | 95%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 92%
|
||||
 | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 62%
|
||||
 | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 75%
|
||||
 | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 97%
|
||||
 | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 94%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 90%
|
||||
 | 日本語 | [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.
|
||||
|
||||
|
@@ -89,9 +89,9 @@ android {
|
||||
defaultConfig {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 22
|
||||
versionCode 2097296
|
||||
versionName "1.0.118"
|
||||
targetSdkVersion 26
|
||||
versionCode 2097303
|
||||
versionName "1.0.125"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@@ -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 -->
|
||||
<!-- ================================== -->
|
||||
|
@@ -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;
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
|
@@ -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];
|
||||
|
@@ -14,7 +14,6 @@ class MdToHtml {
|
||||
constructor(options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
this.supportsResourceLinks_ = !!options.supportsResourceLinks;
|
||||
this.loadedResources_ = {};
|
||||
this.cachedContent_ = null;
|
||||
this.cachedContentKey_ = null;
|
||||
@@ -132,43 +131,38 @@ class MdToHtml {
|
||||
const isResourceUrl = Resource.isResourceUrl(href);
|
||||
const title = isResourceUrl ? this.getAttr_(attrs, 'title') : href;
|
||||
|
||||
if (isResourceUrl && !this.supportsResourceLinks_) {
|
||||
// In mobile, links to local resources, such as PDF, etc. currently aren't supported.
|
||||
// Ideally they should be opened in the user's browser.
|
||||
return '<span style="opacity: 0.5">(Resource not yet supported: '; //+ htmlentities(text) + ']';
|
||||
let resourceIdAttr = "";
|
||||
let icon = "";
|
||||
let hrefAttr = '#';
|
||||
if (isResourceUrl) {
|
||||
const resourceId = Resource.pathToId(href);
|
||||
href = "joplin://" + resourceId;
|
||||
resourceIdAttr = "data-resource-id='" + resourceId + "'";
|
||||
icon = '<span class="resource-icon"></span>';
|
||||
} else {
|
||||
let resourceIdAttr = "";
|
||||
let icon = "";
|
||||
if (isResourceUrl) {
|
||||
const resourceId = Resource.pathToId(href);
|
||||
href = "joplin://" + resourceId;
|
||||
resourceIdAttr = "data-resource-id='" + resourceId + "'";
|
||||
icon = '<span class="resource-icon"></span>';
|
||||
}
|
||||
|
||||
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
||||
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='#' onclick='" + js + "'>" + icon;
|
||||
return output;
|
||||
// If the link is a plain URL (as opposed to a resource link), set the href to the actual
|
||||
// link. This allows the link to be exported too when exporting to PDF.
|
||||
hrefAttr = href;
|
||||
}
|
||||
|
||||
const js = options.postMessageSyntax + "(" + JSON.stringify(href) + "); return false;";
|
||||
let output = "<a " + resourceIdAttr + " title='" + htmlentities(title) + "' href='" + hrefAttr + "' onclick='" + js + "'>" + icon;
|
||||
return output;
|
||||
}
|
||||
|
||||
renderCloseLink_(attrs, options) {
|
||||
const href = this.getAttr_(attrs, 'href');
|
||||
const isResourceUrl = Resource.isResourceUrl(href);
|
||||
|
||||
if (isResourceUrl && !this.supportsResourceLinks_) {
|
||||
return ')</span>';
|
||||
} else {
|
||||
return '</a>';
|
||||
}
|
||||
return '</a>';
|
||||
}
|
||||
|
||||
rendererPlugin_(language) {
|
||||
if (!language) return null;
|
||||
|
||||
const handlers = {};
|
||||
handlers['katex'] = new MdToHtml_Katex();
|
||||
return language in handlers ? handlers[language] : null;
|
||||
if (!this.rendererPlugins_) {
|
||||
this.rendererPlugins_ = {};
|
||||
this.rendererPlugins_['katex'] = new MdToHtml_Katex();
|
||||
}
|
||||
|
||||
return language in this.rendererPlugins_ ? this.rendererPlugins_[language] : null;
|
||||
}
|
||||
|
||||
parseInlineCodeLanguage_(content) {
|
||||
@@ -220,12 +214,7 @@ class MdToHtml {
|
||||
|
||||
if (isCodeBlock) rendererPlugin = this.rendererPlugin_(codeBlockLanguage);
|
||||
|
||||
if (previousToken && previousToken.tag === 'li' && tag === 'p') {
|
||||
// Markdown-it render list items as <li><p>Text<p></li> which makes it
|
||||
// complicated to style and layout the HTML, so we remove this extra
|
||||
// <p> here and below in closeTag.
|
||||
openTag = null;
|
||||
} else if (isInlineCode) {
|
||||
if (isInlineCode) {
|
||||
openTag = null;
|
||||
} else if (tag && t.type.indexOf('html_inline') >= 0) {
|
||||
openTag = null;
|
||||
@@ -403,7 +392,7 @@ class MdToHtml {
|
||||
const md = new MarkdownIt({
|
||||
breaks: true,
|
||||
linkify: true,
|
||||
html: true,
|
||||
html: false, // For security, HTML tags are not supported - https://github.com/laurent22/joplin/issues/500
|
||||
});
|
||||
|
||||
// This is currently used only so that the $expression$ and $$\nexpression\n$$ blocks are translated
|
||||
@@ -420,8 +409,8 @@ class MdToHtml {
|
||||
|
||||
if (HORRIBLE_HACK) {
|
||||
let counter = -1;
|
||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
||||
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
|
||||
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
|
||||
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
||||
counter++;
|
||||
return '- mJOPmCHECKBOXm' + s + 'm' + counter + 'm';
|
||||
@@ -449,6 +438,9 @@ class MdToHtml {
|
||||
}
|
||||
}
|
||||
|
||||
// Support <br> tag to allow newlines inside table cells
|
||||
renderedBody = renderedBody.replace(/<br>/gi, '<br>');
|
||||
|
||||
// https://necolas.github.io/normalize.css/
|
||||
const normalizeCss = `
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||
@@ -490,6 +482,9 @@ class MdToHtml {
|
||||
ul {
|
||||
padding-left: 1.3em;
|
||||
}
|
||||
li p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.resource-icon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
@@ -577,10 +572,10 @@ class MdToHtml {
|
||||
|
||||
toggleTickAt(body, index) {
|
||||
let counter = -1;
|
||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0) {
|
||||
while (body.indexOf('- [ ]') >= 0 || body.indexOf('- [X]') >= 0 || body.indexOf('- [x]') >= 0) {
|
||||
counter++;
|
||||
|
||||
body = body.replace(/- \[(X| )\]/, function(v, p1) {
|
||||
body = body.replace(/- \[(X| |x)\]/, function(v, p1) {
|
||||
let s = p1 == ' ' ? 'NOTICK' : 'TICK';
|
||||
if (index == counter) {
|
||||
s = s == 'NOTICK' ? 'TICK' : 'NOTICK';
|
||||
|
@@ -5,13 +5,28 @@ const Setting = require('lib/models/Setting');
|
||||
|
||||
class MdToHtml_Katex {
|
||||
|
||||
constructor() {
|
||||
this.cache_ = {};
|
||||
this.assetsLoaded_ = false;
|
||||
}
|
||||
|
||||
name() {
|
||||
return 'katex';
|
||||
}
|
||||
|
||||
processContent(renderedTokens, content, tagType) {
|
||||
try {
|
||||
let renderered = katex.renderToString(content);
|
||||
const cacheKey = tagType + '_' + content;
|
||||
let renderered = null;
|
||||
|
||||
if (this.cache_[cacheKey]) {
|
||||
renderered = this.cache_[cacheKey];
|
||||
} else {
|
||||
renderered = katex.renderToString(content, {
|
||||
displayMode: tagType === 'block',
|
||||
});
|
||||
this.cache_[cacheKey] = renderered;
|
||||
}
|
||||
|
||||
if (tagType === 'block') renderered = '<p>' + renderered + '</p>';
|
||||
|
||||
@@ -27,6 +42,8 @@ class MdToHtml_Katex {
|
||||
}
|
||||
|
||||
async loadAssets() {
|
||||
if (this.assetsLoaded_) return;
|
||||
|
||||
// In node, the fonts are simply copied using copycss to where Katex expects to find them, which is under app/gui/note-viewer/fonts
|
||||
|
||||
// In React Native, it's more complicated and we need to download and copy them to the right directory. Ideally, we should embed
|
||||
@@ -41,6 +58,8 @@ class MdToHtml_Katex {
|
||||
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Math-Italic.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Math-Italic.woff2' });
|
||||
await shim.fetchBlob('https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.9.0-beta1/fonts/KaTeX_Size1-Regular.woff2', { overwrite: false, path: baseDir + '/fonts/KaTeX_Size1-Regular.woff2' });
|
||||
}
|
||||
|
||||
this.assetsLoaded_ = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Platform, WebView, View, Linking } = require('react-native');
|
||||
const { Platform, WebView, View } = require('react-native');
|
||||
const { globalStyle } = require('lib/components/global-style.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
@@ -19,7 +19,7 @@ class NoteBodyViewer extends Component {
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: false });
|
||||
this.mdToHtml_ = new MdToHtml();
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
@@ -39,6 +39,19 @@ class NoteBodyViewer extends Component {
|
||||
}, 100);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
// To address https://github.com/laurent22/joplin/issues/433
|
||||
// If a checkbox in a note is ticked, the body changes, which normally would trigger a re-render
|
||||
// of this component, which has the unfortunate side effect of making the view scroll back to the top.
|
||||
// This re-rendering however is uncessary since the component is already visually updated via JS.
|
||||
// So here, if the note has not changed, we prevent the component from updating.
|
||||
// This fixes the above issue. A drawback of this is if the note is updated via sync, this change
|
||||
// will not be displayed immediately.
|
||||
const currentNoteId = this.props && this.props.note ? this.props.note.id : null;
|
||||
const nextNoteId = nextProps && nextProps.note ? nextProps.note.id : null;
|
||||
return currentNoteId !== nextNoteId || nextState.webViewLoaded !== this.state.webViewLoaded;
|
||||
}
|
||||
|
||||
render() {
|
||||
const note = this.props.note;
|
||||
const style = this.props.style;
|
||||
@@ -109,13 +122,13 @@ class NoteBodyViewer extends Component {
|
||||
let msg = event.nativeEvent.data;
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = this.mdToHtml_.handleCheckboxClick(msg, note.body);
|
||||
const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.props.note.body);
|
||||
if (onCheckboxChange) onCheckboxChange(newBody);
|
||||
} else if (msg.indexOf('bodyscroll:') === 0) {
|
||||
//msg = msg.split(':');
|
||||
//this.bodyScrollTop_ = Number(msg[1]);
|
||||
} else {
|
||||
Linking.openURL(msg);
|
||||
this.props.onJoplinLinkClick(msg);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
@@ -99,7 +99,8 @@ class ScreenHeaderComponent extends Component {
|
||||
height: 18,
|
||||
},
|
||||
contextMenuTrigger: {
|
||||
fontSize: 25,
|
||||
fontSize: 30,
|
||||
paddingLeft: 10,
|
||||
paddingRight: theme.marginRight,
|
||||
color: theme.raisedColor,
|
||||
fontWeight: 'bold',
|
||||
@@ -445,7 +446,7 @@ class ScreenHeaderComponent extends Component {
|
||||
const menuComp = !showContextMenuButton ? null : (
|
||||
<Menu onSelect={(value) => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={{ paddingTop: PADDING_V, paddingBottom: PADDING_V }}>
|
||||
<Text style={this.styles().contextMenuTrigger}> ⋮</Text>
|
||||
<Icon name='md-more' style={this.styles().contextMenuTrigger} />
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
<ScrollView style={{ maxHeight: windowHeight }}>
|
||||
|
@@ -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>
|
||||
|
@@ -1,9 +1,10 @@
|
||||
const React = require('react'); const Component = React.Component;
|
||||
const { Platform, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image } = require('react-native');
|
||||
const { Platform, Clipboard, Keyboard, BackHandler, View, Button, TextInput, WebView, Text, StyleSheet, Linking, Image, Share } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const RNFS = require('react-native-fs');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
@@ -22,8 +23,8 @@ const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { NoteBodyViewer } = require('lib/components/note-body-viewer.js');
|
||||
const RNFetchBlob = require('react-native-fetch-blob').default;
|
||||
@@ -107,6 +108,39 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.noteTagDialog_closeRequested = () => {
|
||||
this.setState({ noteTagDialogShown: false });
|
||||
}
|
||||
|
||||
this.onJoplinLinkClick_ = async (msg) => {
|
||||
try {
|
||||
if (msg.indexOf('joplin://') === 0) {
|
||||
const itemId = msg.substr('joplin://'.length);
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
if (!item) throw new Error(_('No item with ID %s', itemId));
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
// Easier to just go back, then go to the note since
|
||||
// the Note screen doesn't handle reloading a different note
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_BACK',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: item.id,
|
||||
});
|
||||
}, 5);
|
||||
} else {
|
||||
throw new Error(_('The Joplin mobile app does not currently support this type of link: %s', BaseModel.modelTypeToName(item.type_)));
|
||||
}
|
||||
} else {
|
||||
Linking.openURL(msg);
|
||||
}
|
||||
} catch (error) {
|
||||
dialogs.error(this, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styles() {
|
||||
@@ -186,8 +220,8 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
shared.noteComponent_change(this, 'body', text);
|
||||
}
|
||||
|
||||
async saveNoteButton_press() {
|
||||
await shared.saveNoteButton_press(this);
|
||||
async saveNoteButton_press(folderId = null) {
|
||||
await shared.saveNoteButton_press(this, folderId);
|
||||
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
@@ -330,10 +364,16 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
return;
|
||||
} else {
|
||||
await RNFetchBlob.fs.cp(localFilePath, targetPath);
|
||||
const stat = await shim.fsDriver().stat(targetPath);
|
||||
if (stat.size >= 10000000) {
|
||||
await shim.fsDriver().remove(targetPath);
|
||||
throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reg.logger().warn('Could not attach file:', error);
|
||||
await dialogs.error(this, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -369,6 +409,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.setState({ noteTagDialogShown: true });
|
||||
}
|
||||
|
||||
async share_onPress() {
|
||||
await Share.share({
|
||||
message: this.state.note.title + '\n\n' + this.state.note.body,
|
||||
title: this.state.note.title,
|
||||
});
|
||||
}
|
||||
|
||||
setAlarm_onPress() {
|
||||
this.setState({ alarmDialogShown: true });
|
||||
}
|
||||
@@ -402,6 +449,11 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
}
|
||||
}
|
||||
|
||||
copyMarkdownLink_onPress() {
|
||||
const note = this.state.note;
|
||||
Clipboard.setString(Note.markdownTag(note));
|
||||
}
|
||||
|
||||
menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
@@ -423,8 +475,10 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
output.push({ title: _('Set alarm'), onPress: () => { this.setState({ alarmDialogShown: true }) }});;
|
||||
}
|
||||
|
||||
output.push({ title: _('Share'), onPress: () => { this.share_onPress(); } });
|
||||
if (isSaved) output.push({ title: _('Tags'), onPress: () => { this.tags_onPress(); } });
|
||||
output.push({ title: isTodo ? _('Convert to note') : _('Convert to todo'), onPress: () => { this.toggleIsTodo_onPress(); } });
|
||||
if (isSaved) output.push({ title: _('Copy Markdown link'), onPress: () => { this.copyMarkdownLink_onPress(); } });
|
||||
output.push({ isDivider: true });
|
||||
if (this.props.showAdvancedOptions) output.push({ title: this.state.showNoteMetadata ? _('Hide metadata') : _('Show metadata'), onPress: () => { this.showMetadata_onPress(); } });
|
||||
output.push({ title: _('View on map'), onPress: () => { this.showOnMap_onPress(); } });
|
||||
@@ -466,7 +520,13 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
this.saveOneProperty('body', newBody);
|
||||
};
|
||||
|
||||
bodyComponent = <NoteBodyViewer style={this.styles().noteBodyViewer} webViewStyle={theme} note={note} onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}/>
|
||||
bodyComponent = <NoteBodyViewer
|
||||
onJoplinLinkClick={this.onJoplinLinkClick_}
|
||||
style={this.styles().noteBodyViewer}
|
||||
webViewStyle={theme}
|
||||
note={note}
|
||||
onCheckboxChange={(newBody) => { onCheckboxChange(newBody) }}
|
||||
/>
|
||||
} else {
|
||||
const focusBody = !isNew && !!note.title;
|
||||
|
||||
@@ -497,7 +557,7 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
},
|
||||
});
|
||||
|
||||
if (this.state.mode == 'edit') return <ActionButton style={{display:'none'}}/>;
|
||||
if (this.state.mode == 'edit') return null;//<ActionButton style={{display:'none'}}/>;
|
||||
|
||||
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />
|
||||
}
|
||||
@@ -560,7 +620,12 @@ class NoteScreenComponent extends BaseScreenComponent {
|
||||
enabled: true,
|
||||
selectedFolderId: folder ? folder.id : null,
|
||||
onValueChange: async (itemValue, itemIndex) => {
|
||||
if (note.id) await Note.moveToFolder(note.id, itemValue);
|
||||
if (!note.id) {
|
||||
await this.saveNoteButton_press(itemValue);
|
||||
} else {
|
||||
await Note.moveToFolder(note.id, itemValue);
|
||||
}
|
||||
|
||||
note.parent_id = itemValue;
|
||||
|
||||
const folder = await Folder.load(note.parent_id);
|
||||
|
@@ -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),
|
||||
|
@@ -2,6 +2,7 @@ const { reg } = require('lib/registry.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
const shared = {};
|
||||
|
||||
@@ -10,15 +11,19 @@ shared.noteExists = async function(noteId) {
|
||||
return !!existingNote;
|
||||
}
|
||||
|
||||
shared.saveNoteButton_press = async function(comp) {
|
||||
shared.saveNoteButton_press = async function(comp, folderId = null) {
|
||||
let note = Object.assign({}, comp.state.note);
|
||||
|
||||
// Note has been deleted while user was modifying it. In that case, we
|
||||
// just save a new note by clearing the note ID.
|
||||
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
|
||||
|
||||
if (!note.parent_id) {
|
||||
let folder = await Folder.defaultFolder();
|
||||
if (folderId) {
|
||||
note.parent_id = folderId;
|
||||
} else if (!note.parent_id) {
|
||||
const activeFolderId = Setting.value('activeFolderId');
|
||||
let folder = await Folder.load(activeFolderId);
|
||||
if (!folder) folder = await Folder.defaultFolder();
|
||||
if (!folder) return;
|
||||
note.parent_id = folder.id;
|
||||
}
|
||||
|
@@ -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;
|
@@ -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; });
|
||||
|
@@ -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)
|
||||
|
@@ -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));
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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;
|
@@ -6,6 +6,7 @@ const { time } = require('lib/time-utils.js');
|
||||
const { sprintf } = require('sprintf-js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const moment = require('moment');
|
||||
const { markdownUtils } = require('lib/markdown-utils.js');
|
||||
|
||||
class BaseItem extends BaseModel {
|
||||
|
||||
@@ -649,6 +650,15 @@ class BaseItem extends BaseModel {
|
||||
return super.save(o, options);
|
||||
}
|
||||
|
||||
static markdownTag(item) {
|
||||
const output = [];
|
||||
output.push('[');
|
||||
output.push(markdownUtils.escapeLinkText(item.title));
|
||||
output.push(']');
|
||||
output.push('(:/' + item.id + ')');
|
||||
return output.join('');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BaseItem.encryptionService_ = null;
|
||||
|
@@ -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
|
||||
|
@@ -107,7 +107,7 @@ class Note extends BaseItem {
|
||||
return BaseModel.TYPE_NOTE;
|
||||
}
|
||||
|
||||
static linkedResourceIds(body) {
|
||||
static linkedItemIds(body) {
|
||||
// For example: 
|
||||
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');
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -46,7 +46,7 @@ function toSystemSlashes(path, os) {
|
||||
}
|
||||
|
||||
function rtrimSlashes(path) {
|
||||
return path.replace(/\/+$/, '');
|
||||
return path.replace(/[\/\\]+$/, '');
|
||||
}
|
||||
|
||||
function ltrimSlashes(path) {
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
@@ -5,6 +5,7 @@ const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { setLocale, defaultLocale, closestSupportedLocale } = require('lib/locale.js');
|
||||
const { FsDriverNode } = require('lib/fs-driver-node.js');
|
||||
const urlValidator = require('valid-url');
|
||||
|
||||
function shimInit() {
|
||||
shim.fsDriver = () => { throw new Error('Not implemented') }
|
||||
@@ -34,6 +35,23 @@ function shimInit() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
// For Electron only
|
||||
shim.writeImageToFile = async function(nativeImage, mime, targetPath) {
|
||||
let buffer = null;
|
||||
|
||||
mime = mime.toLowerCase();
|
||||
|
||||
if (mime === 'image/png') {
|
||||
buffer = nativeImage.toPNG();
|
||||
} else if (mime === 'image/jpg' || mime === 'image/jpeg') {
|
||||
buffer = nativeImage.toJPEG(90);
|
||||
}
|
||||
|
||||
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
|
||||
|
||||
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
|
||||
}
|
||||
|
||||
const resizeImage_ = async function(filePath, targetPath, mime) {
|
||||
if (shim.isElectron()) { // For Electron
|
||||
const nativeImage = require('electron').nativeImage;
|
||||
@@ -57,17 +75,7 @@ function shimInit() {
|
||||
|
||||
image = image.resize(options);
|
||||
|
||||
let buffer = null;
|
||||
|
||||
if (mime === 'image/png') {
|
||||
buffer = image.toPNG();
|
||||
} else if (mime === 'image/jpg' || mime === 'image/jpeg') {
|
||||
buffer = image.toJPEG(90);
|
||||
}
|
||||
|
||||
if (!buffer) throw new Error('Cannot reisze image because mime type "' + mime + '" is not supported: ' + targetPath);
|
||||
|
||||
await shim.fsDriver().writeFile(targetPath, buffer, 'buffer');
|
||||
await shim.writeImageToFile(image, mime, targetPath);
|
||||
} else { // For the CLI tool
|
||||
const sharp = require('sharp');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
@@ -88,7 +96,7 @@ function shimInit() {
|
||||
}
|
||||
}
|
||||
|
||||
shim.attachFileToNote = async function(note, filePath) {
|
||||
shim.attachFileToNote = async function(note, filePath, position = null) {
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { basename, fileExtension, safeFileExtension } = require('lib/path-utils.js');
|
||||
@@ -110,14 +118,23 @@ function shimInit() {
|
||||
if (resource.mime == 'image/jpeg' || resource.mime == 'image/jpg' || resource.mime == 'image/png') {
|
||||
const result = await resizeImage_(filePath, targetPath, resource.mime);
|
||||
} else {
|
||||
const stat = await shim.fsDriver().stat(filePath);
|
||||
if (stat.size >= 10000000) throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
|
||||
|
||||
await fs.copy(filePath, targetPath, { overwrite: true });
|
||||
}
|
||||
|
||||
await Resource.save(resource, { isNew: true });
|
||||
|
||||
const newBody = [];
|
||||
if (note.body) newBody.push(note.body);
|
||||
|
||||
if (position === null) {
|
||||
position = note.body ? note.body.length : 0;
|
||||
}
|
||||
|
||||
if (note.body && position) newBody.push(note.body.substr(0, position));
|
||||
newBody.push(Resource.markdownTag(resource));
|
||||
newBody.push(note.body.substr(position));
|
||||
|
||||
const newNote = Object.assign({}, note, {
|
||||
body: newBody.join('\n\n'),
|
||||
@@ -133,6 +150,9 @@ function shimInit() {
|
||||
}
|
||||
|
||||
shim.fetch = async function(url, options = null) {
|
||||
const validatedUrl = urlValidator.isUri(url);
|
||||
if (!validatedUrl) throw new Error('Not a valid URL: ' + url);
|
||||
|
||||
return shim.fetchWithRetry(() => {
|
||||
return nodeFetch(url, options)
|
||||
}, options);
|
||||
|
@@ -263,7 +263,7 @@ class Synchronizer {
|
||||
//
|
||||
// TODO: assuming a particular sync target is guaranteed to have accurate timestamps, the driver maybe
|
||||
// could expose this with a accurateTimestamps() method that returns "true". In that case, the test
|
||||
// could be done using the file timestamp and the potentially unecessary content loading could be skipped.
|
||||
// could be done using the file timestamp and the potentially unnecessary content loading could be skipped.
|
||||
// OneDrive does not appear to have accurate timestamps as lastModifiedDateTime would occasionally be
|
||||
// a few seconds ahead of what it was set with setTimestamp()
|
||||
remoteContent = await this.api().get(path);
|
||||
@@ -567,7 +567,7 @@ class Synchronizer {
|
||||
// If user has cancelled, don't record the new context (2) so that synchronisation
|
||||
// can start again from the previous context (1) next time. It is ok if some items
|
||||
// have been synced between (1) and (2) because the loop above will handle the same
|
||||
// items being synced twice as an update. If the local and remote items are indentical
|
||||
// items being synced twice as an update. If the local and remote items are identical
|
||||
// the update will simply be skipped.
|
||||
if (!hasCancelled) {
|
||||
if (!listResult.hasMore) {
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user