Merge branch 'master' of github.com:laurent22/joplin

pull/2456/head
Laurent Cozic 2020-02-06 11:55:35 +00:00
commit 10cf80d6ca
53 changed files with 1690 additions and 630 deletions

View File

@ -68,6 +68,8 @@ module.exports = {
"linebreak-style": ["error", "unix"], "linebreak-style": ["error", "unix"],
"prefer-template": ["error"], "prefer-template": ["error"],
"template-curly-spacing": ["error", "never"], "template-curly-spacing": ["error", "never"],
"object-curly-spacing": ["error", "always"],
"array-bracket-spacing": ["error", "never"],
"key-spacing": ["error", { "key-spacing": ["error", {
"beforeColon": false, "beforeColon": false,
"afterColon": true, "afterColon": true,

View File

@ -25,7 +25,7 @@ class Command extends BaseCommand {
info: stdoutFn, info: stdoutFn,
warn: stdoutFn, warn: stdoutFn,
error: stdoutFn, error: stdoutFn,
}}); } });
ClipperServer.instance().setDispatch(() => {}); ClipperServer.instance().setDispatch(() => {});
ClipperServer.instance().setLogger(clipperLogger); ClipperServer.instance().setLogger(clipperLogger);

View File

@ -7,13 +7,13 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Joplin-CLI 1.0.0\n" "Project-Id-Version: Joplin-CLI 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"Last-Translator: Michael Sonntag <ms@editorei.de>\n" "Last-Translator: Fabian <fab4x@mailbox.org>\n"
"Language-Team: \n" "Language-Team: \n"
"Language: de_DE\n" "Language: de_DE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.4\n" "X-Generator: Poedit 2.3\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"POT-Creation-Date: \n" "POT-Creation-Date: \n"
"PO-Revision-Date: \n" "PO-Revision-Date: \n"
@ -1518,7 +1518,7 @@ msgstr "Link-Adresse kopieren"
#: /mnt/c/Users/laurent/src/joplin/Tools/../ElectronClient/app/gui/NoteText.min.js:829 #: /mnt/c/Users/laurent/src/joplin/Tools/../ElectronClient/app/gui/NoteText.min.js:829
msgid "There was an error downloading this attachment:" msgid "There was an error downloading this attachment:"
msgstr "" msgstr "Es gab einen Fehler beim Herunterladen des Anhangs: "
#: /mnt/c/Users/laurent/src/joplin/Tools/../ElectronClient/app/gui/NoteText.min.js:831 #: /mnt/c/Users/laurent/src/joplin/Tools/../ElectronClient/app/gui/NoteText.min.js:831
msgid "This attachment is not downloaded or not decrypted yet" msgid "This attachment is not downloaded or not decrypted yet"
@ -1836,6 +1836,11 @@ msgid ""
"\n" "\n"
"%s" "%s"
msgstr "" msgstr ""
"Konnte keine Verbindung zu der Joplin Nextcloud Applikation herstellen. "
"Bitte überprüfe die Konfiguration in der Ausgabe der Synchronisation. "
"Vollständige Fehlermeldung:\n"
"\n"
"%s"
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/SyncTargetDropbox.js:25 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/SyncTargetDropbox.js:25
msgid "Dropbox" msgid "Dropbox"
@ -2238,6 +2243,8 @@ msgstr "Sortiere Notizen nach"
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:293 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:293
msgid "Auto-pair braces, parenthesis, quotations, etc." msgid "Auto-pair braces, parenthesis, quotations, etc."
msgstr "" msgstr ""
"Automatisches hinzufügen von Geschweiften Klammern, runden Klammern, "
"Anführungszeichen usw."
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:295 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:295
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:313 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:313
@ -2375,7 +2382,7 @@ msgstr ""
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:463 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:463
msgid "Custom stylesheet for Joplin-wide app styles" msgid "Custom stylesheet for Joplin-wide app styles"
msgstr "" msgstr "Benutzerdefiniertes Stylesheet für Programmweiten Stil"
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:467 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/models/Setting.js:467
msgid "Automatically update the application" msgid "Automatically update the application"
@ -2951,6 +2958,7 @@ msgstr "Exportiere Profil"
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/components/screens/config.js:438 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/components/screens/config.js:438
msgid "For debugging purpose only: export your profile to an external SD card." msgid "For debugging purpose only: export your profile to an external SD card."
msgstr "" msgstr ""
"Nur für Debugging-Zwecke: Exportiere dein Profil auf eine externe SD-Karte."
#: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/components/screens/config.js:453 #: /mnt/c/Users/laurent/src/joplin/Tools/../ReactNativeClient/lib/components/screens/config.js:453
msgid "More information" msgid "More information"

View File

@ -90,13 +90,13 @@ describe('models_Note', function() {
})); }));
it('should serialize and unserialize without modifying data', asyncTest(async () => { it('should serialize and unserialize without modifying data', asyncTest(async () => {
let folder1 = await Folder.save({ title: 'folder1'}); let folder1 = await Folder.save({ title: 'folder1' });
const testCases = [ const testCases = [
[ {title: '', body: 'Body and no title\nSecond line\nThird Line', parent_id: folder1.id}, [{ title: '', body: 'Body and no title\nSecond line\nThird Line', parent_id: folder1.id },
'', 'Body and no title\nSecond line\nThird Line'], '', 'Body and no title\nSecond line\nThird Line'],
[ {title: 'Note title', body: 'Body and title', parent_id: folder1.id}, [{ title: 'Note title', body: 'Body and title', parent_id: folder1.id },
'Note title', 'Body and title'], 'Note title', 'Body and title'],
[ {title: 'Title and no body', body: '', parent_id: folder1.id}, [{ title: 'Title and no body', body: '', parent_id: folder1.id },
'Title and no body', ''], 'Title and no body', ''],
]; ];
@ -116,4 +116,17 @@ describe('models_Note', function() {
} }
})); }));
it('should reset fields for a duplicate', asyncTest(async () => {
let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'note', parent_id: folder1.id });
let duplicatedNote = await Note.duplicate(note1.id);
expect(duplicatedNote !== note1).toBe(true);
expect(duplicatedNote.created_time !== note1.created_time).toBe(true);
expect(duplicatedNote.updated_time !== note1.updated_time).toBe(true);
expect(duplicatedNote.user_created_time !== note1.user_created_time).toBe(true);
expect(duplicatedNote.user_updated_time !== note1.user_updated_time).toBe(true);
}));
}); });

View File

@ -86,7 +86,7 @@ describe('models_Tag', function() {
let folder1 = await Folder.save({ title: 'folder1' }); let folder1 = await Folder.save({ title: 'folder1' });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
let note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id }); let note2 = await Note.save({ title: 'ma 2nd note', parent_id: folder1.id });
let tag = await Tag.save({ title: 'mytag'}); let tag = await Tag.save({ title: 'mytag' });
await Tag.addNote(tag.id, note1.id); await Tag.addNote(tag.id, note1.id);
let tagWithCount = await Tag.loadWithCount(tag.id); let tagWithCount = await Tag.loadWithCount(tag.id);
@ -97,4 +97,57 @@ describe('models_Tag', function() {
expect(tagWithCount.note_count).toBe(2); expect(tagWithCount.note_count).toBe(2);
})); }));
it('should get common tags for set of notes', asyncTest(async () => {
let folder1 = await Folder.save({ title: 'folder1' });
let taga = await Tag.save({ title: 'mytaga' });
let tagb = await Tag.save({ title: 'mytagb' });
let tagc = await Tag.save({ title: 'mytagc' });
let tagd = await Tag.save({ title: 'mytagd' });
let note0 = await Note.save({ title: 'ma note 0', parent_id: folder1.id });
let note1 = await Note.save({ title: 'ma note 1', parent_id: folder1.id });
let note2 = await Note.save({ title: 'ma note 2', parent_id: folder1.id });
let note3 = await Note.save({ title: 'ma note 3', parent_id: folder1.id });
await Tag.addNote(taga.id, note1.id);
await Tag.addNote(taga.id, note2.id);
await Tag.addNote(tagb.id, note2.id);
await Tag.addNote(taga.id, note3.id);
await Tag.addNote(tagb.id, note3.id);
await Tag.addNote(tagc.id, note3.id);
let commonTags = await Tag.commonTagsByNoteIds(null);
expect(commonTags.length).toBe(0);
commonTags = await Tag.commonTagsByNoteIds(undefined);
expect(commonTags.length).toBe(0);
commonTags = await Tag.commonTagsByNoteIds([]);
expect(commonTags.length).toBe(0);
commonTags = await Tag.commonTagsByNoteIds([note0.id, note1.id, note2.id, note3.id]);
let commonTagIds = commonTags.map(t => t.id);
expect(commonTagIds.length).toBe(0);
commonTags = await Tag.commonTagsByNoteIds([note1.id, note2.id, note3.id]);
commonTagIds = commonTags.map(t => t.id);
expect(commonTagIds.length).toBe(1);
expect(commonTagIds.includes(taga.id)).toBe(true);
commonTags = await Tag.commonTagsByNoteIds([note2.id, note3.id]);
commonTagIds = commonTags.map(t => t.id);
expect(commonTagIds.length).toBe(2);
expect(commonTagIds.includes(taga.id)).toBe(true);
expect(commonTagIds.includes(tagb.id)).toBe(true);
commonTags = await Tag.commonTagsByNoteIds([note3.id]);
commonTagIds = commonTags.map(t => t.id);
expect(commonTags.length).toBe(3);
expect(commonTagIds.includes(taga.id)).toBe(true);
expect(commonTagIds.includes(tagb.id)).toBe(true);
expect(commonTagIds.includes(tagc.id)).toBe(true);
}));
}); });

View File

@ -1,11 +1,11 @@
/* eslint-disable no-unused-vars */ /* eslint-disable no-unused-vars */
require('app-module-path').addPath(__dirname); require('app-module-path').addPath(__dirname);
const {setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js'); const { setupDatabaseAndSynchronizer, switchClient, asyncTest } = require('test-utils.js');
const Folder = require('lib/models/Folder.js'); const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js'); const Note = require('lib/models/Note.js');
const Tag = require('lib/models/Tag.js'); const Tag = require('lib/models/Tag.js');
const { reducer, defaultState, stateUtils} = require('lib/reducer.js'); const { reducer, defaultState, stateUtils } = require('lib/reducer.js');
async function createNTestFolders(n) { async function createNTestFolders(n) {
let folders = []; let folders = [];
@ -19,7 +19,7 @@ async function createNTestFolders(n) {
async function createNTestNotes(n, folder) { async function createNTestNotes(n, folder) {
let notes = []; let notes = [];
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
let note = await Note.save({ title: 'note', parent_id: folder.id }); let note = await Note.save({ title: 'note', parent_id: folder.id, is_conflict: 0 });
notes.push(note); notes.push(note);
} }
return notes; return notes;
@ -36,12 +36,13 @@ async function createNTestTags(n) {
function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) { function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tags=null, selectedTagIndex=null) {
let state = defaultState; let state = defaultState;
if (folders != null) {
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
}
if (selectedFolderIndex != null) { if (selectedFolderIndex != null) {
state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id }); state = reducer(state, { type: 'FOLDER_SELECT', id: folders[selectedFolderIndex].id });
} }
if (folders != null) {
state = reducer(state, { type: 'FOLDER_UPDATE_ALL', items: folders });
}
if (notes != null) { if (notes != null) {
state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' }); state = reducer(state, { type: 'NOTE_UPDATE_ALL', notes: notes, noteSource: 'test' });
} }
@ -63,7 +64,7 @@ function initTestState(folders, selectedFolderIndex, notes, selectedIndexes, tag
} }
function createExpectedState(items, keepIndexes, selectedIndexes) { function createExpectedState(items, keepIndexes, selectedIndexes) {
let expected = { items: [], selectedIds: []}; let expected = { items: [], selectedIds: [] };
for (let i = 0; i < selectedIndexes.length; i++) { for (let i = 0; i < selectedIndexes.length; i++) {
expected.selectedIds.push(items[selectedIndexes[i]].id); expected.selectedIds.push(items[selectedIndexes[i]].id);
@ -110,7 +111,7 @@ describe('Reducer', function() {
// test action // test action
// delete the third note // delete the third note
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
// expect that the third note is missing, and the 4th note is now selected // expect that the third note is missing, and the 4th note is now selected
let expected = createExpectedState(notes, [0,1,3,4], [3]); let expected = createExpectedState(notes, [0,1,3,4], [3]);
@ -127,7 +128,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [1]); let state = initTestState(folders, 0, notes, [1]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
let expected = createExpectedState(notes, [1,2,3,4], [1]); let expected = createExpectedState(notes, [1,2,3,4], [1]);
@ -141,7 +142,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [0]); let state = initTestState(folders, 0, notes, [0]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
let expected = createExpectedState(notes, [], []); let expected = createExpectedState(notes, [], []);
@ -155,7 +156,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [4]); let state = initTestState(folders, 0, notes, [4]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
let expected = createExpectedState(notes, [0,1,2,3], [3]); let expected = createExpectedState(notes, [0,1,2,3], [3]);
@ -169,7 +170,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [3]); let state = initTestState(folders, 0, notes, [3]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
let expected = createExpectedState(notes, [0,2,3,4], [3]); let expected = createExpectedState(notes, [0,2,3,4], [3]);
@ -183,7 +184,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [1]); let state = initTestState(folders, 0, notes, [1]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
let expected = createExpectedState(notes, [0,1,2,4], [1]); let expected = createExpectedState(notes, [0,1,2,4], [1]);
@ -197,8 +198,8 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [1,2]); let state = initTestState(folders, 0, notes, [1,2]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
let expected = createExpectedState(notes, [0,3,4], [3]); let expected = createExpectedState(notes, [0,3,4], [3]);
@ -212,7 +213,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [3,4]); let state = initTestState(folders, 0, notes, [3,4]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[1].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[1].id });
let expected = createExpectedState(notes, [0,2,3,4], [3,4]); let expected = createExpectedState(notes, [0,2,3,4], [3,4]);
@ -226,7 +227,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [1,2]); let state = initTestState(folders, 0, notes, [1,2]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
let expected = createExpectedState(notes, [0,1,2,4], [1,2]); let expected = createExpectedState(notes, [0,1,2,4], [1,2]);
@ -240,8 +241,8 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [3,4]); let state = initTestState(folders, 0, notes, [3,4]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[3].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[3].id });
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
let expected = createExpectedState(notes, [0,1,2], [2]); let expected = createExpectedState(notes, [0,1,2], [2]);
@ -255,9 +256,9 @@ describe('Reducer', function() {
let state = initTestState(folders, 0, notes, [0,2,4]); let state = initTestState(folders, 0, notes, [0,2,4]);
// test action // test action
state = reducer(state, {type: 'NOTE_DELETE', id: notes[0].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[0].id });
state = reducer(state, {type: 'NOTE_DELETE', id: notes[2].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[2].id });
state = reducer(state, {type: 'NOTE_DELETE', id: notes[4].id}); state = reducer(state, { type: 'NOTE_DELETE', id: notes[4].id });
let expected = createExpectedState(notes, [1,3], [1]); let expected = createExpectedState(notes, [1,3], [1]);
@ -272,7 +273,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 2, notes, [2]); let state = initTestState(folders, 2, notes, [2]);
// test action // test action
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id}); state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
let expected = createExpectedState(folders, [0,1,3,4], [3]); let expected = createExpectedState(folders, [0,1,3,4], [3]);
@ -286,7 +287,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 1, notes, [2]); let state = initTestState(folders, 1, notes, [2]);
// test action // test action
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id}); state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
let expected = createExpectedState(folders, [0,1,3,4], [1]); let expected = createExpectedState(folders, [0,1,3,4], [1]);
@ -300,7 +301,7 @@ describe('Reducer', function() {
let state = initTestState(folders, 4, notes, [2]); let state = initTestState(folders, 4, notes, [2]);
// test action // test action
state = reducer(state, {type: 'FOLDER_DELETE', id: folders[2].id}); state = reducer(state, { type: 'FOLDER_DELETE', id: folders[2].id });
let expected = createExpectedState(folders, [0,1,3,4], [4]); let expected = createExpectedState(folders, [0,1,3,4], [4]);
@ -314,7 +315,7 @@ describe('Reducer', function() {
let state = initTestState(null, null, null, null, tags, [2]); let state = initTestState(null, null, null, null, tags, [2]);
// test action // test action
state = reducer(state, {type: 'TAG_DELETE', id: tags[2].id}); state = reducer(state, { type: 'TAG_DELETE', id: tags[2].id });
let expected = createExpectedState(tags, [0,1,3,4], [3]); let expected = createExpectedState(tags, [0,1,3,4], [3]);
@ -327,7 +328,7 @@ describe('Reducer', function() {
let state = initTestState(null, null, null, null, tags, [2]); let state = initTestState(null, null, null, null, tags, [2]);
// test action // test action
state = reducer(state, {type: 'TAG_DELETE', id: tags[4].id}); state = reducer(state, { type: 'TAG_DELETE', id: tags[4].id });
let expected = createExpectedState(tags, [0,1,2,3], [2]); let expected = createExpectedState(tags, [0,1,2,3], [2]);
@ -340,11 +341,35 @@ describe('Reducer', function() {
let state = initTestState(null, null, null, null, tags, [2]); let state = initTestState(null, null, null, null, tags, [2]);
// test action // test action
state = reducer(state, {type: 'TAG_DELETE', id: tags[0].id}); state = reducer(state, { type: 'TAG_DELETE', id: tags[0].id });
let expected = createExpectedState(tags, [1,2,3,4], [2]); let expected = createExpectedState(tags, [1,2,3,4], [2]);
expect(getIds(state.tags)).toEqual(getIds(expected.items)); expect(getIds(state.tags)).toEqual(getIds(expected.items));
expect(state.selectedTagId).toEqual(expected.selectedIds[0]); expect(state.selectedTagId).toEqual(expected.selectedIds[0]);
})); }));
it('should select all notes', asyncTest(async () => {
let folders = await createNTestFolders(2);
let notes = [];
for (let i = 0; i < folders.length; i++) {
notes.push(...await createNTestNotes(3, folders[i]));
}
let state = initTestState(folders, 0, notes.slice(0,3), [0]);
let expected = createExpectedState(notes, [0,1,2], [0]);
expect(state.notes.length).toEqual(expected.items.length);
expect(getIds(state.notes.slice(0,4))).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
// test action
state = reducer(state, { type: 'NOTE_SELECT_ALL' });
expected = createExpectedState(notes.slice(0,3), [0,1,2], [0,1,2]);
expect(getIds(state.notes)).toEqual(getIds(expected.items));
expect(state.selectedNoteIds).toEqual(expected.selectedIds);
}));
}); });

View File

@ -279,7 +279,7 @@ describe('services_rest_Api', function() {
const response = await api.route('GET', 'notes', { token: 'mytoken' }); const response = await api.route('GET', 'notes', { token: 'mytoken' });
expect(response.length).toBe(0); expect(response.length).toBe(0);
hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({title: 'testing'}))); hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({ title: 'testing' })));
expect(hasThrown).toBe(true); expect(hasThrown).toBe(true);
})); }));

View File

@ -189,10 +189,10 @@ class AppComponent extends Component {
} }
async loadContentScripts() { async loadContentScripts() {
await bridge().tabsExecuteScript({file: '/content_scripts/JSDOMParser.js'}); await bridge().tabsExecuteScript({ file: '/content_scripts/JSDOMParser.js' });
await bridge().tabsExecuteScript({file: '/content_scripts/Readability.js'}); await bridge().tabsExecuteScript({ file: '/content_scripts/Readability.js' });
await bridge().tabsExecuteScript({file: '/content_scripts/Readability-readerable.js'}); await bridge().tabsExecuteScript({ file: '/content_scripts/Readability-readerable.js' });
await bridge().tabsExecuteScript({file: '/content_scripts/index.js'}); await bridge().tabsExecuteScript({ file: '/content_scripts/index.js' });
} }
async componentDidMount() { async componentDidMount() {
@ -248,7 +248,7 @@ class AppComponent extends Component {
if (!this.state.contentScriptLoaded) { if (!this.state.contentScriptLoaded) {
let msg = 'Loading...'; let msg = 'Loading...';
if (this.state.contentScriptError) msg = `The Joplin extension is not available on this tab due to: ${this.state.contentScriptError}`; if (this.state.contentScriptError) msg = `The Joplin extension is not available on this tab due to: ${this.state.contentScriptError}`;
return <div style={{padding: 10, fontSize: 12, maxWidth: 200}}>{msg}</div>; return <div style={{ padding: 10, fontSize: 12, maxWidth: 200 }}>{msg}</div>;
} }
const warningComponent = !this.props.warning ? null : <div className="Warning">{ this.props.warning }</div>; const warningComponent = !this.props.warning ? null : <div className="Warning">{ this.props.warning }</div>;

View File

@ -124,6 +124,13 @@ class ElectronAppWrapper {
// automatically (the listeners will be removed when the window is closed) // automatically (the listeners will be removed when the window is closed)
// and restore the maximized or full screen state // and restore the maximized or full screen state
windowState.manage(this.win_); windowState.manage(this.win_);
// HACK: Ensure the window is hidden, as `windowState.manage` may make the window
// visible with isMaximized set to true in window-state-${this.env_}.json.
// https://github.com/laurent22/joplin/issues/2365
if (!windowOptions.show) {
this.win_.hide();
}
} }
async waitForElectronAppReady() { async waitForElectronAppReady() {

View File

@ -61,12 +61,12 @@ class InteropServiceHelper {
cleanup(); cleanup();
} }
} else { } else {
win.webContents.print(options, (success) => { win.webContents.print(options, (success, reason) => {
// TODO: This is correct but broken in Electron 4. Need to upgrade to 5+ // TODO: This is correct but broken in Electron 4. Need to upgrade to 5+
// It calls the callback right away with "false" even if the document hasn't be print yet. // It calls the callback right away with "false" even if the document hasn't be print yet.
cleanup(); cleanup();
if (!success) reject(new Error('Could not print')); if (!success && reason !== 'cancelled') reject(new Error(`Could not print: ${reason}`));
resolve(); resolve();
}); });
} }
@ -99,7 +99,7 @@ class InteropServiceHelper {
if (module.target === 'file') { if (module.target === 'file') {
path = bridge().showSaveDialog({ path = bridge().showSaveDialog({
filters: [{ name: module.description, extensions: module.fileExtensions}], filters: [{ name: module.description, extensions: module.fileExtensions }],
}); });
} else { } else {
path = bridge().showOpenDialog({ path = bridge().showOpenDialog({

View File

@ -408,7 +408,7 @@ class Application extends BaseApplication {
if (moduleSource === 'file') { if (moduleSource === 'file') {
path = bridge().showOpenDialog({ path = bridge().showOpenDialog({
filters: [{ name: module.description, extensions: module.fileExtensions}], filters: [{ name: module.description, extensions: module.fileExtensions }],
}); });
} else { } else {
path = bridge().showOpenDialog({ path = bridge().showOpenDialog({
@ -874,12 +874,10 @@ class Application extends BaseApplication {
accelerator: 'CommandOrControl+Alt+T', accelerator: 'CommandOrControl+Alt+T',
click: () => { click: () => {
const selectedNoteIds = this.store().getState().selectedNoteIds; const selectedNoteIds = this.store().getState().selectedNoteIds;
if (selectedNoteIds.length !== 1) return;
this.dispatch({ this.dispatch({
type: 'WINDOW_COMMAND', type: 'WINDOW_COMMAND',
name: 'setTags', name: 'setTags',
noteId: selectedNoteIds[0], noteIds: selectedNoteIds,
}); });
}, },
}, { }, {
@ -1132,7 +1130,7 @@ class Application extends BaseApplication {
const selectedNoteIds = state.selectedNoteIds; const selectedNoteIds = state.selectedNoteIds;
const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null; const note = selectedNoteIds.length === 1 ? await Note.load(selectedNoteIds[0]) : null;
for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'setTags', 'showLocalSearch']) { for (const itemId of ['copy', 'paste', 'cut', 'selectAll', 'bold', 'italic', 'link', 'code', 'insertDateTime', 'commandStartExternalEditing', 'showLocalSearch']) {
const menuItem = Menu.getApplicationMenu().getMenuItemById(`edit:${itemId}`); const menuItem = Menu.getApplicationMenu().getMenuItemById(`edit:${itemId}`);
if (!menuItem) continue; if (!menuItem) continue;
menuItem.enabled = !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN; menuItem.enabled = !!note && note.markup_language === MarkupToHtml.MARKUP_LANGUAGE_MARKDOWN;

View File

@ -56,7 +56,7 @@ class Bridge {
} }
showSaveDialog(options) { showSaveDialog(options) {
const {dialog} = require('electron'); const { dialog } = require('electron');
if (!options) options = {}; if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_; if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
const filePath = dialog.showSaveDialogSync(this.window(), options); const filePath = dialog.showSaveDialogSync(this.window(), options);
@ -67,7 +67,7 @@ class Bridge {
} }
showOpenDialog(options) { showOpenDialog(options) {
const {dialog} = require('electron'); const { dialog } = require('electron');
if (!options) options = {}; if (!options) options = {};
if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_; if (!('defaultPath' in options) && this.lastSelectedPath_) options.defaultPath = this.lastSelectedPath_;
if (!('createDirectory' in options)) options.createDirectory = true; if (!('createDirectory' in options)) options.createDirectory = true;
@ -80,7 +80,7 @@ class Bridge {
// Don't use this directly - call one of the showXxxxxxxMessageBox() instead // Don't use this directly - call one of the showXxxxxxxMessageBox() instead
showMessageBox_(window, options) { showMessageBox_(window, options) {
const {dialog} = require('electron'); const { dialog } = require('electron');
if (!window) window = this.window(); if (!window) window = this.window();
return dialog.showMessageBoxSync(window, options); return dialog.showMessageBoxSync(window, options);
} }
@ -89,6 +89,7 @@ class Bridge {
return this.showMessageBox_(this.window(), { return this.showMessageBox_(this.window(), {
type: 'error', type: 'error',
message: message, message: message,
buttons: [_('OK')],
}); });
} }

View File

@ -21,8 +21,8 @@ let branch;
let hash; let hash;
try { try {
// Use stdio: 'pipe' so that execSync doesn't print error directly to stdout // Use stdio: 'pipe' so that execSync doesn't print error directly to stdout
branch = execSync('git rev-parse --abbrev-ref HEAD', {stdio: 'pipe' }).toString().trim(); branch = execSync('git rev-parse --abbrev-ref HEAD', { stdio: 'pipe' }).toString().trim();
hash = execSync('git log --pretty="%h" -1', {stdio: 'pipe' }).toString().trim(); hash = execSync('git log --pretty="%h" -1', { stdio: 'pipe' }).toString().trim();
} catch (err) { } catch (err) {
// Don't display error object as it's a "fatal" error, but // Don't display error object as it's a "fatal" error, but
// not for us, since is it not critical information // not for us, since is it not critical information

View File

@ -121,9 +121,9 @@ class ClipperConfigScreenComponent extends React.Component {
<div style={stepBoxStyle}> <div style={stepBoxStyle}>
<p style={theme.h1Style}>{_('Step 2: Install the extension')}</p> <p style={theme.h1Style}>{_('Step 2: Install the extension')}</p>
<p style={theme.textStyle}>{_('Download and install the relevant extension for your browser:')}</p> <p style={theme.textStyle}>{_('Download and install the relevant extension for your browser:')}</p>
<div style={{display: 'flex', flexDirection: 'row'}}> <div style={{ display: 'flex', flexDirection: 'row' }}>
<ExtensionBadge theme={this.props.theme} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/> <ExtensionBadge theme={this.props.theme} type="firefox" url="https://addons.mozilla.org/en-US/firefox/addon/joplin-web-clipper/"/>
<ExtensionBadge style={{marginLeft: 10}} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/> <ExtensionBadge style={{ marginLeft: 10 }} theme={this.props.theme} type="chrome" url="https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek"/>
</div> </div>
</div> </div>

View File

@ -212,7 +212,7 @@ class ConfigScreenComponent extends React.Component {
if (advancedSettingComps.length) { if (advancedSettingComps.length) {
const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down'; const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down';
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 }); const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{fontSize: 14}} className={iconName}></i> {_('Show Advanced Settings')}</button>; advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{ fontSize: 14 }} className={iconName}></i> {_('Show Advanced Settings')}</button>;
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none'; advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
} }
@ -575,7 +575,7 @@ class ConfigScreenComponent extends React.Component {
borderTopColor: theme.dividerColor, borderTopColor: theme.dividerColor,
}; };
const screenComp = this.state.screenName ? <div style={{overflow: 'scroll', flex: 1}}>{this.screenFromName(this.state.screenName)}</div> : null; const screenComp = this.state.screenName ? <div style={{ overflow: 'scroll', flex: 1 }}>{this.screenFromName(this.state.screenName)}</div> : null;
if (screenComp) containerStyle.display = 'none'; if (screenComp) containerStyle.display = 'none';

View File

@ -144,33 +144,53 @@ class MainScreenComponent extends React.Component {
}, },
}); });
} else if (command.name === 'setTags') { } else if (command.name === 'setTags') {
const tags = await Tag.tagsByNoteId(command.noteId); const tags = await Tag.commonTagsByNoteIds(command.noteIds);
const noteTags = tags const startTags = tags
.map(a => { .map(a => {
return { value: a.id, label: a.title }; return { value: a.id, label: a.title };
}) })
.sort((a, b) => { .sort((a, b) => {
// sensitivity accent will treat accented characters as differemt // sensitivity accent will treat accented characters as differemt
// but treats caps as equal // but treats caps as equal
return a.label.localeCompare(b.label, undefined, {sensitivity: 'accent'}); return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
}); });
const allTags = await Tag.allWithNotes(); const allTags = await Tag.allWithNotes();
const tagSuggestions = allTags.map(a => { const tagSuggestions = allTags.map(a => {
return { value: a.id, label: a.title }; return { value: a.id, label: a.title };
})
.sort((a, b) => {
// sensitivity accent will treat accented characters as differemt
// but treats caps as equal
return a.label.localeCompare(b.label, undefined, { sensitivity: 'accent' });
}); });
this.setState({ this.setState({
promptOptions: { promptOptions: {
label: _('Add or remove tags:'), label: _('Add or remove tags:'),
inputType: 'tags', inputType: 'tags',
value: noteTags, value: startTags,
autocomplete: tagSuggestions, autocomplete: tagSuggestions,
onClose: async answer => { onClose: async answer => {
if (answer !== null) { if (answer !== null) {
const tagTitles = answer.map(a => { const endTagTitles = answer.map(a => {
return a.label.trim(); return a.label.trim();
}); });
await Tag.setNoteTagsByTitles(command.noteId, tagTitles); if (command.noteIds.length === 1) {
await Tag.setNoteTagsByTitles(command.noteIds[0], endTagTitles);
} else {
const startTagTitles = startTags.map(a => { return a.label.trim(); });
const addTags = endTagTitles.filter(value => !startTagTitles.includes(value));
const delTags = startTagTitles.filter(value => !endTagTitles.includes(value));
// apply the tag additions and deletions to each selected note
for (let i = 0; i < command.noteIds.length; i++) {
const tags = await Tag.tagsByNoteId(command.noteIds[i]);
let tagTitles = tags.map(a => { return a.title; });
tagTitles = tagTitles.concat(addTags);
tagTitles = tagTitles.filter(value => !delTags.includes(value));
await Tag.setNoteTagsByTitles(command.noteIds[i], tagTitles);
}
}
} }
this.setState({ promptOptions: null }); this.setState({ promptOptions: null });
}, },

View File

@ -295,20 +295,49 @@ class NoteListComponent extends React.Component {
} }
} }
scrollNoteIndex_(keyCode, ctrlKey, metaKey, noteIndex) {
if (keyCode === 33) {
// Page Up
noteIndex -= (this.itemListRef.current.visibleItemCount() - 1);
} else if (keyCode === 34) {
// Page Down
noteIndex += (this.itemListRef.current.visibleItemCount() - 1);
} else if ((keyCode === 35 && ctrlKey) || (keyCode === 40 && metaKey)) {
// CTRL+End, CMD+Down
noteIndex = this.props.notes.length - 1;
} else if ((keyCode === 36 && ctrlKey) || (keyCode === 38 && metaKey)) {
// CTRL+Home, CMD+Up
noteIndex = 0;
} else if (keyCode === 38 && !metaKey) {
// Up
noteIndex -= 1;
} else if (keyCode === 40 && !metaKey) {
// Down
noteIndex += 1;
}
if (noteIndex < 0) noteIndex = 0;
if (noteIndex > this.props.notes.length - 1) noteIndex = this.props.notes.length - 1;
return noteIndex;
}
async onKeyDown(event) { async onKeyDown(event) {
const keyCode = event.keyCode; const keyCode = event.keyCode;
const noteIds = this.props.selectedNoteIds; const noteIds = this.props.selectedNoteIds;
if (noteIds.length === 1 && (keyCode === 40 || keyCode === 38)) { if (noteIds.length === 1 && (keyCode === 40 || keyCode === 38 || keyCode === 33 || keyCode === 34 || keyCode === 35 || keyCode == 36)) {
// DOWN / UP // DOWN / UP / PAGEDOWN / PAGEUP / END / HOME
const noteId = noteIds[0]; const noteId = noteIds[0];
let noteIndex = BaseModel.modelIndexById(this.props.notes, noteId); let noteIndex = BaseModel.modelIndexById(this.props.notes, noteId);
const inc = keyCode === 38 ? -1 : +1;
noteIndex += inc; noteIndex = this.scrollNoteIndex_(keyCode, event.ctrlKey, event.metaKey, noteIndex);
if (noteIndex < 0) noteIndex = 0;
if (noteIndex > this.props.notes.length - 1) noteIndex = this.props.notes.length - 1;
const newSelectedNote = this.props.notes[noteIndex]; const newSelectedNote = this.props.notes[noteIndex];
@ -364,6 +393,15 @@ class NoteListComponent extends React.Component {
}); });
} }
} }
if (event.keyCode === 65 && (event.ctrlKey || event.metaKey)) {
// Ctrl+A key
event.preventDefault();
this.props.dispatch({
type: 'NOTE_SELECT_ALL',
});
}
} }
focusNoteId_(noteId) { focusNoteId_(noteId) {

View File

@ -118,6 +118,9 @@ class NoteSearchBarComponent extends React.Component {
render() { render() {
const query = this.props.query ? this.props.query : ''; const query = this.props.query ? this.props.query : '';
// backgroundColor needs to cached to a local variable to prevent the
// colour from blinking.
// For more info: https://github.com/laurent22/joplin/pull/2329#issuecomment-578376835
const theme = themeStyle(this.props.theme); const theme = themeStyle(this.props.theme);
if (!this.props.searching) { if (!this.props.searching) {
if (this.props.resultCount === 0 && query.length > 0) { if (this.props.resultCount === 0 && query.length > 0) {

View File

@ -1121,7 +1121,7 @@ class NoteTextComponent extends React.Component {
if (command.name === 'exportPdf') { if (command.name === 'exportPdf') {
fn = this.commandSavePdf; fn = this.commandSavePdf;
args = {noteId: command.noteId}; args = { noteId: command.noteId };
} else if (command.name === 'print') { } else if (command.name === 'print') {
fn = this.commandPrint; fn = this.commandPrint;
} }
@ -1342,7 +1342,7 @@ class NoteTextComponent extends React.Component {
this.props.dispatch({ this.props.dispatch({
type: 'WINDOW_COMMAND', type: 'WINDOW_COMMAND',
name: 'setTags', name: 'setTags',
noteId: this.state.note.id, noteIds: [this.state.note.id],
}); });
} }
@ -1406,7 +1406,7 @@ class NoteTextComponent extends React.Component {
} }
editorPasteText() { editorPasteText() {
this.wrapSelectionWithStrings('', '', '', clipboard.readText()); this.wrapSelectionWithStrings(clipboard.readText(), '', '', '');
} }
selectionRangePreviousLine() { selectionRangePreviousLine() {
@ -1425,7 +1425,7 @@ class NoteTextComponent extends React.Component {
return this.selectionRange_ ? this.rangeToTextOffsets(this.selectionRange_, this.state.note.body) : null; return this.selectionRange_ ? this.rangeToTextOffsets(this.selectionRange_, this.state.note.body) : null;
} }
wrapSelectionWithStrings(string1, string2 = '', defaultText = '', replacementText = '') { wrapSelectionWithStrings(string1, string2 = '', defaultText = '', replacementText = null, byLine = false) {
if (!this.rawEditor() || !this.state.note) return; if (!this.rawEditor() || !this.state.note) return;
const selection = this.textOffsetSelection(); const selection = this.textOffsetSelection();
@ -1433,10 +1433,14 @@ class NoteTextComponent extends React.Component {
let newBody = this.state.note.body; let newBody = this.state.note.body;
if (selection && selection.start !== selection.end) { if (selection && selection.start !== selection.end) {
const s1 = this.state.note.body.substr(0, selection.start); const selectedLines = replacementText !== null ? replacementText : this.state.note.body.substr(selection.start, selection.end - selection.start);
const s2 = replacementText ? replacementText : this.state.note.body.substr(selection.start, selection.end - selection.start); let selectedStrings = byLine ? selectedLines.split(/\r?\n/) : [selectedLines];
const s3 = this.state.note.body.substr(selection.end);
newBody = s1 + string1 + s2 + string2 + s3; newBody = this.state.note.body.substr(0, selection.start);
for (let i = 0; i < selectedStrings.length; i++) {
newBody += string1 + selectedStrings[i] + string2;
}
newBody += this.state.note.body.substr(selection.end);
const r = this.selectionRange_; const r = this.selectionRange_;
@ -1452,7 +1456,7 @@ class NoteTextComponent extends React.Component {
column: r.end.column + str1Split[str1Split.length - 1].length }, column: r.end.column + str1Split[str1Split.length - 1].length },
}; };
if (replacementText) { if (replacementText !== null) {
const diff = replacementText.length - (selection.end - selection.start); const diff = replacementText.length - (selection.end - selection.start);
newRange.end.column += diff; newRange.end.column += diff;
} }
@ -1468,7 +1472,7 @@ class NoteTextComponent extends React.Component {
editor.focus(); editor.focus();
}); });
} else { } else {
let middleText = replacementText ? replacementText : defaultText; let middleText = replacementText !== null ? replacementText : defaultText;
const textOffset = this.currentTextOffset(); const textOffset = this.currentTextOffset();
const s1 = this.state.note.body.substr(0, textOffset); const s1 = this.state.note.body.substr(0, textOffset);
const s2 = this.state.note.body.substr(textOffset); const s2 = this.state.note.body.substr(textOffset);
@ -1540,26 +1544,30 @@ class NoteTextComponent extends React.Component {
this.wrapSelectionWithStrings(TemplateUtils.render(value)); this.wrapSelectionWithStrings(TemplateUtils.render(value));
} }
addListItem(string1, string2 = '', defaultText = '') { addListItem(string1, string2 = '', defaultText = '', byLine=false) {
const currentLine = this.selectionRangeCurrentLine();
let newLine = '\n'; let newLine = '\n';
if (!currentLine) newLine = ''; const range = this.selectionRange_;
this.wrapSelectionWithStrings(newLine + string1, string2, defaultText); if (!range || (range.start.row === range.end.row && !this.selectionRangeCurrentLine())) {
newLine = '';
}
this.wrapSelectionWithStrings(newLine + string1, string2, defaultText, null, byLine);
} }
commandTextCheckbox() { commandTextCheckbox() {
this.addListItem('- [ ] ', '', _('List item')); this.addListItem('- [ ] ', '', _('List item'), true);
} }
commandTextListUl() { commandTextListUl() {
this.addListItem('- ', '', _('List item')); this.addListItem('- ', '', _('List item'), true);
} }
// Converting multiple lines to a numbered list will use the same number on each line
// Not ideal, but the rendered text will still be correct.
commandTextListOl() { commandTextListOl() {
let bulletNumber = markdownUtils.olLineNumber(this.selectionRangeCurrentLine()); let bulletNumber = markdownUtils.olLineNumber(this.selectionRangeCurrentLine());
if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(this.selectionRangePreviousLine()); if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(this.selectionRangePreviousLine());
if (!bulletNumber) bulletNumber = 0; if (!bulletNumber) bulletNumber = 0;
this.addListItem(`${bulletNumber + 1}. `, '', _('List item')); this.addListItem(`${bulletNumber + 1}. `, '', _('List item'), true);
} }
commandTextHeading() { commandTextHeading() {
@ -1938,14 +1946,17 @@ class NoteTextComponent extends React.Component {
paddingLeft: 8, paddingLeft: 8,
paddingRight: 8, paddingRight: 8,
marginRight: rootStyle.paddingLeft, marginRight: rootStyle.paddingLeft,
color: theme.color, color: theme.textStyle.color,
fontSize: theme.textStyle.fontSize * 1.25 *1.5,
backgroundColor: theme.backgroundColor, backgroundColor: theme.backgroundColor,
border: '1px solid', border: '1px solid',
borderColor: theme.dividerColor, borderColor: theme.dividerColor,
fontSize: theme.fontSize,
}; };
const toolbarStyle = {}; const toolbarStyle = {
marginTop: 3,
marginBottom: 0,
};
const tagStyle = { const tagStyle = {
marginBottom: 10, marginBottom: 10,
@ -1956,10 +1967,10 @@ class NoteTextComponent extends React.Component {
let bottomRowHeight = 0; let bottomRowHeight = 0;
if (this.canDisplayTagBar()) { if (this.canDisplayTagBar()) {
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - tagStyle.height - tagStyle.marginBottom; bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginTop - toolbarStyle.marginBottom - tagStyle.height - tagStyle.marginBottom;
} else { } else {
toolbarStyle.marginBottom = tagStyle.marginBottom, toolbarStyle.marginBottom = tagStyle.marginBottom,
bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginBottom; bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop - theme.toolbarHeight - toolbarStyle.marginTop - toolbarStyle.marginBottom;
} }
bottomRowHeight -= searchBarHeight; bottomRowHeight -= searchBarHeight;

View File

@ -70,7 +70,7 @@ class OneDriveLoginScreenComponent extends React.Component {
return ( return (
<div> <div>
<Header style={headerStyle}/> <Header style={headerStyle}/>
<div style={{padding: 10}}> <div style={{ padding: 10 }}>
{logComps} {logComps}
</div> </div>
</div> </div>

View File

@ -680,6 +680,11 @@ class SideBarComponent extends React.Component {
id: selectedItem.id, id: selectedItem.id,
}); });
} }
if (keyCode === 65 && (event.ctrlKey || event.metaKey)) {
// Ctrl+A key
event.preventDefault();
}
} }
onHeaderClick_(key, event) { onHeaderClick_(key, event) {

View File

@ -25,12 +25,11 @@ class NoteListUtils {
menu.append( menu.append(
new MenuItem({ new MenuItem({
label: _('Add or remove tags'), label: _('Add or remove tags'),
enabled: noteIds.length === 1,
click: async () => { click: async () => {
props.dispatch({ props.dispatch({
type: 'WINDOW_COMMAND', type: 'WINDOW_COMMAND',
name: 'setTags', name: 'setTags',
noteId: noteIds[0], noteIds: noteIds,
}); });
}, },
}) })

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "Joplin", "name": "Joplin",
"version": "1.0.179", "version": "1.0.181",
"description": "Joplin for Desktop", "description": "Joplin for Desktop",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -75,7 +75,7 @@
"babel-cli": "^6.26.0", "babel-cli": "^6.26.0",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"electron": "^7.1.9", "electron": "^7.1.9",
"electron-builder": "^21.2.0", "electron-builder": "20.15.0",
"electron-rebuild": "^1.8.8" "electron-rebuild": "^1.8.8"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -62,9 +62,9 @@ class Dialog extends React.PureComponent {
this.styles_[this.props.theme] = { this.styles_[this.props.theme] = {
dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }), dialogBox: Object.assign({}, theme.dialogBox, { minWidth: '50%', maxWidth: '50%' }),
input: Object.assign({}, theme.inputStyle, { flex: 1 }), input: Object.assign({}, theme.inputStyle, { flex: 1 }),
row: {overflow: 'hidden', height: itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10}, row: { overflow: 'hidden', height: itemHeight, display: 'flex', justifyContent: 'center', flexDirection: 'column', paddingLeft: 10, paddingRight: 10 },
help: Object.assign({}, theme.textStyle, { marginBottom: 10 }), help: Object.assign({}, theme.textStyle, { marginBottom: 10 }),
inputHelpWrapper: {display: 'flex', flexDirection: 'row', alignItems: 'center'}, inputHelpWrapper: { display: 'flex', flexDirection: 'row', alignItems: 'center' },
}; };
const rowTextStyle = { const rowTextStyle = {
@ -254,7 +254,7 @@ class Dialog extends React.PureComponent {
return ( return (
<div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}> <div key={item.id} style={rowStyle} onClick={this.listItem_onClick} data-id={item.id} data-parent-id={item.parent_id}>
<div style={style.rowTitle} dangerouslySetInnerHTML={{__html: titleHtml}}></div> <div style={style.rowTitle} dangerouslySetInnerHTML={{ __html: titleHtml }}></div>
{pathComp} {pathComp}
</div> </div>
); );

View File

@ -176,6 +176,14 @@ class BaseApplication {
continue; continue;
} }
if (arg === '--no-sandbox') {
// Electron-specific flag for running the app without chrome-sandbox
// Allows users to use it as a workaround for the electron+AppImage issue
// https://github.com/laurent22/joplin/issues/2246
argv.splice(0, 1);
continue;
}
if (arg.length && arg[0] == '-') { if (arg.length && arg[0] == '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError'); throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else { } else {

View File

@ -24,4 +24,4 @@ const injectCustomStyles = async cssFilePath => {
document.head.appendChild(styleTag); document.head.appendChild(styleTag);
}; };
module.exports = {loadCustomCss, injectCustomStyles}; module.exports = { loadCustomCss, injectCustomStyles };

View File

@ -3,7 +3,7 @@ const { shim } = require('lib/shim.js');
const JoplinError = require('lib/JoplinError'); const JoplinError = require('lib/JoplinError');
const { rtrimSlashes } = require('lib/path-utils.js'); const { rtrimSlashes } = require('lib/path-utils.js');
const base64 = require('base-64'); const base64 = require('base-64');
const {_ } = require('lib/locale'); const { _ } = require('lib/locale');
interface JoplinServerApiOptions { interface JoplinServerApiOptions {
username: Function, username: Function,

View File

@ -45,7 +45,7 @@ TemplateUtils.loadTemplates = async function(filePath) {
// Make sure templates are always in the same order // Make sure templates are always in the same order
// sensitivity ensures that the sort will ignore case // sensitivity ensures that the sort will ignore case
files.sort((a, b) => { return a.path.localeCompare(b.path, undefined, {sensitivity: 'accent'}); }); files.sort((a, b) => { return a.path.localeCompare(b.path, undefined, { sensitivity: 'accent' }); });
files.forEach(async file => { files.forEach(async file => {
if (file.path.endsWith('.md')) { if (file.path.endsWith('.md')) {

View File

@ -99,7 +99,7 @@ class CameraView extends Component {
return ( return (
<TouchableOpacity onPress={onPress} style={Object.assign({}, style)}> <TouchableOpacity onPress={onPress} style={Object.assign({}, style)}>
<View style={{borderRadius: 32, width: 60, height: 60, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center', alignSelf: 'baseline' }}> <View style={{ borderRadius: 32, width: 60, height: 60, borderColor: '#00000040', borderWidth: 1, borderStyle: 'solid', backgroundColor: '#ffffff77', justifyContent: 'center', alignItems: 'center', alignSelf: 'baseline' }}>
{ icon } { icon }
</View> </View>
</TouchableOpacity> </TouchableOpacity>
@ -152,7 +152,7 @@ class CameraView extends Component {
const displayRatios = shim.mobilePlatform() === 'android' && this.state.ratios.length > 1; const displayRatios = shim.mobilePlatform() === 'android' && this.state.ratios.length > 1;
const reverseCameraButton = this.renderButton(this.reverse_onPress, 'md-reverse-camera', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 }); const reverseCameraButton = this.renderButton(this.reverse_onPress, 'md-reverse-camera', { flex: 1, flexDirection: 'row', justifyContent: 'flex-start', marginLeft: 20 });
const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{fontWeight: 'bold', fontSize: 20}}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 }); const ratioButton = !displayRatios ? <View style={{ flex: 1 }}/> : this.renderButton(this.ratio_onPress, <Text style={{ fontWeight: 'bold', fontSize: 20 }}>{Setting.value('camera.ratio')}</Text>, { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', marginRight: 20 });
let cameraRatio = '4:3'; let cameraRatio = '4:3';
const cameraProps = {}; const cameraProps = {};

View File

@ -170,7 +170,7 @@ class ScreenHeaderComponent extends React.PureComponent {
// Duplicate all selected notes. ensureUniqueTitle is set to true to use the // Duplicate all selected notes. ensureUniqueTitle is set to true to use the
// original note's name as a root for the new unique identifier. // original note's name as a root for the new unique identifier.
await Note.duplicateMultipleNotes(noteIds, {ensureUniqueTitle: true}); await Note.duplicateMultipleNotes(noteIds, { ensureUniqueTitle: true });
this.props.dispatch({ type: 'NOTE_SELECTION_END' }); this.props.dispatch({ type: 'NOTE_SELECTION_END' });
} }

View File

@ -443,7 +443,7 @@ class ConfigScreenComponent extends BaseScreenComponent {
const profileExportPrompt = ( const profileExportPrompt = (
<View style={this.styles().settingContainer}> <View style={this.styles().settingContainer}>
<Text style={this.styles().settingText}>Path:</Text> <Text style={this.styles().settingText}>Path:</Text>
<TextInput style={{marginRight: 20}} onChange={(event) => this.setState({profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard"></TextInput> <TextInput style={{ marginRight: 20 }} onChange={(event) => this.setState({ profileExportPath: event.nativeEvent.text })} value={this.state.profileExportPath} placeholder="/path/to/sdcard"></TextInput>
<Button title="OK" onPress={this.exportProfileButtonPress2_}></Button> <Button title="OK" onPress={this.exportProfileButtonPress2_}></Button>
</View> </View>
); );

View File

@ -81,7 +81,7 @@ class SelectDateTimeDialog extends React.PureComponent {
width={0.9} width={0.9}
height={350} height={350}
> >
<View style={{flex: 1, margin: 20, alignItems: 'center'}}> <View style={{ flex: 1, margin: 20, alignItems: 'center' }}>
<DatePicker <DatePicker
date={this.state.date} date={this.state.date}
mode="datetime" mode="datetime"
@ -90,7 +90,7 @@ class SelectDateTimeDialog extends React.PureComponent {
confirmBtnText={_('Confirm')} confirmBtnText={_('Confirm')}
cancelBtnText={_('Cancel')} cancelBtnText={_('Cancel')}
onDateChange={(date) => { this.setState({ date: this.stringToDate(date) }); }} onDateChange={(date) => { this.setState({ date: this.stringToDate(date) }); }}
style={{width: 300}} style={{ width: 300 }}
customStyles={{ customStyles={{
btnConfirm: { btnConfirm: {
paddingVertical: 0, paddingVertical: 0,

View File

@ -11,7 +11,7 @@ function addResourceTag(lines, resource, attributes) {
const src = `:/${resource.id}`; const src = `:/${resource.id}`;
if (resourceUtils.isImageMimeType(resource.mime)) { if (resourceUtils.isImageMimeType(resource.mime)) {
lines.push(resourceUtils.imgElement({src, attributes})); lines.push(resourceUtils.imgElement({ src, attributes }));
} else if (resource.mime === 'audio/x-m4a') { } else if (resource.mime === 'audio/x-m4a') {
/** /**
* TODO: once https://github.com/laurent22/joplin/issues/1794 is resolved, * TODO: once https://github.com/laurent22/joplin/issues/1794 is resolved,
@ -162,9 +162,9 @@ async function enexXmlToHtml(xmlString, resources, options = {}) {
const beautifyHtml = (html) => { const beautifyHtml = (html) => {
return new Promise((resolve) => { return new Promise((resolve) => {
const options = {wrap: 0}; const options = { wrap: 0 };
cleanHtml.clean(html, options, (...cleanedHtml) => resolve(cleanedHtml)); cleanHtml.clean(html, options, (...cleanedHtml) => resolve(cleanedHtml));
}); });
}; };
module.exports = {enexXmlToHtml}; module.exports = { enexXmlToHtml };

View File

@ -28,6 +28,7 @@ const plugins = {
insert: { module: require('markdown-it-ins') }, insert: { module: require('markdown-it-ins') },
multitable: { module: require('markdown-it-multimd-table'), options: { multiline: true, rowspan: true, headerless: true } }, multitable: { module: require('markdown-it-multimd-table'), options: { multiline: true, rowspan: true, headerless: true } },
toc: { module: require('markdown-it-toc-done-right'), options: { listType: 'ul', slugify: uslugify } }, toc: { module: require('markdown-it-toc-done-right'), options: { listType: 'ul', slugify: uslugify } },
expand_tabs: { module: require('markdown-it-expand-tabs'), options: { tabWidth: 4 } },
}; };
const defaultNoteStyle = require('./defaultNoteStyle'); const defaultNoteStyle = require('./defaultNoteStyle');

View File

@ -1,5 +1,5 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const {dirname } = require('../pathUtils'); const { dirname } = require('../pathUtils');
const rootDir = dirname(__dirname); const rootDir = dirname(__dirname);
const assetsDir = `${rootDir}/assets`; const assetsDir = `${rootDir}/assets`;

View File

@ -14,6 +14,7 @@ module.exports = function(style, options) {
body { body {
font-size: ${style.htmlFontSize}; font-size: ${style.htmlFontSize};
color: ${style.htmlColor}; color: ${style.htmlColor};
word-wrap: break-word;
line-height: ${style.htmlLineHeight}; line-height: ${style.htmlLineHeight};
background-color: ${style.htmlBackgroundColor}; background-color: ${style.htmlBackgroundColor};
font-family: ${fontFamily}; font-family: ${fontFamily};

View File

@ -26,6 +26,7 @@
"markdown-it-anchor": "^5.2.5", "markdown-it-anchor": "^5.2.5",
"markdown-it-deflist": "^2.0.3", "markdown-it-deflist": "^2.0.3",
"markdown-it-emoji": "^1.4.0", "markdown-it-emoji": "^1.4.0",
"markdown-it-expand-tabs": "^1.0.13",
"markdown-it-footnote": "^3.0.2", "markdown-it-footnote": "^3.0.2",
"markdown-it-ins": "^3.0.0", "markdown-it-ins": "^3.0.0",
"markdown-it-mark": "^3.0.0", "markdown-it-mark": "^3.0.0",

View File

@ -511,7 +511,11 @@ class Note extends BaseItem {
if (!originalNote) throw new Error(`Unknown note: ${noteId}`); if (!originalNote) throw new Error(`Unknown note: ${noteId}`);
let newNote = Object.assign({}, originalNote); let newNote = Object.assign({}, originalNote);
delete newNote.id; const fieldsToReset = ['id', 'created_time', 'updated_time', 'user_created_time', 'user_updated_time'];
for (let field of fieldsToReset) {
delete newNote[field];
}
for (let n in changes) { for (let n in changes) {
if (!changes.hasOwnProperty(n)) continue; if (!changes.hasOwnProperty(n)) continue;

View File

@ -345,8 +345,8 @@ class Setting extends BaseModel {
}, },
// Deprecated - use markdown.plugin.* // Deprecated - use markdown.plugin.*
'markdown.softbreaks': { value: false, type: Setting.TYPE_BOOL, public: false, appTypes: ['mobile', 'desktop']}, 'markdown.softbreaks': { value: false, type: Setting.TYPE_BOOL, public: false, appTypes: ['mobile', 'desktop'] },
'markdown.typographer': { value: false, type: Setting.TYPE_BOOL, public: false, appTypes: ['mobile', 'desktop']}, 'markdown.typographer': { value: false, type: Setting.TYPE_BOOL, public: false, appTypes: ['mobile', 'desktop'] },
// Deprecated // Deprecated
'markdown.plugin.softbreaks': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable soft breaks') }, 'markdown.plugin.softbreaks': { value: false, type: Setting.TYPE_BOOL, section: 'plugins', public: true, appTypes: ['mobile', 'desktop'], label: () => _('Enable soft breaks') },
@ -502,13 +502,13 @@ class Setting extends BaseModel {
'Tabloid': _('Tabloid'), 'Tabloid': _('Tabloid'),
'Legal': _('Legal'), 'Legal': _('Legal'),
}; };
}}, } },
'export.pdfPageOrientation': { value: 'portrait', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('Page orientation for PDF export'), options: () => { 'export.pdfPageOrientation': { value: 'portrait', type: Setting.TYPE_STRING, isEnum: true, public: true, appTypes: ['desktop'], label: () => _('Page orientation for PDF export'), options: () => {
return { return {
'portrait': _('Portrait'), 'portrait': _('Portrait'),
'landscape': _('Landscape'), 'landscape': _('Landscape'),
}; };
}}, } },
'net.customCertificates': { 'net.customCertificates': {

View File

@ -112,6 +112,21 @@ class Tag extends BaseItem {
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${tagIds.join('","')}")`); return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${tagIds.join('","')}")`);
} }
static async commonTagsByNoteIds(noteIds) {
if (!noteIds || noteIds.length === 0) {
return [];
}
let commonTagIds = await NoteTag.tagIdsByNoteId(noteIds[0]);
for (let i = 1; i < noteIds.length; i++) {
const tagIds = await NoteTag.tagIdsByNoteId(noteIds[i]);
commonTagIds = commonTagIds.filter(value => tagIds.includes(value));
if (commonTagIds.length === 0) {
break;
}
}
return this.modelSelectAll(`SELECT * FROM tags WHERE id IN ("${commonTagIds.join('","')}")`);
}
static async loadByTitle(title) { static async loadByTitle(title) {
return this.loadByField('title', title, { caseInsensitive: true }); return this.loadByField('title', title, { caseInsensitive: true });
} }

View File

@ -394,6 +394,11 @@ const reducer = (state = defaultState, action) => {
} }
break; break;
case 'NOTE_SELECT_ALL':
newState = Object.assign({}, state);
newState.selectedNoteIds = newState.notes.map(n => n.id);
break;
case 'FOLDER_SELECT': case 'FOLDER_SELECT':
newState = changeSelectedFolder(state, action, { clearSelectedNoteIds: true }); newState = changeSelectedFolder(state, action, { clearSelectedNoteIds: true });
break; break;

View File

@ -45,17 +45,17 @@ const attributesToStr = (attributes) =>
.map(([key, value]) => ` ${key}="${escapeQuotes(value)}"`) .map(([key, value]) => ` ${key}="${escapeQuotes(value)}"`)
.join(''); .join('');
const attachmentElement = ({src, attributes, id}) => const attachmentElement = ({ src, attributes, id }) =>
[ [
`<a href='joplin://${id}' ${attributesToStr(attributes)}>`, `<a href='joplin://${id}' ${attributesToStr(attributes)}>`,
` ${attributes.alt || src}`, ` ${attributes.alt || src}`,
'</a>', '</a>',
].join(''); ].join('');
const imgElement = ({src, attributes}) => const imgElement = ({ src, attributes }) =>
`<img src="${src}" ${attributesToStr(attributes)} />`; `<img src="${src}" ${attributesToStr(attributes)} />`;
const audioElement = ({src, alt, id}) => const audioElement = ({ src, alt, id }) =>
[ [
'<audio controls preload="none" style="width:480px;">', '<audio controls preload="none" style="width:480px;">',
` <source src="${src}" type="audio/mp4" />`, ` <source src="${src}" type="audio/mp4" />`,

View File

@ -196,7 +196,7 @@ class InteropService {
const ModuleClass = require(modulePath); const ModuleClass = require(modulePath);
const output = new ModuleClass(); const output = new ModuleClass();
const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target); const moduleMetadata = this.findModuleByFormat_(type, options.format, options.target);
output.setMetadata({options, ...moduleMetadata}); // TODO: Check that this metadata is equivalent to module above output.setMetadata({ options, ...moduleMetadata }); // TODO: Check that this metadata is equivalent to module above
return output; return output;
} }

View File

@ -13,7 +13,7 @@ class InteropService_Importer_EnexToHtml extends InteropService_Importer_Base {
folder = await Folder.save({ title: folderTitle }); folder = await Folder.save({ title: folderTitle });
} }
await importEnex(folder.id, this.sourcePath_, {...this.options_, outputFormat: 'html'}); await importEnex(folder.id, this.sourcePath_, { ...this.options_, outputFormat: 'html' });
return result; return result;
} }

View File

@ -4,8 +4,8 @@ const Note = require('lib/models/Note.js');
const { basename, filename, rtrimSlashes, fileExtension, dirname } = require('lib/path-utils.js'); const { basename, filename, rtrimSlashes, fileExtension, dirname } = require('lib/path-utils.js');
const { shim } = require('lib/shim'); const { shim } = require('lib/shim');
const { _ } = require('lib/locale'); const { _ } = require('lib/locale');
const {extractImageUrls} = require('lib/markdownUtils'); const { extractImageUrls } = require('lib/markdownUtils');
const {unique} = require('lib/ArrayUtils'); const { unique } = require('lib/ArrayUtils');
const { pregQuote } = require('lib/string-utils-common'); const { pregQuote } = require('lib/string-utils-common');
class InteropService_Importer_Md extends InteropService_Importer_Base { class InteropService_Importer_Md extends InteropService_Importer_Base {

View File

@ -3,7 +3,7 @@ module.exports = {
{ {
"id": "8a1556e382704160808e9a7bef7135d3", "id": "8a1556e382704160808e9a7bef7135d3",
"title": "1. Welcome to Joplin! 🗒️", "title": "1. Welcome to Joplin! 🗒️",
"body": "# Welcome to Joplin! 🗒️\n\nJoplin is a free, open source note taking and to-do application, which helps you write and organise your notes, and synchronise them between your devices. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](https://joplinapp.org/#markdown). Joplin is available as a **💻 desktop**, **📱 mobile** and **🔡 terminal** application.\n\nThe notes in this notebook give an overview of what Joplin can do and how to use it. In general, the three applications share roughly the same functionalities; any differences will be clearly indicated.\n\n![](./AllClients.png)\n\n## Joplin is divided into three parts\n\nJoplin has three main columns:\n\n- **Sidebar** contains the list of your notebooks and tags, as well as the synchronisation status.\n- **Note List** contains the current list of notes - either the notes in the currently selected notebook, the notes in the currently selected tag, or search results.\n- **Note Editor** is the place where you write your notes in Markdown, with a viewer showing what the note will look like. You may also use an [external editor](https://joplinapp.org/#external-text-editor) to edit notes. For example, if you like WYSIWYG editors, you can use something like Typora as an external editor and it will display the note as well as any embedded images.\n\n## Writing notes in Markdown\n\nMarkdown is a lightweight markup language with plain text formatting syntax. Joplin supports a [Github-flavoured Markdown syntax](https://joplinapp.org/markdown/) with a few variations and additions.\n\nIn general, while Markdown is a markup language, it is meant to be human readable, even without being rendered. This is a simple example (you can see how it looks in the viewer panel):\n\n* * *\n\n# Heading\n\n## Sub-heading\n\nParagraphs are separated by a blank line. Text attributes _italic_, **bold** and `monospace` are supported. You can create bullet lists:\n\n* apples\n* oranges\n* pears\n\nOr numbered lists:\n\n1. wash\n2. rinse\n3. repeat\n\nThis is a [link](https://joplinapp.org) and, finally, below is a horizontal rule:\n\n* * *\n\nA lot more is possible including adding code samples, math formulae or checkbox lists - see the [Markdown documentation](https://joplinapp.org/#markdown) for more information.\n\n## Organising your notes\n\n### With notebooks 📔\n\nJoplin notes are organised into a tree of notebooks and sub-notebooks.\n\n- On **desktop**, you can create a notebook by clicking on New Notebook, then you can drag and drop them into other notebooks to organise them as you wish.\n- On **mobile**, press the \"+\" icon and select \"New notebook\".\n- On **terminal**, press `:mn`\n\n![](./SubNotebooks.png)\n\n### With tags 🏷️\n\nThe second way to organise your notes is using tags:\n\n- On **desktop**, right-click on any note in the Note List, and select \"Edit tags\". You can then add the tags, separating them by commas.\n- On **mobile**, open the note and press the \"⋮\" button and select \"Tags\".\n- On **terminal**, type `:help tag` for the available commands.\n", "body": "Joplin is a free, open source note taking and to-do application, which helps you write and organise your notes, and synchronise them between your devices. The notes are searchable, can be copied, tagged and modified either from the applications directly or from your own text editor. The notes are in [Markdown format](https://joplinapp.org/#markdown). Joplin is available as a **💻 desktop**, **📱 mobile** and **🔡 terminal** application.\n\nThe notes in this notebook give an overview of what Joplin can do and how to use it. In general, the three applications share roughly the same functionalities; any differences will be clearly indicated.\n\n![](./AllClients.png)\n\n## Joplin is divided into three parts\n\nJoplin has three main columns:\n\n- **Sidebar** contains the list of your notebooks and tags, as well as the synchronisation status.\n- **Note List** contains the current list of notes - either the notes in the currently selected notebook, the notes in the currently selected tag, or search results.\n- **Note Editor** is the place where you write your notes in Markdown, with a viewer showing what the note will look like. You may also use an [external editor](https://joplinapp.org/#external-text-editor) to edit notes. For example, if you like WYSIWYG editors, you can use something like Typora as an external editor and it will display the note as well as any embedded images.\n\n## Writing notes in Markdown\n\nMarkdown is a lightweight markup language with plain text formatting syntax. Joplin supports a [Github-flavoured Markdown syntax](https://joplinapp.org/markdown/) with a few variations and additions.\n\nIn general, while Markdown is a markup language, it is meant to be human readable, even without being rendered. This is a simple example (you can see how it looks in the viewer panel):\n\n* * *\n\n# Heading\n\n## Sub-heading\n\nParagraphs are separated by a blank line. Text attributes _italic_, **bold** and `monospace` are supported. You can create bullet lists:\n\n* apples\n* oranges\n* pears\n\nOr numbered lists:\n\n1. wash\n2. rinse\n3. repeat\n\nThis is a [link](https://joplinapp.org) and, finally, below is a horizontal rule:\n\n* * *\n\nA lot more is possible including adding code samples, math formulae or checkbox lists - see the [Markdown documentation](https://joplinapp.org/#markdown) for more information.\n\n## Organising your notes\n\n### With notebooks 📔\n\nJoplin notes are organised into a tree of notebooks and sub-notebooks.\n\n- On **desktop**, you can create a notebook by clicking on New Notebook, then you can drag and drop them into other notebooks to organise them as you wish.\n- On **mobile**, press the \"+\" icon and select \"New notebook\".\n- On **terminal**, press `:mn`\n\n![](./SubNotebooks.png)\n\n### With tags 🏷️\n\nThe second way to organise your notes is using tags:\n\n- On **desktop**, right-click on any note in the Note List, and select \"Edit tags\". You can then add the tags, separating them by commas.\n- On **mobile**, open the note and press the \"⋮\" button and select \"Tags\".\n- On **terminal**, type `:help tag` for the available commands.\n",
"tags": [], "tags": [],
"resources": { "resources": {
"./AllClients.png": { "./AllClients.png": {
@ -20,7 +20,7 @@ module.exports = {
{ {
"id": "b863cbc514cb4cafbae8dd6a4fcad919", "id": "b863cbc514cb4cafbae8dd6a4fcad919",
"title": "2. Importing and exporting notes ↔️", "title": "2. Importing and exporting notes ↔️",
"body": "# Importing and exporting notes ↔️\n\n## Importing from Evernote\n\nJoplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, images, attached files and note metadata (such as author, geo-location, etc.) via ENEX files.\n\nTo import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then, on **desktop**, do the following: Open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc. Read [more about Evernote import](https://joplinapp.org/#importing-from-evernote).\n\n# Importing from other apps\n\nJoplin can also import notes from [many other apps](https://github.com/laurent22/joplin#importing-from-other-applications) as well as [from Markdown or text files](https://github.com/laurent22/joplin#importing-from-markdown-files).\n\n# Exporting notes\n\nJoplin can export to the JEX format (Joplin Export file), which is an archive that can contain multiple notes, notebooks, etc. This is a format mostly designed for backup purposes. You may also export to other formats such as plain Markdown files, to JSON or to PDF. Find out [more about exporting notes](https://github.com/laurent22/joplin#exporting) on the official website.", "body": "## Importing from Evernote\n\nJoplin was designed as a replacement for Evernote and so can import complete Evernote notebooks, as well as notes, tags, images, attached files and note metadata (such as author, geo-location, etc.) via ENEX files.\n\nTo import Evernote data, first export your Evernote notebooks to ENEX files as described [here](https://help.evernote.com/hc/en-us/articles/209005557-How-to-back-up-export-and-restore-import-notes-and-notebooks). Then, on **desktop**, do the following: Open File > Import > ENEX and select your file. The notes will be imported into a new separate notebook. If needed they can then be moved to a different notebook, or the notebook can be renamed, etc. Read [more about Evernote import](https://joplinapp.org/#importing-from-evernote).\n\n# Importing from other apps\n\nJoplin can also import notes from [many other apps](https://github.com/laurent22/joplin#importing-from-other-applications) as well as [from Markdown or text files](https://github.com/laurent22/joplin#importing-from-markdown-files).\n\n# Exporting notes\n\nJoplin can export to the JEX format (Joplin Export file), which is an archive that can contain multiple notes, notebooks, etc. This is a format mostly designed for backup purposes. You may also export to other formats such as plain Markdown files, to JSON or to PDF. Find out [more about exporting notes](https://github.com/laurent22/joplin#exporting) on the official website.",
"tags": [], "tags": [],
"resources": {}, "resources": {},
"parent_id": "9bb5d498aba74cc6a047cfdc841e82a1" "parent_id": "9bb5d498aba74cc6a047cfdc841e82a1"
@ -28,7 +28,7 @@ module.exports = {
{ {
"id": "25b656aac0564d1a91ab98295aa3cc58", "id": "25b656aac0564d1a91ab98295aa3cc58",
"title": "3. Synchronising your notes 🔄", "title": "3. Synchronising your notes 🔄",
"body": "# Synchronising your notes 🔄\n\nOne of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. You basically choose the service you prefer among those supported, setup the configuration, and the app will be able to sync between your computers or mobile devices.\n\nThe supported cloud services are the following:\n\n## Setting up Dropbox synchronisation\n\nSelect \"Dropbox\" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the \"Synchronise\" button in the sidebar and follow the instructions.\n\n## Setting up Nextcloud synchronisation\n\nNextcloud is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin.\n\nTo set it up, go to the config screen and select Nextcloud as the synchronisation target. Then input the WebDAV URL (to get it, go to your Nextcloud page, click on Settings in the bottom left corner of the page and copy the URL). Note that it has to be the **full URL**, so for example if you want the notes to be under `/Joplin`, the URL would be something like `https://example.com/remote.php/webdav/Joplin` (note that \"/Joplin\" part). And **make sure to create the \"/Joplin\" directory in Nextcloud**. Finally set the username and password. If it does not work, please [see this explanation](https://github.com/laurent22/joplin/issues/61#issuecomment-373282608) for more details.\n\n## Setting up OneDrive or WebDAV synchronisation\n\nOneDrive and WebDAV are also supported as synchronisation services. Please see [the export documentation](https://github.com/laurent22/joplin#exporting) for more information.\n\n## Using End-To-End Encryption\n\nJoplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the data can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the [End-To-End Encryption Tutorial](https://joplinapp.org/e2ee/) for more information about this feature and how to enable it.", "body": "One of the goals of Joplin was to avoid being tied to any particular company or service, whether it is Evernote, Google or Microsoft. As such the synchronisation is designed without any hard dependency to any particular service. You basically choose the service you prefer among those supported, setup the configuration, and the app will be able to sync between your computers or mobile devices.\n\nThe supported cloud services are the following:\n\n## Setting up Dropbox synchronisation\n\nSelect \"Dropbox\" as the synchronisation target in the config screen (it is selected by default). Then, to initiate the synchronisation process, click on the \"Synchronise\" button in the sidebar and follow the instructions.\n\n## Setting up Nextcloud synchronisation\n\nNextcloud is a self-hosted, private cloud solution. It can store documents, images and videos but also calendars, passwords and countless other things and can sync them to your laptop or phone. As you can host your own Nextcloud server, you own both the data on your device and infrastructure used for synchronisation. As such it is a good fit for Joplin.\n\nTo set it up, go to the config screen and select Nextcloud as the synchronisation target. Then input the WebDAV URL (to get it, go to your Nextcloud page, click on Settings in the bottom left corner of the page and copy the URL). Note that it has to be the **full URL**, so for example if you want the notes to be under `/Joplin`, the URL would be something like `https://example.com/remote.php/webdav/Joplin` (note that \"/Joplin\" part). And **make sure to create the \"/Joplin\" directory in Nextcloud**. Finally set the username and password. If it does not work, please [see this explanation](https://github.com/laurent22/joplin/issues/61#issuecomment-373282608) for more details.\n\n## Setting up OneDrive or WebDAV synchronisation\n\nOneDrive and WebDAV are also supported as synchronisation services. Please see [the export documentation](https://github.com/laurent22/joplin#exporting) for more information.\n\n## Using End-To-End Encryption\n\nJoplin supports end-to-end encryption (E2EE) on all the applications. E2EE is a system where only the owner of the data can read it. It prevents potential eavesdroppers - including telecom providers, internet providers, and even the developers of Joplin from being able to access the data. Please see the [End-To-End Encryption Tutorial](https://joplinapp.org/e2ee/) for more information about this feature and how to enable it.",
"tags": [], "tags": [],
"resources": {}, "resources": {},
"parent_id": "9bb5d498aba74cc6a047cfdc841e82a1" "parent_id": "9bb5d498aba74cc6a047cfdc841e82a1"
@ -36,7 +36,7 @@ module.exports = {
{ {
"id": "2ee48f80889447429a3cccb04a466072", "id": "2ee48f80889447429a3cccb04a466072",
"title": "4. Tips 💡", "title": "4. Tips 💡",
"body": "# Tips 💡\n\nThe first few notes should have given you an overview of the main functionalities of Joplin, but there's more it can do. See below for some of these features and how to get more help using the app:\n\n## Web clipper\n\n![](./WebClipper.png)\n\nThe Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. To start using it, open the Joplin desktop application, go to the Web Clipper Options and follow the instructions.\n\nMore info on the official website: https://joplinapp.org/clipper/\n\n## Attachments\n\nAny kind of file can be attached to a note. In Markdown, links to these files are represented as an ID. 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.\n\nImages can be attached either by clicking on \"Attach file\" or by pasting (with `Ctrl+V` or `Cmd+V`) an image directly in the editor, or by drag and dropping an image.\n\nMore info about attachments: https://joplinapp.org#attachments--resources\n\n## Search\n\nJoplin supports advanced search queries, which are fully documented on the official website: https://joplinapp.org#searching\n\n## Alarms\n\nAn alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. To use this feature, see the documentation: https://joplinapp.org#notifications\n\n## Markdown advanced tips\n\nJoplin uses and renders [Github-flavoured Markdown](https://joplinapp.org/markdown/) with a few variations and additions.\n\nFor example, tables are supported:\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | $1600 |\n| col 2 is | centered | $12 |\n| zebra stripes | are neat | $1 |\n\nYou can also create lists of checkboxes. These checkboxes can be ticked directly in the viewer, or by adding an \"x\" inside:\n\n- [ ] Milk\n- [ ] Eggs\n- [x] Beer\n\nMath expressions can be added using the [KaTeX notation](https://khan.github.io/KaTeX/):\n\n$$\nf(x) = \\int_{-\\infty}^\\infty\n \\hat f(\\xi)\\,e^{2 \\pi i \\xi x}\n \\,d\\xi\n$$\n\nVarious other tricks are possible, such as using HTML, or customising the CSS. See the Markdown documentation for more info - https://joplinapp.org#markdown\n\n## Community and further help\n\n- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplinapp.org/). It is possible to login with your GitHub account.\n- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).\n- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).\n\n## Donations\n\nDonations 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.\n\nPlease see the [donation page](https://joplinapp.org/donate/) for information on how to support the development of Joplin.", "body": "The first few notes should have given you an overview of the main functionalities of Joplin, but there's more it can do. See below for some of these features and how to get more help using the app:\n\n## Web clipper\n\n![](./WebClipper.png)\n\nThe Web Clipper is a browser extension that allows you to save web pages and screenshots from your browser. To start using it, open the Joplin desktop application, go to the Web Clipper Options and follow the instructions.\n\nMore info on the official website: https://joplinapp.org/clipper/\n\n## Attachments\n\nAny kind of file can be attached to a note. In Markdown, links to these files are represented as an ID. 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.\n\nImages can be attached either by clicking on \"Attach file\" or by pasting (with `Ctrl+V` or `Cmd+V`) an image directly in the editor, or by drag and dropping an image.\n\nMore info about attachments: https://joplinapp.org#attachments--resources\n\n## Search\n\nJoplin supports advanced search queries, which are fully documented on the official website: https://joplinapp.org#searching\n\n## Alarms\n\nAn alarm can be associated with any to-do. It will be triggered at the given time by displaying a notification. To use this feature, see the documentation: https://joplinapp.org#notifications\n\n## Markdown advanced tips\n\nJoplin uses and renders [Github-flavoured Markdown](https://joplinapp.org/markdown/) with a few variations and additions.\n\nFor example, tables are supported:\n\n| Tables | Are | Cool |\n| ------------- |:-------------:| -----:|\n| col 3 is | right-aligned | $1600 |\n| col 2 is | centered | $12 |\n| zebra stripes | are neat | $1 |\n\nYou can also create lists of checkboxes. These checkboxes can be ticked directly in the viewer, or by adding an \"x\" inside:\n\n- [ ] Milk\n- [ ] Eggs\n- [x] Beer\n\nMath expressions can be added using the [KaTeX notation](https://khan.github.io/KaTeX/):\n\n$$\nf(x) = \\int_{-\\infty}^\\infty\n \\hat f(\\xi)\\,e^{2 \\pi i \\xi x}\n \\,d\\xi\n$$\n\nVarious other tricks are possible, such as using HTML, or customising the CSS. See the Markdown documentation for more info - https://joplinapp.org#markdown\n\n## Community and further help\n\n- For general discussion about Joplin, user support, software development questions, and to discuss new features, go to the [Joplin Forum](https://discourse.joplinapp.org/). It is possible to login with your GitHub account.\n- The latest news are posted [on the Patreon page](https://www.patreon.com/joplin).\n- For bug reports and feature requests, go to the [GitHub Issue Tracker](https://github.com/laurent22/joplin/issues).\n\n## Donations\n\nDonations 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.\n\nPlease see the [donation page](https://joplinapp.org/donate/) for information on how to support the development of Joplin.",
"tags": [], "tags": [],
"resources": { "resources": {
"./WebClipper.png": { "./WebClipper.png": {

View File

@ -8,7 +8,7 @@
// console.disableYellowBox = true // console.disableYellowBox = true
import {YellowBox} from 'react-native'; import { YellowBox } from 'react-native';
YellowBox.ignoreWarnings([ YellowBox.ignoreWarnings([
'Require cycle: node_modules/react-native-', 'Require cycle: node_modules/react-native-',
'Require cycle: node_modules/rn-fetch-blob', 'Require cycle: node_modules/rn-fetch-blob',

View File

@ -717,10 +717,10 @@ class AppComponent extends React.Component {
let menuPosition = 'left'; let menuPosition = 'left';
if (this.props.routeName === 'Note') { if (this.props.routeName === 'Note') {
sideMenuContent = <SafeAreaView style={{flex: 1, backgroundColor: theme.backgroundColor}}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>; sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContentNote options={this.props.noteSideMenuOptions}/></SafeAreaView>;
menuPosition = 'right'; menuPosition = 'right';
} else { } else {
sideMenuContent = <SafeAreaView style={{flex: 1, backgroundColor: theme.backgroundColor}}><SideMenuContent/></SafeAreaView>; sideMenuContent = <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}><SideMenuContent/></SafeAreaView>;
} }
const appNavInit = { const appNavInit = {
@ -750,12 +750,12 @@ class AppComponent extends React.Component {
}} }}
> >
<MenuContext style={{ flex: 1 }}> <MenuContext style={{ flex: 1 }}>
<SafeAreaView style={{flex: 0, backgroundColor: theme.raisedBackgroundColor}} /> <SafeAreaView style={{ flex: 0, backgroundColor: theme.raisedBackgroundColor }} />
<SafeAreaView style={{flex: 1, backgroundColor: theme.backgroundColor}}> <SafeAreaView style={{ flex: 1, backgroundColor: theme.backgroundColor }}>
<AppNav screens={appNavInit} /> <AppNav screens={appNavInit} />
</SafeAreaView> </SafeAreaView>
<DropdownAlert ref={ref => this.dropdownAlert_ = ref} tapToCloseEnabled={true} /> <DropdownAlert ref={ref => this.dropdownAlert_ = ref} tapToCloseEnabled={true} />
<Animated.View pointerEvents='none' style={{position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '100%'}}/> <Animated.View pointerEvents='none' style={{ position: 'absolute', backgroundColor: 'black', opacity: this.state.sideMenuContentOpacity, width: '100%', height: '100%' }}/>
</MenuContext> </MenuContext>
</SideMenu> </SideMenu>
); );

View File

@ -566,25 +566,25 @@ async function main() {
renderMdToHtml(makeHomePageMd(), `${rootDir}/docs/index.html`, { sourceMarkdownFile: 'README.md' }); renderMdToHtml(makeHomePageMd(), `${rootDir}/docs/index.html`, { sourceMarkdownFile: 'README.md' });
const sources = [ const sources = [
[ 'readme/changelog.md', 'docs/changelog/index.html', { title: 'Changelog (Desktop App)' } ], ['readme/changelog.md', 'docs/changelog/index.html', { title: 'Changelog (Desktop App)' }],
[ 'readme/changelog_cli.md', 'docs/changelog_cli/index.html', { title: 'Changelog (CLI App)' } ], ['readme/changelog_cli.md', 'docs/changelog_cli/index.html', { title: 'Changelog (CLI App)' }],
[ 'readme/clipper.md', 'docs/clipper/index.html', { title: 'Web Clipper' } ], ['readme/clipper.md', 'docs/clipper/index.html', { title: 'Web Clipper' }],
[ 'readme/debugging.md', 'docs/debugging/index.html', { title: 'Debugging' } ], ['readme/debugging.md', 'docs/debugging/index.html', { title: 'Debugging' }],
[ 'readme/desktop.md', 'docs/desktop/index.html', { title: 'Desktop Application' } ], ['readme/desktop.md', 'docs/desktop/index.html', { title: 'Desktop Application' }],
[ 'readme/donate.md', 'docs/donate/index.html', { title: 'Donate' } ], ['readme/donate.md', 'docs/donate/index.html', { title: 'Donate' }],
[ 'readme/e2ee.md', 'docs/e2ee/index.html', { title: 'End-To-End Encryption' } ], ['readme/e2ee.md', 'docs/e2ee/index.html', { title: 'End-To-End Encryption' }],
[ 'readme/faq.md', 'docs/faq/index.html', { title: 'FAQ' } ], ['readme/faq.md', 'docs/faq/index.html', { title: 'FAQ' }],
[ 'readme/mobile.md', 'docs/mobile/index.html', { title: 'Mobile Application' } ], ['readme/mobile.md', 'docs/mobile/index.html', { title: 'Mobile Application' }],
[ 'readme/spec.md', 'docs/spec/index.html', { title: 'Specifications' } ], ['readme/spec.md', 'docs/spec/index.html', { title: 'Specifications' }],
[ 'readme/stats.md', 'docs/stats/index.html', { title: 'Statistics' } ], ['readme/stats.md', 'docs/stats/index.html', { title: 'Statistics' }],
[ 'readme/terminal.md', 'docs/terminal/index.html', { title: 'Terminal Application' } ], ['readme/terminal.md', 'docs/terminal/index.html', { title: 'Terminal Application' }],
[ 'readme/api.md', 'docs/api/index.html', { title: 'REST API' } ], ['readme/api.md', 'docs/api/index.html', { title: 'REST API' }],
[ 'readme/prereleases.md', 'docs/prereleases/index.html', { title: 'Pre-releases' } ], ['readme/prereleases.md', 'docs/prereleases/index.html', { title: 'Pre-releases' }],
[ 'readme/markdown.md', 'docs/markdown/index.html', { title: 'Markdown Guide' } ], ['readme/markdown.md', 'docs/markdown/index.html', { title: 'Markdown Guide' }],
[ 'readme/nextcloud_app.md', 'docs/nextcloud_app/index.html', { title: 'Joplin Web API for Nextcloud' } ], ['readme/nextcloud_app.md', 'docs/nextcloud_app/index.html', { title: 'Joplin Web API for Nextcloud' }],
[ 'readme/gsoc2020/index.md', 'docs/gsoc2020/index.html', { title: 'Google Summer of Code' } ], ['readme/gsoc2020/index.md', 'docs/gsoc2020/index.html', { title: 'Google Summer of Code' }],
[ 'readme/gsoc2020/ideas.md', 'docs/gsoc2020/ideas.html', { title: 'GSoC: Project Ideas' } ], ['readme/gsoc2020/ideas.md', 'docs/gsoc2020/ideas.html', { title: 'GSoC: Project Ideas' }],
]; ];
const path = require('path'); const path = require('path');

View File

@ -25,7 +25,7 @@ toolUtils.execCommandWithPipes = function(executable, args) {
var spawn = require('child_process').spawn; var spawn = require('child_process').spawn;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const child = spawn(executable, args, { stdio: 'inherit'}); const child = spawn(executable, args, { stdio: 'inherit' });
child.on('error', (error) => { child.on('error', (error) => {
reject(error); reject(error);

View File

@ -12,7 +12,7 @@ async function gitHubContributors(page) {
request.get({ request.get({
url: `https://api.github.com/repos/laurent22/joplin/contributors${page ? `?page=${page}` : ''}`, url: `https://api.github.com/repos/laurent22/joplin/contributors${page ? `?page=${page}` : ''}`,
json: true, json: true,
headers: {'User-Agent': 'Joplin Readme Updater'}, headers: { 'User-Agent': 'Joplin Readme Updater' },
}, (error, response, data) => { }, (error, response, data) => {
if (error) { if (error) {
reject(error); reject(error);

View File

@ -22,7 +22,7 @@ async function gitHubLatestRelease() {
request.get({ request.get({
url: url, url: url,
json: true, json: true,
headers: {'User-Agent': 'Joplin Readme Updater'}, headers: { 'User-Agent': 'Joplin Readme Updater' },
}, (error, response, data) => { }, (error, response, data) => {
if (error) { if (error) {
reject(error); reject(error);