You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-30 20:39:46 +02:00
Compare commits
30 Commits
v1.0.107
...
cli-v1.0.1
Author | SHA1 | Date | |
---|---|---|---|
|
ff2d793fbb | ||
|
c83391e624 | ||
|
a3a818ea74 | ||
|
54a4965503 | ||
|
2233d88c01 | ||
|
9680ab74a3 | ||
|
ef711af5b5 | ||
|
8a619e4b8b | ||
|
bc09d2c640 | ||
|
f82dfde6f4 | ||
|
312c7f2d27 | ||
|
953cc327c6 | ||
|
14cff96713 | ||
|
34b9af2ce0 | ||
|
6a6ee280c3 | ||
|
861387707a | ||
|
830e665366 | ||
|
f14ae68ea0 | ||
|
c7084bf27e | ||
|
fc8ffcbe46 | ||
|
77f089654e | ||
|
e7a12bb0dd | ||
|
22fe3a4e44 | ||
|
afb8b92528 | ||
|
5178f99100 | ||
|
72af564382 | ||
|
0a2b83998c | ||
|
73e79213dc | ||
|
e31ffc9474 | ||
|
fdb8706a5f |
1
CliClient/.gitignore
vendored
1
CliClient/.gitignore
vendored
@@ -13,6 +13,7 @@ tests/fuzzing.*
|
||||
tests/fuzzing -*
|
||||
tests/logs/*
|
||||
tests/cli-integration/
|
||||
tests/tmp/
|
||||
*.mo
|
||||
*.*~
|
||||
tests/sync
|
||||
|
@@ -32,8 +32,6 @@ class FolderListWidget extends ListWidget {
|
||||
output.push(_('Search:'));
|
||||
output.push(item.title);
|
||||
}
|
||||
|
||||
// if (item && item.id) output.push(item.id.substr(0, 5));
|
||||
|
||||
return output.join(' ');
|
||||
};
|
||||
@@ -85,7 +83,6 @@ class FolderListWidget extends ListWidget {
|
||||
}
|
||||
|
||||
set notesParentType(v) {
|
||||
//if (this.notesParentType_ === v) return;
|
||||
this.notesParentType_ = v;
|
||||
this.updateIndexFromSelectedItemId()
|
||||
this.invalidate();
|
||||
@@ -123,6 +120,14 @@ class FolderListWidget extends ListWidget {
|
||||
this.updateIndexFromSelectedItemId()
|
||||
this.invalidate();
|
||||
}
|
||||
|
||||
folderHasChildren_(folders, folderId) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
let folder = folders[i];
|
||||
if (folder.parent_id === folderId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.updateItems_) {
|
||||
@@ -130,7 +135,18 @@ class FolderListWidget extends ListWidget {
|
||||
const wasSelectedItemId = this.selectedJoplinItemId;
|
||||
const previousParentType = this.notesParentType;
|
||||
|
||||
let newItems = this.folders.slice();
|
||||
let newItems = [];
|
||||
const orderFolders = (parentId) => {
|
||||
for (let i = 0; i < this.folders.length; i++) {
|
||||
const f = this.folders[i];
|
||||
if (f.parent_id === parentId) {
|
||||
newItems.push(f);
|
||||
if (this.folderHasChildren_(this.folders, f.id)) orderFolders(f.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orderFolders('');
|
||||
|
||||
if (this.tags.length) {
|
||||
if (newItems.length) newItems.push('-');
|
||||
|
@@ -461,7 +461,6 @@ msgstr "Commencement de la synchronisation..."
|
||||
msgid "Cancelling... Please wait."
|
||||
msgstr "Annulation... Veuillez attendre."
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"<tag-command> can be \"add\", \"remove\" or \"list\" to assign or remove "
|
||||
"[tag] from [note], or to list the notes associated with [tag]. The command "
|
||||
@@ -470,7 +469,7 @@ msgstr ""
|
||||
"<tag-command> peut être \"add\", \"remove\" ou \"list\" pour assigner ou "
|
||||
"enlever l'étiquette [tag] de la [note], our pour lister les notes associées "
|
||||
"avec l'étiquette [tag]. La commande `tag list` peut être utilisée pour "
|
||||
"lister les étiquettes."
|
||||
"lister les étiquettes (utilisez l'option -l pour les options complètes)."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Invalid command: \"%s\""
|
||||
@@ -949,10 +948,10 @@ msgstr ""
|
||||
"carnet\"."
|
||||
|
||||
msgid "Location"
|
||||
msgstr ""
|
||||
msgstr "Lieu"
|
||||
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "URL"
|
||||
|
||||
msgid "Open..."
|
||||
msgstr "Ouvrir..."
|
||||
@@ -1008,7 +1007,7 @@ msgid "In: %s"
|
||||
msgstr "Dans : %s"
|
||||
|
||||
msgid "Note properties"
|
||||
msgstr ""
|
||||
msgstr "Propriétés de la note"
|
||||
|
||||
msgid "Hyperlink"
|
||||
msgstr "Lien"
|
||||
@@ -1294,7 +1293,7 @@ msgid "Note: Does not work in all desktop environments."
|
||||
msgstr "Note : Ne fonctionne pas dans tous les environnements de bureau."
|
||||
|
||||
msgid "Start application minimised in the tray icon"
|
||||
msgstr ""
|
||||
msgstr "Démarrer minimisé dans la zone de notification"
|
||||
|
||||
msgid "Global zoom percentage"
|
||||
msgstr "Niveau de zoom"
|
||||
@@ -1371,6 +1370,9 @@ msgid ""
|
||||
"to it before syncing, otherwise all files will be removed! See the FAQ for "
|
||||
"more details: %s"
|
||||
msgstr ""
|
||||
"Attention : si vous changez cet emplacement, copiez-y tout le contenu avant "
|
||||
"de synchroniser, sinon tous les fichiers seront supprimés ! Consulter la FAQ "
|
||||
"pour plus de détails : %s"
|
||||
|
||||
msgid "Nextcloud username"
|
||||
msgstr "Nextcloud : Nom utilisateur"
|
||||
@@ -1411,7 +1413,7 @@ msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
|
||||
|
||||
#, javascript-format
|
||||
msgid "The tag \"%s\" already exists. Please choose a different name."
|
||||
msgstr ""
|
||||
msgstr "L'étiquette \"%s\" existe déjà. Veuillez choisir un autre nom."
|
||||
|
||||
msgid "Joplin Export File"
|
||||
msgstr "Fichier d'export Joplin"
|
||||
|
File diff suppressed because it is too large
Load Diff
1198
CliClient/package-lock.json
generated
1198
CliClient/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "1.0.114",
|
||||
"version": "1.0.115",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -37,12 +37,14 @@
|
||||
"fs-extra": "^5.0.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"html-minifier": "^3.5.15",
|
||||
"image-data-uri": "^2.0.0",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-turndown": "^4.0.8",
|
||||
"joplin-turndown": "^4.0.9",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.7",
|
||||
"jssha": "^2.3.0",
|
||||
"levenshtein": "^1.0.5",
|
||||
"lodash": "^4.17.4",
|
||||
"markdown-it": "^8.4.2",
|
||||
"md5": "^2.2.1",
|
||||
"mime": "^2.0.3",
|
||||
"moment": "^2.18.1",
|
||||
@@ -56,7 +58,7 @@
|
||||
"redux": "^3.7.2",
|
||||
"sax": "^1.2.2",
|
||||
"server-destroy": "^1.0.1",
|
||||
"sharp": "^0.18.4",
|
||||
"sharp": "^0.20.8",
|
||||
"sprintf-js": "^1.1.1",
|
||||
"sqlite3": "^4.0.1",
|
||||
"string-padding": "^1.0.2",
|
||||
|
@@ -36,7 +36,7 @@ describe('HtmlToMd', function() {
|
||||
const htmlPath = basePath + '/' + htmlFilename;
|
||||
const mdPath = basePath + '/' + filename(htmlFilename) + '.md';
|
||||
|
||||
// if (htmlFilename !== 'code_1.html') continue;
|
||||
// if (htmlFilename !== 'anchor_with_url_with_spaces.html') continue;
|
||||
|
||||
const html = await shim.fsDriver().readFile(htmlPath);
|
||||
const expectedMd = await shim.fsDriver().readFile(mdPath);
|
||||
|
@@ -0,0 +1 @@
|
||||
<a href="http://example.com/That is not right"/>Testing</a>
|
@@ -0,0 +1 @@
|
||||
[Testing](http://example.com/That%20is%20not%20right)
|
29
CliClient/tests/html_to_md/picture_with_no_img.html
Normal file
29
CliClient/tests/html_to_md/picture_with_no_img.html
Normal file
@@ -0,0 +1,29 @@
|
||||
Some pictures:
|
||||
|
||||
<picture>
|
||||
<!--[if IE 9]><video style="display: none;"><![endif]-->
|
||||
<source media="(min-width: 768px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w">
|
||||
<source media="(min-width: 768px)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w">
|
||||
<source media="(min-width: 481px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w">
|
||||
<source media="(min-width: 481px)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w">
|
||||
<source media="(min-width: 321px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="450px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop&dpr=1.5 675w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop&dpr=1.5 675w">
|
||||
<source media="(min-width: 321px)" sizes="450px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop 450w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop 450w">
|
||||
<source media="(min-width: 0px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="320px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop&dpr=1.5 480w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop&dpr=1.5 480w">
|
||||
<source media="(min-width: 0px)" sizes="320px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop 320w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop 320w">
|
||||
<!--[if IE 9]></video><![endif]-->
|
||||
<img class=" lazyloaded" title="" alt="" id="img-id-0">
|
||||
</picture>
|
||||
|
||||
<picture>
|
||||
<!--[if IE 9]><video style="display: none;"><![endif]-->
|
||||
<source media="(min-width: 768px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w">
|
||||
<source media="(min-width: 768px)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w">
|
||||
<source media="(min-width: 481px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5 882w">
|
||||
<source media="(min-width: 481px)" sizes="588px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop 588w">
|
||||
<source media="(min-width: 321px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="450px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop&dpr=1.5 675w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop&dpr=1.5 675w">
|
||||
<source media="(min-width: 321px)" sizes="450px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop 450w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=450&h=688&fit=crop 450w">
|
||||
<source media="(min-width: 0px) and (-webkit-min-device-pixel-ratio: 1.25), (min-width: px) and (min-resolution: 120dpi)" sizes="320px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop&dpr=1.5 480w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop&dpr=1.5 480w">
|
||||
<source media="(min-width: 0px)" sizes="320px" data-srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop 320w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=320&h=489&fit=crop 320w">
|
||||
<!--[if IE 9]></video><![endif]-->
|
||||
<img class=" lazyloaded" title="" alt="" id="img-id-0" src="http://example.com/test.gif">
|
||||
</picture>
|
1
CliClient/tests/html_to_md/picture_with_no_img.md
Normal file
1
CliClient/tests/html_to_md/picture_with_no_img.md
Normal file
@@ -0,0 +1 @@
|
||||
Some pictures:  
|
@@ -39,6 +39,7 @@ describe('markdownUtils', function() {
|
||||
['', ['http://test.com/img.png']],
|
||||
[' ', ['http://test.com/img.png', 'http://test.com/img2.png']],
|
||||
['', ['http://test.com/img.png']],
|
||||
['.png)', ['https://test.com/ohoh_(123).png']],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
|
162
CliClient/tests/services_rest_Api.js
Normal file
162
CliClient/tests/services_rest_Api.js
Normal file
@@ -0,0 +1,162 @@
|
||||
require('app-module-path').addPath(__dirname);
|
||||
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
|
||||
const markdownUtils = require('lib/markdownUtils.js');
|
||||
const Api = require('lib/services/rest/Api');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Note = require('lib/models/Note');
|
||||
const Resource = require('lib/models/Resource');
|
||||
|
||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
|
||||
|
||||
process.on('unhandledRejection', (reason, p) => {
|
||||
console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
|
||||
});
|
||||
|
||||
let api = null;
|
||||
|
||||
describe('services_rest_Api', function() {
|
||||
|
||||
beforeEach(async (done) => {
|
||||
api = new Api();
|
||||
await setupDatabaseAndSynchronizer(1);
|
||||
await switchClient(1);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should ping', async (done) => {
|
||||
const response = await api.route('GET', 'ping');
|
||||
expect(response).toBe('JoplinClipperServer');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle Not Found errors', async (done) => {
|
||||
const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'pong'));
|
||||
expect(hasThrown).toBe(true);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should get folders', async (done) => {
|
||||
let f1 = await Folder.save({ title: "mon carnet" });
|
||||
const response = await api.route('GET', 'folders');
|
||||
expect(response.length).toBe(1);
|
||||
expect(response[0].title).toBe('mon carnet');
|
||||
done();
|
||||
});
|
||||
|
||||
it('should get notes', async (done) => {
|
||||
let response = null;
|
||||
const f1 = await Folder.save({ title: "mon carnet" });
|
||||
const f2 = await Folder.save({ title: "mon deuxième carnet" });
|
||||
const n1 = await Note.save({ title: 'un', parent_id: f1.id });
|
||||
const n2 = await Note.save({ title: 'deux', parent_id: f1.id });
|
||||
const n3 = await Note.save({ title: 'trois', parent_id: f2.id });
|
||||
|
||||
response = await api.route('GET', 'notes');
|
||||
expect(response.length).toBe(3);
|
||||
|
||||
response = await api.route('GET', 'notes', { parent_id: f1.id });
|
||||
expect(response.length).toBe(2);
|
||||
|
||||
response = await api.route('GET', 'notes', { parent_id: f2.id });
|
||||
expect(response.length).toBe(1);
|
||||
expect(response[0].id).toBe(n3.id);
|
||||
|
||||
response = await api.route('GET', 'notes/' + n1.id);
|
||||
expect(response.id).toBe(n1.id);
|
||||
|
||||
response = await api.route('GET', 'notes/' + n3.id, { fields: 'id,title' });
|
||||
expect(Object.getOwnPropertyNames(response).length).toBe(3);
|
||||
expect(response.id).toBe(n3.id);
|
||||
expect(response.title).toBe('trois');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create notes', async (done) => {
|
||||
let response = null;
|
||||
const f = await Folder.save({ title: "mon carnet" });
|
||||
|
||||
response = await api.route('POST', 'notes', null, JSON.stringify({
|
||||
title: 'testing',
|
||||
parent_id: f.id,
|
||||
}));
|
||||
expect(response.title).toBe('testing');
|
||||
expect(!!response.id).toBe(true);
|
||||
|
||||
response = await api.route('POST', 'notes', null, JSON.stringify({
|
||||
title: 'testing',
|
||||
parent_id: f.id,
|
||||
}));
|
||||
expect(response.title).toBe('testing');
|
||||
expect(!!response.id).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create notes with images', async (done) => {
|
||||
let response = null;
|
||||
const f = await Folder.save({ title: "mon carnet" });
|
||||
|
||||
response = await api.route('POST', 'notes', null, JSON.stringify({
|
||||
title: 'testing image',
|
||||
parent_id: f.id,
|
||||
image_data_url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAANZJREFUeNoAyAA3/wFwtO3K6gUB/vz2+Prw9fj/+/r+/wBZKAAExOgF4/MC9ff+MRH6Ui4E+/0Bqc/zutj6AgT+/Pz7+vv7++nu82c4DlMqCvLs8goA/gL8/fz09fb59vXa6vzZ6vjT5fbn6voD/fwC8vX4UiT9Zi//APHyAP8ACgUBAPv5APz7BPj2+DIaC2o3E+3o6ywaC5fT6gD6/QD9/QEVf9kD+/dcLQgJA/7v8vqfwOf18wA1IAIEVycAyt//v9XvAPv7APz8LhoIAPz9Ri4OAgwARgx4W/6fVeEAAAAASUVORK5CYII="
|
||||
}));
|
||||
|
||||
const resources = await Resource.all();
|
||||
expect(resources.length).toBe(1);
|
||||
|
||||
const resource = resources[0];
|
||||
expect(response.body.indexOf(resource.id) >= 0).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should create notes from HTML', async (done) => {
|
||||
let response = null;
|
||||
const f = await Folder.save({ title: "mon carnet" });
|
||||
|
||||
response = await api.route('POST', 'notes', null, JSON.stringify({
|
||||
title: 'testing HTML',
|
||||
parent_id: f.id,
|
||||
body_html: '<b>Bold text</b>',
|
||||
}));
|
||||
|
||||
expect(response.body).toBe('**Bold text**');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should filter fields', async (done) => {
|
||||
let f = api.fields_({ query: { fields: 'one,two' } }, []);
|
||||
expect(f.length).toBe(2);
|
||||
expect(f[0]).toBe('one');
|
||||
expect(f[1]).toBe('two');
|
||||
|
||||
f = api.fields_({ query: { fields: 'one ,, two ' } }, []);
|
||||
expect(f.length).toBe(2);
|
||||
expect(f[0]).toBe('one');
|
||||
expect(f[1]).toBe('two');
|
||||
|
||||
f = api.fields_({ query: { fields: ' ' } }, ['def']);
|
||||
expect(f.length).toBe(1);
|
||||
expect(f[0]).toBe('def');
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should handle tokens', async (done) => {
|
||||
api = new Api('mytoken');
|
||||
|
||||
const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'notes'));
|
||||
expect(hasThrown).toBe(true);
|
||||
|
||||
const response = await api.route('GET', 'notes', { token: 'mytoken' })
|
||||
expect(response.length).toBe(0);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
@@ -48,7 +48,9 @@ EncryptionService.fsDriver_ = fsDriver;
|
||||
FileApiDriverLocal.fsDriver_ = fsDriver;
|
||||
|
||||
const logDir = __dirname + '/../tests/logs';
|
||||
const tempDir = __dirname + '/../tests/tmp';
|
||||
fs.mkdirpSync(logDir, 0o755);
|
||||
fs.mkdirpSync(tempDir, 0o755);
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetMemory);
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
@@ -80,6 +82,7 @@ BaseItem.loadClass('MasterKey', MasterKey);
|
||||
|
||||
Setting.setConstant('appId', 'net.cozic.joplin-cli');
|
||||
Setting.setConstant('appType', 'cli');
|
||||
Setting.setConstant('tempDir', tempDir);
|
||||
|
||||
BaseService.logger_ = logger;
|
||||
|
||||
|
@@ -80,8 +80,9 @@
|
||||
title: title,
|
||||
html: html,
|
||||
base_url: baseUrl(),
|
||||
url: location.origin + location.pathname,
|
||||
url: location.origin + location.pathname + location.search,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags || '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -214,6 +215,7 @@
|
||||
crop_rect: selectionArea,
|
||||
url: location.origin + location.pathname,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags,
|
||||
};
|
||||
|
||||
browser_.runtime.sendMessage({
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "1.0.6",
|
||||
"version": "1.0.7",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplin.cozic.net",
|
||||
"icons": {
|
||||
|
@@ -1,5 +1,7 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<!-- NOTE: I think this is not used at all -->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
@@ -100,21 +100,34 @@
|
||||
flex: 0;
|
||||
}
|
||||
|
||||
.App .Folders {
|
||||
.App .Folders,
|
||||
.App .Tags {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: top;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.App .Folders label {
|
||||
.App .Folders label,
|
||||
.App .Tags label {
|
||||
flex: 0;
|
||||
white-space: nowrap;
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.App .Folders select {
|
||||
flex: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.App .Tags input {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
margin-bottom: .5em;
|
||||
}
|
||||
|
||||
.App .ClearTagButton {
|
||||
margin-left: .5em;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.App .StatusBar {
|
||||
|
@@ -14,6 +14,7 @@ class AppComponent extends Component {
|
||||
|
||||
this.state = ({
|
||||
contentScriptLoaded: false,
|
||||
selectedTags: [],
|
||||
});
|
||||
|
||||
this.confirm_click = () => {
|
||||
@@ -31,6 +32,7 @@ class AppComponent extends Component {
|
||||
bridge().sendCommandToActiveTab({
|
||||
name: 'simplifiedPageHtml',
|
||||
parent_id: this.props.selectedFolderId,
|
||||
tags: this.state.selectedTags.join(','),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -38,6 +40,7 @@ class AppComponent extends Component {
|
||||
bridge().sendCommandToActiveTab({
|
||||
name: 'completePageHtml',
|
||||
parent_id: this.props.selectedFolderId,
|
||||
tags: this.state.selectedTags.join(','),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -45,6 +48,7 @@ class AppComponent extends Component {
|
||||
bridge().sendCommandToActiveTab({
|
||||
name: 'selectedHtml',
|
||||
parent_id: this.props.selectedFolderId,
|
||||
tags: this.state.selectedTags.join(','),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -56,6 +60,7 @@ class AppComponent extends Component {
|
||||
name: 'screenshot',
|
||||
api_base_url: baseUrl,
|
||||
parent_id: this.props.selectedFolderId,
|
||||
tags: this.state.selectedTags.join(','),
|
||||
});
|
||||
|
||||
window.close();
|
||||
@@ -74,6 +79,41 @@ class AppComponent extends Component {
|
||||
id: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
this.tagCompChanged = this.tagCompChanged.bind(this);
|
||||
this.onAddTagClick = this.onAddTagClick.bind(this);
|
||||
this.onClearTagButtonClick = this.onClearTagButtonClick.bind(this);
|
||||
}
|
||||
|
||||
onAddTagClick(event) {
|
||||
const newTags = this.state.selectedTags.slice();
|
||||
newTags.push('');
|
||||
this.setState({ selectedTags: newTags });
|
||||
this.focusNewTagInput_ = true;
|
||||
}
|
||||
|
||||
onClearTagButtonClick(event) {
|
||||
const index = event.target.getAttribute('data-index');
|
||||
const newTags = this.state.selectedTags.slice();
|
||||
newTags.splice(index, 1);
|
||||
this.setState({ selectedTags: newTags });
|
||||
}
|
||||
|
||||
tagCompChanged(event) {
|
||||
const index = Number(event.target.getAttribute('data-index'));
|
||||
const value = event.target.value;
|
||||
|
||||
if (this.state.selectedTags.length <= index) {
|
||||
const newTags = this.state.selectedTags.slice();
|
||||
newTags.push(value);
|
||||
this.setState({ selectedTags: newTags });
|
||||
} else {
|
||||
if (this.state.selectedTags[index] !== value) {
|
||||
const newTags = this.state.selectedTags.slice();
|
||||
newTags[index] = value;
|
||||
this.setState({ selectedTags: newTags });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async loadContentScripts() {
|
||||
@@ -87,6 +127,39 @@ class AppComponent extends Component {
|
||||
this.setState({
|
||||
contentScriptLoaded: true,
|
||||
});
|
||||
|
||||
let foundSelectedFolderId = false;
|
||||
|
||||
const searchSelectedFolder = (folders) => {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const folder = folders[i];
|
||||
if (folder.id === this.props.selectedFolderId) foundSelectedFolderId = true;
|
||||
if (folder.children) searchSelectedFolder(folder.children);
|
||||
}
|
||||
}
|
||||
|
||||
searchSelectedFolder(this.props.folders);
|
||||
|
||||
if (!foundSelectedFolderId) {
|
||||
const newFolderId = this.props.folders.length ? this.props.folders[0].id : null;
|
||||
this.props.dispatch({
|
||||
type: 'SELECTED_FOLDER_SET',
|
||||
id: newFolderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.focusNewTagInput_) {
|
||||
this.focusNewTagInput_ = false;
|
||||
let lastRef = null;
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const ref = this.refs['tagSelector' + i];
|
||||
if (!ref) break;
|
||||
lastRef = ref;
|
||||
}
|
||||
if (lastRef) lastRef.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -119,24 +192,17 @@ class AppComponent extends Component {
|
||||
<p className="Info">{ msg }</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (hasContent) {
|
||||
previewComponent = (
|
||||
<div className="Preview">
|
||||
<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/>
|
||||
<div className={"BodyWrapper"}>
|
||||
<div className={"Body"} dangerouslySetInnerHTML={{__html: content.body_html}}></div>
|
||||
</div>
|
||||
<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a>
|
||||
} else if (hasContent) {
|
||||
previewComponent = (
|
||||
<div className="Preview">
|
||||
<h2>Preview:</h2>
|
||||
<input className={"Title"} value={content.title} onChange={this.contentTitle_change}/>
|
||||
<div className={"BodyWrapper"}>
|
||||
<div className={"Body"} dangerouslySetInnerHTML={{__html: content.body_html}}></div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
previewComponent = (
|
||||
<div className="Preview">
|
||||
<p className="Info">(No preview yet)</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<a className={"Confirm Button"} onClick={this.confirm_click}>Confirm</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const clipperStatusComp = () => {
|
||||
@@ -166,8 +232,6 @@ class AppComponent extends Component {
|
||||
return <div className="StatusBar"><img alt={foundState} className="Led" src={led}/><span className="ServerStatus">{ msg }{ helpLink }</span></div>
|
||||
}
|
||||
|
||||
console.info(this.props.selectedFolderId);
|
||||
|
||||
const foldersComp = () => {
|
||||
const optionComps = [];
|
||||
|
||||
@@ -196,6 +260,37 @@ class AppComponent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
const tagsComp = () => {
|
||||
const comps = [];
|
||||
for (let i = 0; i < this.state.selectedTags.length; i++) {
|
||||
comps.push(<div>
|
||||
<input
|
||||
ref={'tagSelector' + i}
|
||||
data-index={i}
|
||||
key={i}
|
||||
type="text"
|
||||
list="tags"
|
||||
value={this.state.selectedTags[i]}
|
||||
onChange={this.tagCompChanged}
|
||||
onInput={this.tagCompChanged}
|
||||
/>
|
||||
<a data-index={i} href="#" className="ClearTagButton" onClick={this.onClearTagButtonClick}>[x]</a>
|
||||
</div>);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{comps}
|
||||
<a className="AddTagButton" href="#" onClick={this.onAddTagClick}>Add tag</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const tagDataListOptions = [];
|
||||
for (let i = 0; i < this.props.tags.length; i++) {
|
||||
const tag = this.props.tags[i];
|
||||
tagDataListOptions.push(<option key={tag.id}>{tag.title}</option>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="Controls">
|
||||
@@ -207,8 +302,14 @@ class AppComponent extends Component {
|
||||
</ul>
|
||||
</div>
|
||||
{ foldersComp() }
|
||||
<div className="Tags">
|
||||
<label>Tags:</label>
|
||||
{tagsComp()}
|
||||
<datalist id="tags">
|
||||
{tagDataListOptions}
|
||||
</datalist>
|
||||
</div>
|
||||
{ warningComponent }
|
||||
<h2>Preview:</h2>
|
||||
{ previewComponent }
|
||||
{ clipperStatusComp() }
|
||||
</div>
|
||||
@@ -224,6 +325,7 @@ const mapStateToProps = (state) => {
|
||||
contentUploadOperation: state.contentUploadOperation,
|
||||
clipperServer: state.clipperServer,
|
||||
folders: state.folders,
|
||||
tags: state.tags,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
};
|
||||
};
|
||||
|
@@ -2,7 +2,7 @@ const randomClipperPort = require('./randomClipperPort');
|
||||
|
||||
class Bridge {
|
||||
|
||||
init(browser, browserSupportsPromises, dispatch) {
|
||||
async init(browser, browserSupportsPromises, dispatch) {
|
||||
console.info('Popup: Init bridge');
|
||||
|
||||
this.browser_ = browser;
|
||||
@@ -28,6 +28,7 @@ class Bridge {
|
||||
base_url: command.base_url,
|
||||
source_url: command.url,
|
||||
parent_id: command.parent_id,
|
||||
tags: command.tags || '',
|
||||
};
|
||||
|
||||
this.dispatch({ type: 'CLIPPED_CONTENT_SET', content: content });
|
||||
@@ -36,7 +37,7 @@ class Bridge {
|
||||
|
||||
this.browser_.runtime.onMessage.addListener(this.browser_notify);
|
||||
|
||||
const backgroundPage = this.browser_.extension.getBackgroundPage();
|
||||
const backgroundPage = await this.backgroundPage(this.browser_);
|
||||
|
||||
// Not sure why the getBackgroundPage() sometimes returns null, so
|
||||
// in that case default to "prod" environment, which means the live
|
||||
@@ -53,6 +54,17 @@ class Bridge {
|
||||
this.findClipperServerPort();
|
||||
}
|
||||
|
||||
async backgroundPage(browser) {
|
||||
const bgp = browser.extension.getBackgroundPage();
|
||||
if (bgp) return bgp;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
browser.runtime.getBackgroundPage((bgp) => {
|
||||
resolve(bgp);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
env() {
|
||||
return this.env_;
|
||||
}
|
||||
@@ -111,6 +123,11 @@ class Bridge {
|
||||
|
||||
const folders = await this.folderTree();
|
||||
this.dispatch({ type: 'FOLDERS_SET', folders: folders });
|
||||
|
||||
const tags = await this.clipperApiExec('GET', 'tags');
|
||||
this.dispatch({ type: 'TAGS_SET', tags: tags });
|
||||
|
||||
bridge().restoreState();
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
|
@@ -16,6 +16,7 @@ const defaultState = {
|
||||
port: null,
|
||||
},
|
||||
folders: [],
|
||||
tags: [],
|
||||
selectedFolderId: null,
|
||||
env: 'prod',
|
||||
};
|
||||
@@ -65,6 +66,11 @@ function reducer(state = defaultState, action) {
|
||||
newState.selectedFolderId = action.folders[0].id;
|
||||
}
|
||||
|
||||
} else if (action.type === 'TAGS_SET') {
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.tags = action.tags;
|
||||
|
||||
} else if (action.type === 'SELECTED_FOLDER_SET') {
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
@@ -88,11 +94,18 @@ function reducer(state = defaultState, action) {
|
||||
return newState;
|
||||
}
|
||||
|
||||
const store = createStore(reducer, applyMiddleware(reduxMiddleware));
|
||||
async function main() {
|
||||
const store = createStore(reducer, applyMiddleware(reduxMiddleware));
|
||||
|
||||
bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store.dispatch);
|
||||
bridge().restoreState();
|
||||
console.info('Popup: Init bridge and restore state...');
|
||||
|
||||
console.info('Popup: Creating React app...');
|
||||
await bridge().init(window.browser ? window.browser : window.chrome, !!window.browser, store.dispatch);
|
||||
|
||||
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
|
||||
console.info('Popup: Creating React app...');
|
||||
|
||||
ReactDOM.render(<Provider store={store}><App /></Provider>, document.getElementById('root'));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error on initialisation:', error);
|
||||
});
|
@@ -166,8 +166,10 @@ class Application extends BaseApplication {
|
||||
|
||||
case 'NOTE_FILE_WATCHER_CLEAR':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.watchedNoteFiles = [];
|
||||
if (state.watchedNoteFiles.length) {
|
||||
newState = Object.assign({}, state);
|
||||
newState.watchedNoteFiles = [];
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
|
@@ -748,7 +748,7 @@ class NoteTextComponent extends React.Component {
|
||||
this.forceUpdate();
|
||||
}, 100);
|
||||
},
|
||||
postMessageSyntax: 'ipcRenderer.sendToHost',
|
||||
postMessageSyntax: 'ipcProxySendToHost',
|
||||
};
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
@@ -1404,6 +1404,7 @@ class NoteTextComponent extends React.Component {
|
||||
style={viewerStyle}
|
||||
preload="gui/note-viewer/preload.js"
|
||||
src="gui/note-viewer/index.html"
|
||||
webpreferences="contextIsolation"
|
||||
ref={(elem) => { this.webview_ref(elem); } }
|
||||
/>
|
||||
|
||||
|
@@ -36,6 +36,22 @@
|
||||
<script>
|
||||
const contentElement = document.getElementById('content');
|
||||
|
||||
const ipc = {};
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
// Here we only deal with messages that are sent from the main Electro process to the webview.
|
||||
if (!event.data || event.data.target !== 'webview') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
const callData = event.data.data;
|
||||
|
||||
if (!ipc[callName]) {
|
||||
console.warn('Missing IPC function:', event.data);
|
||||
} else {
|
||||
ipc[callName](callData);
|
||||
}
|
||||
});
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Handle dynamically loading HLJS when a code element is present
|
||||
// ----------------------------------------------------------------------
|
||||
@@ -119,7 +135,9 @@
|
||||
setPercentScroll(percentScroll_);
|
||||
}
|
||||
|
||||
ipcRenderer.on('setHtml', (event, html) => {
|
||||
ipc.setHtml = (event) => {
|
||||
const html = event.html;
|
||||
|
||||
updateBodyHeight();
|
||||
|
||||
contentElement.innerHTML = html;
|
||||
@@ -158,10 +176,12 @@
|
||||
}
|
||||
}, 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let ignoreNextScrollEvent = false;
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
ipc.setPercentScroll = (event) => {
|
||||
const percent = event.percent;
|
||||
|
||||
if (checkScrollIID_) {
|
||||
clearInterval(checkScrollIID_);
|
||||
checkScrollIID_ = null;
|
||||
@@ -169,7 +189,7 @@
|
||||
|
||||
ignoreNextScrollEvent = true;
|
||||
setPercentScroll(percent);
|
||||
});
|
||||
}
|
||||
|
||||
let mark_ = null;
|
||||
function setMarkers(keywords) {
|
||||
@@ -184,7 +204,9 @@
|
||||
}
|
||||
|
||||
let markLoaded_ = false;
|
||||
ipcRenderer.on('setMarkers', (event, keywords) => {
|
||||
ipc.setMarkers = (event) => {
|
||||
const keywords = event.keywords;
|
||||
|
||||
if (!keywords.length && !markLoaded_) return;
|
||||
|
||||
if (!markLoaded_) {
|
||||
@@ -199,7 +221,7 @@
|
||||
} else {
|
||||
setMarkers(keywords);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function maxScrollTop() {
|
||||
return Math.max(0, contentElement.scrollHeight - contentElement.clientHeight);
|
||||
@@ -210,6 +232,10 @@
|
||||
document.getElementById('body').style.height = window.innerHeight + 'px';
|
||||
}
|
||||
|
||||
const ipcProxySendToHost = (methodName, arg) => {
|
||||
window.postMessage({ target: 'main', name: methodName, args: [ arg ] }, '*');
|
||||
}
|
||||
|
||||
contentElement.addEventListener('scroll', function(e) {
|
||||
if (ignoreNextScrollEvent) {
|
||||
ignoreNextScrollEvent = false;
|
||||
@@ -218,7 +244,8 @@
|
||||
const m = maxScrollTop();
|
||||
const percent = m ? contentElement.scrollTop / m : 0;
|
||||
setPercentScroll(percent);
|
||||
ipcRenderer.sendToHost('percentScroll', percent);
|
||||
|
||||
ipcProxySendToHost('percentScroll', percent);
|
||||
});
|
||||
|
||||
document.addEventListener('contextmenu', function(event) {
|
||||
@@ -228,7 +255,7 @@
|
||||
if (element && !element.getAttribute('data-resource-id')) element = element.parentElement;
|
||||
|
||||
if (element && element.getAttribute('data-resource-id')) {
|
||||
ipcRenderer.sendToHost('contextMenu', {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: element.getAttribute('src') ? 'image' : 'resource',
|
||||
resourceId: element.getAttribute('data-resource-id'),
|
||||
});
|
||||
@@ -236,12 +263,12 @@
|
||||
const selectedText = window.getSelection().toString();
|
||||
|
||||
if (selectedText) {
|
||||
ipcRenderer.sendToHost('contextMenu', {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: 'text',
|
||||
textToCopy: selectedText,
|
||||
});
|
||||
} else if (event.target.getAttribute('href')) {
|
||||
ipcRenderer.sendToHost('contextMenu', {
|
||||
ipcProxySendToHost('contextMenu', {
|
||||
type: 'link',
|
||||
textToCopy: event.target.getAttribute('href'),
|
||||
});
|
||||
|
@@ -1,4 +1,36 @@
|
||||
// Define here Electron objects that need to be accessed from the WebView
|
||||
// https://github.com/electron/electron/blob/master/docs/tutorial/security.md#2-disable-nodejs-integration-for-remote-content
|
||||
// In order to give access to the webview to certain functions of the main process, we need
|
||||
// this bridge which listens from the main process and sends to the webview and the other
|
||||
// way around. This is necessary after having enabled the "contextIsolation" option, which
|
||||
// prevents the webview from accessing low-level methods in the main process.
|
||||
|
||||
window.ipcRenderer = require('electron').ipcRenderer;
|
||||
const ipcRenderer = require('electron').ipcRenderer;
|
||||
|
||||
ipcRenderer.on('setHtml', (event, html) => {
|
||||
window.postMessage({ target: 'webview', name: 'setHtml', data: { html: html } }, '*');
|
||||
});
|
||||
|
||||
ipcRenderer.on('setPercentScroll', (event, percent) => {
|
||||
window.postMessage({ target: 'webview', name: 'setPercentScroll', data: { percent: percent } }, '*');
|
||||
});
|
||||
|
||||
ipcRenderer.on('setMarkers', (event, keywords) => {
|
||||
window.postMessage({ target: 'webview', name: 'setMarkers', data: { keywords: keywords } }, '*');
|
||||
});
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
// Here we only deal with messages that are sent from the webview to the main Electron process
|
||||
if (!event.data || event.data.target !== 'main') return;
|
||||
|
||||
const callName = event.data.name;
|
||||
const args = event.data.args;
|
||||
|
||||
if (args.length === 0) {
|
||||
ipcRenderer.sendToHost(callName);
|
||||
} else if (args.length === 1) {
|
||||
ipcRenderer.sendToHost(callName, args[0]);
|
||||
} else if (args.length === 2) {
|
||||
ipcRenderer.sendToHost(callName, args[1]);
|
||||
} else {
|
||||
throw new Error('Unsupported number of args');
|
||||
}
|
||||
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
ElectronClient/app/package-lock.json
generated
2
ElectronClient/app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.107",
|
||||
"version": "1.0.109",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Joplin",
|
||||
"version": "1.0.107",
|
||||
"version": "1.0.109",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
@@ -92,7 +92,7 @@
|
||||
"highlight.js": "^9.12.0",
|
||||
"html-entities": "^1.2.1",
|
||||
"image-type": "^3.0.0",
|
||||
"joplin-turndown": "^4.0.8",
|
||||
"joplin-turndown": "^4.0.9",
|
||||
"joplin-turndown-plugin-gfm": "^1.0.7",
|
||||
"jssha": "^2.3.1",
|
||||
"katex": "^0.10.0-rc.1",
|
||||
|
@@ -1,7 +1,9 @@
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
const zoomRatio = Setting.value('style.zoom') / 100;
|
||||
|
||||
const globalStyle = {
|
||||
fontSize: 12 * Setting.value('style.zoom') / 100,
|
||||
fontSize: Math.round(12 * zoomRatio),
|
||||
fontFamily: 'sans-serif',
|
||||
margin: 15, // No text and no interactive component should be within this margin
|
||||
itemMarginTop: 10,
|
||||
@@ -46,7 +48,7 @@ globalStyle.htmlColor ='black'; // Note: CSS in WebView component only supports
|
||||
globalStyle.htmlBackgroundColor ='white';
|
||||
globalStyle.htmlDividerColor = 'rgb(150,150,150)';
|
||||
globalStyle.htmlLinkColor ='blue';
|
||||
globalStyle.htmlLineHeight ='20px';
|
||||
globalStyle.htmlLineHeight = Math.round(20 * zoomRatio) + 'px';
|
||||
|
||||
globalStyle.marginRight = globalStyle.margin;
|
||||
globalStyle.marginLeft = globalStyle.margin;
|
||||
|
16
README.md
16
README.md
@@ -20,9 +20,9 @@ Three types of applications are available: for the **desktop** (Windows, macOS a
|
||||
|
||||
Operating System | Download | Alternative
|
||||
-----------------|--------|-------------------
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/Joplin-Setup-1.0.105.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a> | or Get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/JoplinPortable.exe'>Portable version</a><br>(to run from a USB key, etc.)
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/Joplin-1.0.105.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a> |
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/Joplin-1.0.105-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a> | An Arch Linux package<br>[is also available](#terminal-application).
|
||||
Windows (32 and 64-bit) | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.109/Joplin-Setup-1.0.109.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a> | or Get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.109/JoplinPortable.exe'>Portable version</a><br>(to run from a USB key, etc.)
|
||||
macOS | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.107/Joplin-1.0.107.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a> |
|
||||
Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.0.109/Joplin-1.0.109-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a> | An Arch Linux package<br>[is also available](#terminal-application).
|
||||
|
||||
The [portable application](https://en.wikipedia.org/wiki/Portable_application) allows installing the software on a portable device such as a USB key. Simply copy the file JoplinPortable.exe in any directory on that USB key ; the application will then create a directory called "JoplinProfile" next to the executable file.
|
||||
|
||||
@@ -36,7 +36,7 @@ wget -O - https://raw.githubusercontent.com/laurent22/joplin/master/install_ubun
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
-----------------|----------|----------------
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplin.cozic.net/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.131/joplin-v1.0.131.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplin.cozic.net/images/BadgeAndroid.png'/></a> | or [Download APK File](https://github.com/laurent22/joplin-android/releases/download/android-v1.0.133/joplin-v1.0.133.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplin.cozic.net/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
@@ -274,7 +274,9 @@ The checkboxes can then be ticked in the mobile and desktop applications.
|
||||
|
||||
## HTML support
|
||||
|
||||
Only the `<br>` tag is supported - it can be used to force a new line, which is convenient to insert new lines inside table cells. For security reasons, other HTML tags are not supported.
|
||||
It is generally recommended to enter the notes as Markdown as it makes the notes easier to edit. However for cases where certain features aren't supported (such as strikethrough or to highlight text), you can also use HTML code directly. For example this would be a valid note:
|
||||
|
||||
This is <s>strikethrough text</s> mixed with regular **Markdown**.
|
||||
|
||||
# Donations
|
||||
|
||||
@@ -319,7 +321,7 @@ Current translations:
|
||||
 | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Michael Sonntag (ms@editorei.de) | 98%
|
||||
 | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
|
||||
 | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín (f@mrtn.es) | 98%
|
||||
 | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 98%
|
||||
 | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
|
||||
 | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 81%
|
||||
 | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 97%
|
||||
 | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 98%
|
||||
@@ -332,7 +334,7 @@ Current translations:
|
||||
 | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 80%
|
||||
 | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 98%
|
||||
 | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po) | penguinsam (samliu@gmail.com) | 98%
|
||||
 | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 52%
|
||||
 | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | AWASHIRO Ikuya (ikunya@gmail.com) | 99%
|
||||
 | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 98%
|
||||
<!-- LOCALE-TABLE-AUTO-GENERATED -->
|
||||
|
||||
|
@@ -90,8 +90,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 26
|
||||
versionCode 2097309
|
||||
versionName "1.0.131"
|
||||
versionCode 2097311
|
||||
versionName "1.0.133"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86"
|
||||
}
|
||||
|
@@ -1,18 +1,10 @@
|
||||
const { netUtils } = require('lib/net-utils');
|
||||
const urlParser = require("url");
|
||||
const Note = require('lib/models/Note');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Resource = require('lib/models/Resource');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const { shim } = require('lib/shim');
|
||||
const md5 = require('md5');
|
||||
const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils');
|
||||
const HtmlToMd = require('lib/HtmlToMd');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const mimeUtils = require('lib/mime-utils.js').mime;
|
||||
const randomClipperPort = require('lib/randomClipperPort');
|
||||
const enableServerDestroy = require('server-destroy');
|
||||
const Api = require('lib/services/rest/Api');
|
||||
|
||||
class ClipperServer {
|
||||
|
||||
@@ -21,6 +13,7 @@ class ClipperServer {
|
||||
this.startState_ = 'idle';
|
||||
this.server_ = null;
|
||||
this.port_ = null;
|
||||
this.api_ = new Api();
|
||||
}
|
||||
|
||||
static instance() {
|
||||
@@ -31,6 +24,7 @@ class ClipperServer {
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
this.api_.setLogger(l);
|
||||
}
|
||||
|
||||
logger() {
|
||||
@@ -64,133 +58,6 @@ class ClipperServer {
|
||||
});
|
||||
}
|
||||
|
||||
htmlToMdParser() {
|
||||
if (this.htmlToMdParser_) return this.htmlToMdParser_;
|
||||
this.htmlToMdParser_ = new HtmlToMd();
|
||||
return this.htmlToMdParser_;
|
||||
}
|
||||
|
||||
async requestNoteToNote(requestNote) {
|
||||
const output = {
|
||||
title: requestNote.title ? requestNote.title : '',
|
||||
body: requestNote.body ? requestNote.body : '',
|
||||
};
|
||||
|
||||
if (requestNote.body_html) {
|
||||
// Parsing will not work if the HTML is not wrapped in a top level tag, which is not guaranteed
|
||||
// when getting the content from elsewhere. So here wrap it - it won't change anything to the final
|
||||
// rendering but it makes sure everything will be parsed.
|
||||
output.body = await this.htmlToMdParser().parse('<div>' + requestNote.body_html + '</div>', {
|
||||
baseUrl: requestNote.base_url ? requestNote.base_url : '',
|
||||
});
|
||||
}
|
||||
|
||||
if (requestNote.parent_id) {
|
||||
output.parent_id = requestNote.parent_id;
|
||||
} else {
|
||||
const folder = await Folder.defaultFolder();
|
||||
if (!folder) throw new Error('Cannot find folder for note');
|
||||
output.parent_id = folder.id;
|
||||
}
|
||||
|
||||
if (requestNote.source_url) output.source_url = requestNote.source_url;
|
||||
if (requestNote.author) output.author = requestNote.author;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Note must have been saved first
|
||||
async attachImageFromDataUrl_(note, imageDataUrl, cropRect) {
|
||||
const tempDir = Setting.value('tempDir');
|
||||
const mime = mimeUtils.fromDataUrl(imageDataUrl);
|
||||
let ext = mimeUtils.toFileExtension(mime) || '';
|
||||
if (ext) ext = '.' + ext;
|
||||
const tempFilePath = tempDir + '/' + md5(Math.random() + '_' + Date.now()) + ext;
|
||||
const imageConvOptions = {};
|
||||
if (cropRect) imageConvOptions.cropRect = cropRect;
|
||||
await shim.imageFromDataUrl(imageDataUrl, tempFilePath, imageConvOptions);
|
||||
return await shim.attachFileToNote(note, tempFilePath);
|
||||
}
|
||||
|
||||
async downloadImage_(url) {
|
||||
const tempDir = Setting.value('tempDir');
|
||||
const name = filename(url);
|
||||
let fileExt = safeFileExtension(fileExtension(url).toLowerCase());
|
||||
if (fileExt) fileExt = '.' + fileExt;
|
||||
let imagePath = tempDir + '/' + safeFilename(name) + fileExt;
|
||||
if (await shim.fsDriver().exists(imagePath)) imagePath = tempDir + '/' + safeFilename(name) + '_' + md5(Math.random() + '_' + Date.now()).substr(0,10) + fileExt;
|
||||
|
||||
try {
|
||||
const result = await shim.fetchBlob(url, { path: imagePath });
|
||||
return imagePath;
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot download image at ' + url, error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async downloadImages_(urls) {
|
||||
const PromisePool = require('es6-promise-pool')
|
||||
|
||||
const output = {};
|
||||
|
||||
let urlIndex = 0;
|
||||
const promiseProducer = () => {
|
||||
if (urlIndex >= urls.length) return null;
|
||||
|
||||
const url = urls[urlIndex++];
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const imagePath = await this.downloadImage_(url);
|
||||
if (imagePath) output[url] = { path: imagePath };
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
const concurrency = 3
|
||||
const pool = new PromisePool(promiseProducer, concurrency)
|
||||
await pool.start()
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async createResourcesFromPaths_(urls) {
|
||||
for (let url in urls) {
|
||||
if (!urls.hasOwnProperty(url)) continue;
|
||||
const urlInfo = urls[url];
|
||||
try {
|
||||
const resource = await shim.createResourceFromPath(urlInfo.path);
|
||||
urlInfo.resource = resource;
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot create resource for ' + url, error);
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
async removeTempFiles_(urls) {
|
||||
for (let url in urls) {
|
||||
if (!urls.hasOwnProperty(url)) continue;
|
||||
const urlInfo = urls[url];
|
||||
try {
|
||||
await shim.fsDriver().remove(urlInfo.path);
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot remove ' + urlInfo.path, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replaceImageUrlsByResources_(md, urls) {
|
||||
let output = md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (match, before, imageUrl, after) => {
|
||||
const urlInfo = urls[imageUrl];
|
||||
if (!urlInfo || !urlInfo.resource) return before + imageUrl + after;
|
||||
const resourceUrl = Resource.internalUrl(urlInfo.resource);
|
||||
return before + resourceUrl + after;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async findAvailablePort() {
|
||||
const tcpPortUsed = require('tcp-port-used');
|
||||
|
||||
@@ -243,22 +110,33 @@ class ClipperServer {
|
||||
response.end();
|
||||
}
|
||||
|
||||
const requestId = Date.now();
|
||||
this.logger().info('Request (' + requestId + '): ' + request.method + ' ' + request.url);
|
||||
const writeResponse = (code, response) => {
|
||||
if (typeof response === 'string') {
|
||||
writeResponseText(code, response);
|
||||
} else {
|
||||
writeResponseJson(code, response);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger().info('Request: ' + request.method + ' ' + request.url);
|
||||
|
||||
const url = urlParser.parse(request.url, true);
|
||||
|
||||
if (request.method === 'GET') {
|
||||
if (url.pathname === '/ping') {
|
||||
return writeResponseText(200, 'JoplinClipperServer');
|
||||
const execRequest = async (request, body = '') => {
|
||||
try {
|
||||
const response = await this.api_.route(request.method, url.pathname, url.query, body);
|
||||
writeResponse(200, response);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
writeResponse(error.httpCode ? error.httpCode : 500, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (url.pathname === '/folders') {
|
||||
const structure = await Folder.allAsTree({ fields: ['id', 'parent_id', 'title'] });
|
||||
return writeResponseJson(200, structure);
|
||||
}
|
||||
} else if (request.method === 'POST') {
|
||||
if (url.pathname === '/notes') {
|
||||
if (request.method === 'OPTIONS') {
|
||||
writeCorsHeaders(200);
|
||||
response.end();
|
||||
} else {
|
||||
if (request.method === 'POST') {
|
||||
let body = '';
|
||||
|
||||
request.on('data', (data) => {
|
||||
@@ -266,44 +144,14 @@ class ClipperServer {
|
||||
});
|
||||
|
||||
request.on('end', async () => {
|
||||
try {
|
||||
const requestNote = JSON.parse(body);
|
||||
let note = await this.requestNoteToNote(requestNote);
|
||||
|
||||
const imageUrls = markdownUtils.extractImageUrls(note.body);
|
||||
let result = await this.downloadImages_(imageUrls);
|
||||
result = await this.createResourcesFromPaths_(result);
|
||||
await this.removeTempFiles_(result);
|
||||
note.body = this.replaceImageUrlsByResources_(note.body, result);
|
||||
|
||||
note = await Note.save(note);
|
||||
|
||||
if (requestNote.image_data_url) {
|
||||
await this.attachImageFromDataUrl_(note, requestNote.image_data_url, requestNote.crop_rect);
|
||||
}
|
||||
|
||||
this.logger().info('Request (' + requestId + '): Created note ' + note.id);
|
||||
return writeResponseJson(200, note);
|
||||
} catch (error) {
|
||||
this.logger().error(error);
|
||||
return writeResponseJson(400, { errorCode: 'exception', errorMessage: error.message });
|
||||
}
|
||||
execRequest(request, body);
|
||||
});
|
||||
} else {
|
||||
return writeResponseJson(404, { errorCode: 'not_found' });
|
||||
execRequest(request);
|
||||
}
|
||||
} else if (request.method === 'OPTIONS') {
|
||||
writeCorsHeaders(200);
|
||||
response.end();
|
||||
} else {
|
||||
return writeResponseJson(405, { errorCode: 'method_not_allowed' });
|
||||
}
|
||||
});
|
||||
|
||||
this.server_.on('close', () => {
|
||||
|
||||
});
|
||||
|
||||
enableServerDestroy(this.server_);
|
||||
|
||||
this.logger().info('Starting Clipper server on port ' + this.port_);
|
||||
|
@@ -73,31 +73,31 @@ class MdToHtml {
|
||||
return attrs;
|
||||
}
|
||||
|
||||
renderImage_(attrs, options) {
|
||||
const loadResource = async (id) => {
|
||||
// console.info('Loading resource: ' + id);
|
||||
async loadResource(id, options) {
|
||||
// console.info('Loading resource: ' + id);
|
||||
|
||||
// Initially set to to an empty object to make
|
||||
// it clear that it is being loaded. Otherwise
|
||||
// it sometimes results in multiple calls to
|
||||
// loadResource() for the same resource.
|
||||
this.loadedResources_[id] = {};
|
||||
// Initially set to to an empty object to make
|
||||
// it clear that it is being loaded. Otherwise
|
||||
// it sometimes results in multiple calls to
|
||||
// loadResource() for the same resource.
|
||||
this.loadedResources_[id] = {};
|
||||
|
||||
const resource = await Resource.load(id);
|
||||
//const resource = await this.modelCache_.load(Resource, id);
|
||||
const resource = await Resource.load(id);
|
||||
//const resource = await this.modelCache_.load(Resource, id);
|
||||
|
||||
if (!resource) {
|
||||
// Can happen for example if an image is attached to a note, but the resource hasn't
|
||||
// been downloaded from the sync target yet.
|
||||
console.warn('Cannot load resource: ' + id);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedResources_[id] = resource;
|
||||
|
||||
if (options.onResourceLoaded) options.onResourceLoaded();
|
||||
if (!resource) {
|
||||
// Can happen for example if an image is attached to a note, but the resource hasn't
|
||||
// been downloaded from the sync target yet.
|
||||
console.warn('Cannot load resource: ' + id);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadedResources_[id] = resource;
|
||||
|
||||
if (options.onResourceLoaded) options.onResourceLoaded();
|
||||
}
|
||||
|
||||
renderImage_(attrs, options) {
|
||||
const title = this.getAttr_(attrs, 'title');
|
||||
const href = this.getAttr_(attrs, 'src');
|
||||
|
||||
@@ -108,7 +108,7 @@ class MdToHtml {
|
||||
const resourceId = Resource.urlToId(href);
|
||||
const resource = this.loadedResources_[resourceId];
|
||||
if (!resource) {
|
||||
loadResource(resourceId);
|
||||
this.loadResource(resourceId, options);
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -125,6 +125,27 @@ class MdToHtml {
|
||||
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
|
||||
}
|
||||
|
||||
renderImageHtml_(before, src, after, options) {
|
||||
const resourceId = Resource.urlToId(src);
|
||||
const resource = this.loadedResources_[resourceId];
|
||||
if (!resource) {
|
||||
this.loadResource(resourceId, options);
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!resource.id) return ''; // Resource is being loaded
|
||||
|
||||
const mime = resource.mime ? resource.mime.toLowerCase() : '';
|
||||
if (Resource.isSupportedImageMimeType(mime)) {
|
||||
let newSrc = './' + Resource.filename(resource);
|
||||
if (this.resourceBaseUrl_ !== null) newSrc = this.resourceBaseUrl_ + newSrc;
|
||||
let output = '<img ' + before + ' data-resource-id="' + resource.id + '" src="' + newSrc + '" ' + after + '/>';
|
||||
return output;
|
||||
}
|
||||
|
||||
return '[Image: ' + htmlentities(resource.title) + ' (' + htmlentities(mime) + ')]';
|
||||
}
|
||||
|
||||
renderOpenLink_(attrs, options) {
|
||||
let href = this.getAttr_(attrs, 'href');
|
||||
const text = this.getAttr_(attrs, 'text');
|
||||
@@ -472,6 +493,12 @@ class MdToHtml {
|
||||
}
|
||||
}
|
||||
|
||||
renderedBody = renderedBody.replace(/<img(.*?)src=["'](.*?)["'](.*?)\/>/g, (v, before, src, after) => {
|
||||
if (!Resource.isResourceUrl(src)) return '<img ' + before + ' src="' + src + '" ' + after + '/>';
|
||||
return this.renderImageHtml_(before, src, after, options);
|
||||
});
|
||||
|
||||
|
||||
// https://necolas.github.io/normalize.css/
|
||||
const normalizeCss = `
|
||||
html{line-height:1.15;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
|
||||
@@ -561,7 +588,7 @@ class MdToHtml {
|
||||
border-bottom: 1px solid ` + style.htmlDividerColor + `;
|
||||
}
|
||||
img {
|
||||
width: auto;
|
||||
/* width: auto; */
|
||||
max-width: 100%;
|
||||
}
|
||||
.inline-code {
|
||||
|
@@ -26,7 +26,8 @@ class FsDriverNode extends FsDriverBase {
|
||||
|
||||
async writeBinaryFile(path, content) {
|
||||
try {
|
||||
let buffer = new Buffer(content);
|
||||
// let buffer = new Buffer(content);
|
||||
let buffer = Buffer.from(content);
|
||||
return await fs.writeFile(path, buffer);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
@@ -173,7 +174,8 @@ class FsDriverNode extends FsDriverBase {
|
||||
}
|
||||
|
||||
async readFileChunk(handle, length, encoding = 'base64') {
|
||||
let buffer = new Buffer(length);
|
||||
//let buffer = new Buffer(length);
|
||||
let buffer = Buffer.alloc(length);
|
||||
const result = await fs.read(handle, buffer, 0, length, null);
|
||||
if (!result.bytesRead) return null;
|
||||
buffer = buffer.slice(0, result.bytesRead);
|
||||
|
@@ -8,17 +8,19 @@ class GeolocationNode {
|
||||
|
||||
const ip = await netUtils.ip();
|
||||
|
||||
let response = await shim.fetch('https://freegeoip.net/json/' + ip);
|
||||
let response = await shim.fetch('http://ip-api.com/json/' + ip);
|
||||
if (!response.ok) throw new Error('Could not get geolocation: ' + await response.text());
|
||||
|
||||
response = await response.json();
|
||||
|
||||
if (!('lat' in response) || !('lon' in response)) throw new Error('Invalid geolocation response: ' . JSON.stringify(response));
|
||||
|
||||
return {
|
||||
timestamp: (new Date()).getTime(),
|
||||
coords: {
|
||||
longitude: response.longitude,
|
||||
longitude: response.lon,
|
||||
altitude: 0,
|
||||
latitude: response.latitude
|
||||
latitude: response.lat
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
const MarkdownIt = require('markdown-it');
|
||||
|
||||
const markdownUtils = {
|
||||
|
||||
@@ -20,15 +21,32 @@ const markdownUtils = {
|
||||
},
|
||||
|
||||
extractImageUrls(md) {
|
||||
// 
|
||||
const regex = new RegExp(/!\[.*?\]\(([^\s\)]+).*?\)/, 'g')
|
||||
let match = regex.exec(md);
|
||||
const markdownIt = new MarkdownIt();
|
||||
const env = {};
|
||||
const tokens = markdownIt.parse(md, env);
|
||||
const output = [];
|
||||
while (match) {
|
||||
const url = match[1];
|
||||
if (output.indexOf(url) < 0) output.push(url);
|
||||
match = regex.exec(md);
|
||||
|
||||
const searchUrls = (tokens) => {
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
|
||||
if (token.type === 'image') {
|
||||
for (let j = 0; j < token.attrs.length; j++) {
|
||||
const a = token.attrs[j];
|
||||
if (a[0] === 'src' && a.length >= 2 && a[1]) {
|
||||
output.push(a[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (token.children && token.children.length) {
|
||||
searchUrls(token.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
searchUrls(tokens);
|
||||
|
||||
return output;
|
||||
},
|
||||
|
||||
|
@@ -101,7 +101,6 @@ class Note extends BaseItem {
|
||||
if (!('latitude' in note) || !('longitude' in note)) throw new Error('Latitude or longitude is missing');
|
||||
if (!Number(note.latitude) && !Number(note.longitude)) throw new Error(_('This note does not have geolocation information.'));
|
||||
return this.geoLocationUrlFromLatLong(note.latitude, note.longitude);
|
||||
//return sprintf('https://www.openstreetmap.org/?lat=%s&lon=%s&zoom=20', note.latitude, note.longitude);
|
||||
}
|
||||
|
||||
static geoLocationUrlFromLatLong(lat, long) {
|
||||
@@ -208,8 +207,9 @@ class Note extends BaseItem {
|
||||
return ['id', 'title', 'body', 'is_todo', 'todo_completed', 'parent_id', 'updated_time', 'user_updated_time', 'user_created_time', 'encryption_applied'];
|
||||
}
|
||||
|
||||
static previewFieldsSql() {
|
||||
return this.db().escapeFields(this.previewFields()).join(',');
|
||||
static previewFieldsSql(fields = null) {
|
||||
if (fields === null) fields = this.previewFields();
|
||||
return this.db().escapeFields(fields).join(',');
|
||||
}
|
||||
|
||||
static async loadFolderNoteByField(folderId, field, value) {
|
||||
@@ -310,8 +310,9 @@ class Note extends BaseItem {
|
||||
return this.search(options);
|
||||
}
|
||||
|
||||
static preview(noteId) {
|
||||
return this.modelSelectOne('SELECT ' + this.previewFieldsSql() + ' FROM notes WHERE is_conflict = 0 AND id = ?', [noteId]);
|
||||
static preview(noteId, options = null) {
|
||||
if (!options) options = { fields: null };
|
||||
return this.modelSelectOne('SELECT ' + this.previewFieldsSql(options.fields) + ' FROM notes WHERE is_conflict = 0 AND id = ?', [noteId]);
|
||||
}
|
||||
|
||||
static conflictedNotes() {
|
||||
|
328
ReactNativeClient/lib/services/rest/Api.js
Normal file
328
ReactNativeClient/lib/services/rest/Api.js
Normal file
@@ -0,0 +1,328 @@
|
||||
const { ltrimSlashes } = require('lib/path-utils.js');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Note = require('lib/models/Note');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const mimeUtils = require('lib/mime-utils.js').mime;
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const md5 = require('md5');
|
||||
const { shim } = require('lib/shim');
|
||||
const HtmlToMd = require('lib/HtmlToMd');
|
||||
const { fileExtension, safeFileExtension, safeFilename, filename } = require('lib/path-utils');
|
||||
|
||||
class ApiError extends Error {
|
||||
|
||||
constructor(message, httpCode = 400) {
|
||||
super(message);
|
||||
this.httpCode_ = httpCode;
|
||||
}
|
||||
|
||||
get httpCode() {
|
||||
return this.httpCode_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorMethodNotAllowed extends ApiError {
|
||||
|
||||
constructor(message = 'Method Not Allowed') {
|
||||
super(message, 405);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorNotFound extends ApiError {
|
||||
|
||||
constructor(message = 'Not Found') {
|
||||
super(message, 404);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ErrorForbidden extends ApiError {
|
||||
|
||||
constructor(message = 'Forbidden') {
|
||||
super(message, 404);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Api {
|
||||
|
||||
constructor(token = null) {
|
||||
this.token_ = token;
|
||||
this.logger_ = new Logger();
|
||||
}
|
||||
|
||||
get token() {
|
||||
return this.token_;
|
||||
}
|
||||
|
||||
async route(method, path, query = null, body = null) {
|
||||
path = ltrimSlashes(path);
|
||||
if (!path) throw new ErrorNotFound(); // Nothing at the root yet
|
||||
|
||||
const pathParts = path.split('/');
|
||||
const callSuffix = pathParts.splice(0,1)[0];
|
||||
const callName = 'action_' + callSuffix;
|
||||
if (!this[callName]) throw new ErrorNotFound();
|
||||
|
||||
try {
|
||||
return this[callName]({
|
||||
method: method,
|
||||
query: query ? query : {},
|
||||
body: body,
|
||||
params: pathParts,
|
||||
});
|
||||
} catch (error) {
|
||||
if (!error.httpCode) error.httpCode = 500;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
fields_(request, defaultFields) {
|
||||
const query = request.query;
|
||||
if (!query || !query.fields) return defaultFields;
|
||||
const fields = query.fields.split(',').map(f => f.trim()).filter(f => !!f);
|
||||
return fields.length ? fields : defaultFields;
|
||||
}
|
||||
|
||||
checkToken_(request) {
|
||||
if (!this.token) return;
|
||||
if (!request.query || !request.query.token) throw new ErrorForbidden('Missing "token" parameter');
|
||||
if (request.query.token !== this.token) throw new ErrorForbidden('Invalid "token" parameter');
|
||||
}
|
||||
|
||||
async action_ping(request) {
|
||||
if (request.method === 'GET') {
|
||||
return 'JoplinClipperServer';
|
||||
}
|
||||
throw new ErrorMethodNotAllowed();
|
||||
}
|
||||
|
||||
async action_folders(request) {
|
||||
if (request.method === 'GET') {
|
||||
return await Folder.allAsTree({ fields: this.fields_(request, ['id', 'parent_id', 'title']) });
|
||||
}
|
||||
|
||||
throw new ErrorMethodNotAllowed();
|
||||
}
|
||||
|
||||
async action_tags(request) {
|
||||
if (request.method === 'GET') {
|
||||
return await Tag.all({ fields: this.fields_(request, ['id', 'title']) })
|
||||
}
|
||||
|
||||
throw new ErrorMethodNotAllowed();
|
||||
}
|
||||
|
||||
async action_notes(request) {
|
||||
if (request.method === 'GET') {
|
||||
this.checkToken_(request);
|
||||
|
||||
const noteId = request.params.length ? request.params[0] : null;
|
||||
const parentId = request.query.parent_id ? request.query.parent_id : null;
|
||||
const fields = this.fields_(request, []); // previews() already returns default fields
|
||||
const options = {};
|
||||
if (fields.length) options.fields = fields;
|
||||
|
||||
if (noteId) {
|
||||
return await Note.preview(noteId, options);
|
||||
} else {
|
||||
return await Note.previews(parentId, options);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.method === 'POST') {
|
||||
const requestId = Date.now();
|
||||
const requestNote = JSON.parse(request.body);
|
||||
let note = await this.requestNoteToNote(requestNote);
|
||||
|
||||
const imageUrls = markdownUtils.extractImageUrls(note.body);
|
||||
|
||||
this.logger().info('Request (' + requestId + '): Downloading images: ' + imageUrls.length);
|
||||
|
||||
let result = await this.downloadImages_(imageUrls);
|
||||
|
||||
this.logger().info('Request (' + requestId + '): Creating resources from paths: ' + Object.getOwnPropertyNames(result).length);
|
||||
|
||||
result = await this.createResourcesFromPaths_(result);
|
||||
await this.removeTempFiles_(result);
|
||||
note.body = this.replaceImageUrlsByResources_(note.body, result);
|
||||
|
||||
this.logger().info('Request (' + requestId + '): Saving note...');
|
||||
|
||||
note = await Note.save(note);
|
||||
|
||||
if (requestNote.tags) {
|
||||
const tagTitles = requestNote.tags.split(',');
|
||||
await Tag.setNoteTagsByTitles(note.id, tagTitles);
|
||||
}
|
||||
|
||||
if (requestNote.image_data_url) {
|
||||
note = await this.attachImageFromDataUrl_(note, requestNote.image_data_url, requestNote.crop_rect);
|
||||
}
|
||||
|
||||
this.logger().info('Request (' + requestId + '): Created note ' + note.id);
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
throw new ErrorMethodNotAllowed();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ========================================================================================================================
|
||||
// UTILIY FUNCTIONS
|
||||
// ========================================================================================================================
|
||||
|
||||
htmlToMdParser() {
|
||||
if (this.htmlToMdParser_) return this.htmlToMdParser_;
|
||||
this.htmlToMdParser_ = new HtmlToMd();
|
||||
return this.htmlToMdParser_;
|
||||
}
|
||||
|
||||
async requestNoteToNote(requestNote) {
|
||||
const output = {
|
||||
title: requestNote.title ? requestNote.title : '',
|
||||
body: requestNote.body ? requestNote.body : '',
|
||||
};
|
||||
|
||||
if (requestNote.body_html) {
|
||||
// Parsing will not work if the HTML is not wrapped in a top level tag, which is not guaranteed
|
||||
// when getting the content from elsewhere. So here wrap it - it won't change anything to the final
|
||||
// rendering but it makes sure everything will be parsed.
|
||||
output.body = await this.htmlToMdParser().parse('<div>' + requestNote.body_html + '</div>', {
|
||||
baseUrl: requestNote.base_url ? requestNote.base_url : '',
|
||||
});
|
||||
}
|
||||
|
||||
if (requestNote.parent_id) {
|
||||
output.parent_id = requestNote.parent_id;
|
||||
} else {
|
||||
const folder = await Folder.defaultFolder();
|
||||
if (!folder) throw new Error('Cannot find folder for note');
|
||||
output.parent_id = folder.id;
|
||||
}
|
||||
|
||||
if (requestNote.source_url) output.source_url = requestNote.source_url;
|
||||
if (requestNote.author) output.author = requestNote.author;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
// Note must have been saved first
|
||||
async attachImageFromDataUrl_(note, imageDataUrl, cropRect) {
|
||||
const tempDir = Setting.value('tempDir');
|
||||
const mime = mimeUtils.fromDataUrl(imageDataUrl);
|
||||
let ext = mimeUtils.toFileExtension(mime) || '';
|
||||
if (ext) ext = '.' + ext;
|
||||
const tempFilePath = tempDir + '/' + md5(Math.random() + '_' + Date.now()) + ext;
|
||||
const imageConvOptions = {};
|
||||
if (cropRect) imageConvOptions.cropRect = cropRect;
|
||||
await shim.imageFromDataUrl(imageDataUrl, tempFilePath, imageConvOptions);
|
||||
return await shim.attachFileToNote(note, tempFilePath);
|
||||
}
|
||||
|
||||
async downloadImage_(url) {
|
||||
const tempDir = Setting.value('tempDir');
|
||||
|
||||
const isDataUrl = url && url.toLowerCase().indexOf('data:') === 0;
|
||||
|
||||
const name = isDataUrl ? md5(Math.random() + '_' + Date.now()) : filename(url);
|
||||
let fileExt = isDataUrl ? mimeUtils.toFileExtension(mimeUtils.fromDataUrl(url)) : safeFileExtension(fileExtension(url).toLowerCase());
|
||||
if (fileExt) fileExt = '.' + fileExt;
|
||||
let imagePath = tempDir + '/' + safeFilename(name) + fileExt;
|
||||
if (await shim.fsDriver().exists(imagePath)) imagePath = tempDir + '/' + safeFilename(name) + '_' + md5(Math.random() + '_' + Date.now()).substr(0,10) + fileExt;
|
||||
|
||||
try {
|
||||
if (isDataUrl) {
|
||||
await shim.imageFromDataUrl(url, imagePath);
|
||||
} else {
|
||||
await shim.fetchBlob(url, { path: imagePath });
|
||||
}
|
||||
return imagePath;
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot download image at ' + url, error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async downloadImages_(urls) {
|
||||
const PromisePool = require('es6-promise-pool')
|
||||
|
||||
const output = {};
|
||||
|
||||
let urlIndex = 0;
|
||||
const promiseProducer = () => {
|
||||
if (urlIndex >= urls.length) return null;
|
||||
|
||||
const url = urls[urlIndex++];
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const imagePath = await this.downloadImage_(url);
|
||||
if (imagePath) output[url] = { path: imagePath };
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
const concurrency = 3
|
||||
const pool = new PromisePool(promiseProducer, concurrency)
|
||||
await pool.start()
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async createResourcesFromPaths_(urls) {
|
||||
for (let url in urls) {
|
||||
if (!urls.hasOwnProperty(url)) continue;
|
||||
const urlInfo = urls[url];
|
||||
try {
|
||||
const resource = await shim.createResourceFromPath(urlInfo.path);
|
||||
urlInfo.resource = resource;
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot create resource for ' + url, error);
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
async removeTempFiles_(urls) {
|
||||
for (let url in urls) {
|
||||
if (!urls.hasOwnProperty(url)) continue;
|
||||
const urlInfo = urls[url];
|
||||
try {
|
||||
await shim.fsDriver().remove(urlInfo.path);
|
||||
} catch (error) {
|
||||
this.logger().warn('Cannot remove ' + urlInfo.path, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
replaceImageUrlsByResources_(md, urls) {
|
||||
let output = md.replace(/(!\[.*?\]\()([^\s\)]+)(.*?\))/g, (match, before, imageUrl, after) => {
|
||||
const urlInfo = urls[imageUrl];
|
||||
if (!urlInfo || !urlInfo.resource) return before + imageUrl + after;
|
||||
const resourceUrl = Resource.internalUrl(urlInfo.resource);
|
||||
return before + resourceUrl + after;
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = Api;
|
@@ -173,16 +173,30 @@ function shimInit() {
|
||||
if (shim.isElectron()) {
|
||||
const nativeImage = require('electron').nativeImage;
|
||||
let image = nativeImage.createFromDataURL(imageDataUrl);
|
||||
if (options.cropRect) image = image.crop(options.cropRect);
|
||||
if (image.isEmpty()) throw new Error('Could not convert data URL to image'); // Would throw for example if the image format is no supported (eg. image/gif)
|
||||
if (options.cropRect) {
|
||||
// Crop rectangle values need to be rounded or the crop() call will fail
|
||||
const c = options.cropRect;
|
||||
if ('x' in c) c.x = Math.round(c.x);
|
||||
if ('y' in c) c.y = Math.round(c.y);
|
||||
if ('width' in c) c.width = Math.round(c.width);
|
||||
if ('height' in c) c.height = Math.round(c.height);
|
||||
image = image.crop(c);
|
||||
}
|
||||
const mime = mimeUtils.fromDataUrl(imageDataUrl);
|
||||
await shim.writeImageToFile(image, mime, filePath);
|
||||
} else {
|
||||
throw new Error('Node support not implemented');
|
||||
if (options.cropRect) throw new Error('Crop rect not supported in Node');
|
||||
|
||||
const imageDataURI = require('image-data-uri');
|
||||
const result = imageDataURI.decode(imageDataUrl);
|
||||
await shim.fsDriver().writeFile(filePath, result.dataBuffer, 'buffer');
|
||||
}
|
||||
}
|
||||
|
||||
const nodeFetch = require('node-fetch');
|
||||
|
||||
// Not used??
|
||||
shim.readLocalFileBase64 = (path) => {
|
||||
const data = fs.readFileSync(path);
|
||||
return new Buffer(data).toString('base64');
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -147,6 +147,11 @@ function historyCanGoBackTo(route, nextRoute) {
|
||||
if (route.routeName === 'Note') return false;
|
||||
if (route.routeName === 'Folder') return false;
|
||||
|
||||
// There's no point going back to these screens in general and, at least in OneDrive case,
|
||||
// it can be buggy to do so, due to incorrectly relying on global state (reg.syncTarget...)
|
||||
if (route.routeName === 'OneDriveLogin') return false;
|
||||
if (route.routeName === 'DropboxLogin') return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -250,6 +250,41 @@
|
||||
</ul>
|
||||
</div>
|
||||
<h1 id="joplin-changelog">Joplin changelog</h1>
|
||||
<h2 id="-v1-0-109-https-github-com-laurent22-joplin-releases-tag-v1-0-109-2018-09-27t18-01-41z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.109">v1.0.109</a> - 2018-09-27T18:01:41Z</h2>
|
||||
<ul>
|
||||
<li>New: Allow loading image resources in IMG html tags. For example, this is now possible: <code><img src=":/a92ac34387ff467a8c839d201dcd39aa" width="50"/></code></li>
|
||||
<li>Security: Fixed security issue by enabling contextIsolation and proxying IPC messages via preload script. Thank you Yaroslav Lobachevski for discovering the issue.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/801">#801</a>: Replaced freegeoip which is no longer free with ip-api to enable again geo-location for notes.</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/802">#802</a>: Scale note text correctly when using zoom</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/805">#805</a>: Fixed app freezing when opening note in external editor and then creating new note</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/809">#809</a>: Saves full URL with note, including query parameters</li>
|
||||
<li>Clipper: Resolves <a href="https://github.com/laurent22/joplin/issues/681">#681</a>: Allow adding tags from Web Clipper</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/672">#672</a>: Make sure selected notebook is saved and restored correctly</li>
|
||||
<li>Clipper: Fixes <a href="https://github.com/laurent22/joplin/issues/817">#817</a>: Added support for PICTURE tags, which will fix issues with certain pages from which images were not being imported</li>
|
||||
<li>Clipper: Fixed importing certain images with sources that contain brackets</li>
|
||||
<li>Improved: Mostly an invisible change at this point, but the REST API has been refactored to allow adding more calls and to support third-party applications.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-107-https-github-com-laurent22-joplin-releases-tag-v1-0-107-2018-09-16t19-51-07z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.107">v1.0.107</a> - 2018-09-16T19:51:07Z</h2>
|
||||
<ul>
|
||||
<li>New: Resolves <a href="https://github.com/laurent22/joplin/issues/755">#755</a>: Added note properties dialog box to view and edit created time, updated time, source URL and geolocation</li>
|
||||
<li>Added Dutch (Netherlands) translation</li>
|
||||
<li>Added Romanian translation</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/718">#718</a>: Allow recursively importing Markdown folder</li>
|
||||
<li>Fix <a href="https://github.com/laurent22/joplin/issues/764">#764</a>: Fix equation tag positioning</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/710">#710</a>: Don't unwatch file when it is temporarily deleted</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/781">#781</a>: Allow creating notebooks with duplicate titles to allow two notebooks with same name to exist under different parents</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/799">#799</a>: Handle restricted_content error for Dropbox (skip files that cannot be uploaded to copyright or other Dropbox t&c violation)</li>
|
||||
<li>Provided script to install on Ubuntu (with icon)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-106-https-github-com-laurent22-joplin-releases-tag-v1-0-106-2018-09-08t15-23-40z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.106">v1.0.106</a> - 2018-09-08T15:23:40Z</h2>
|
||||
<p>Note: this release is no longer signed to avoid issues with renewing certificates. If you get a warning or the application cannot be installed, please report on the forum on GitHub.</p>
|
||||
<ul>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/761">#761</a>: Highlight single tick code segments</li>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/714">#714</a>: Allow starting application minimised in the tray icon</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/759">#759</a>: Add border around code block when exporting to PDF</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/697">#697</a>: Focus search text input after clearing search</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/709">#709</a>: Now that HTML is supported in notes, remove BR tag replacement hack to fix newline issues.</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-105-https-github-com-laurent22-joplin-releases-tag-v1-0-105-2018-09-05t11-29-36z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.105">v1.0.105</a> - 2018-09-05T11:29:36Z</h2>
|
||||
<ul>
|
||||
<li>Resolves <a href="https://github.com/laurent22/joplin/issues/679">#679</a>: Drag a note on a tag to associate the tag.</li>
|
||||
@@ -477,35 +512,6 @@
|
||||
<li>Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken.</li>
|
||||
<li>Improved WebDAV driver compatibility with some services (eg. Seafile)</li>
|
||||
</ul>
|
||||
<h2 id="-v1-0-62-https-github-com-laurent22-joplin-releases-tag-v1-0-62-2018-02-12t20-19-58z"><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.62">v1.0.62</a> - 2018-02-12T20:19:58Z</h2>
|
||||
<ul>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/205">#205</a>: Importing Evernote notes while on import page re-imports previous import</li>
|
||||
<li>Fixes <a href="https://github.com/laurent22/joplin/issues/209">#209</a>: Items with non-ASCII characters end up truncated on Nextcloud</li>
|
||||
<li>Added Basque translation, fixed issue with handling invalid translations. Updated translation FR.</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-61-https-github-com-laurent22-joplin-releases-tag-v0-10-61-2018-02-08t18-27-39z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.61">v0.10.61</a> - 2018-02-08T18:27:39Z</h2>
|
||||
<ul>
|
||||
<li>New: Display message when creating new note or to-do so that it doesn't look like the previous note content got deleted.</li>
|
||||
<li>New: Also support $ as delimiter for Katex expressions</li>
|
||||
<li>New: Added sync config check to config screens</li>
|
||||
<li>New: Allowing opening and saving resource images</li>
|
||||
<li>New: Toolbar button to set tags</li>
|
||||
<li>Update: Improved request repeating mechanism</li>
|
||||
<li>Fix: Make sure alarms and resources are attached to right note when creating new note</li>
|
||||
<li>Fix: Use mutex when saving model to avoid race conditions when decrypting and syncing at the same time</li>
|
||||
</ul>
|
||||
<h2 id="-v0-10-60-https-github-com-laurent22-joplin-releases-tag-v0-10-60-2018-02-06t13-09-56z"><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.60">v0.10.60</a> - 2018-02-06T13:09:56Z</h2>
|
||||
<ul>
|
||||
<li>New: WebDAV synchronisation target</li>
|
||||
<li>New: Support for math typesetting <a href="https://khan.github.io/KaTeX/">Katex</a></li>
|
||||
<li>New: Tray icon for Windows and macOS</li>
|
||||
<li>Fixed: Don't allow adding notes to conflict notebook</li>
|
||||
<li>Updated: Russian translation</li>
|
||||
<li>Updated: French translation</li>
|
||||
<li>New: List missing master keys in encryption screen</li>
|
||||
<li>Fixed: Attaching images in Linux was no longer working</li>
|
||||
<li>Fixed crash in macOS</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
function stickyHeader() {
|
||||
|
@@ -339,6 +339,10 @@ for (let portToTest = 41184; portToTest <= 41194; portToTest++) {
|
||||
<td>If <code>body_html</code> is provided and contains relative URLs, provide the <code>base_url</code> parameter too so that all the URLs can be converted to absolute ones. The base URL is basically where the HTML was fetched from, minus the query (everything after the '?'). For example if the original page was <code>https://stackoverflow.com/search?q=%5Bjava%5D+test</code>, the base URL is <code>https://stackoverflow.com/search</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>source_url</td>
|
||||
<td>The <em>full URL</em> of the page.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>image_data_url</td>
|
||||
<td>An image to attach to the note, in <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs">Data URL</a> format.</td>
|
||||
</tr>
|
||||
|
@@ -260,7 +260,7 @@
|
||||
<li>On your WebDAV service, copy all the Joplin files from the old location to the new one. Make sure to also copy the <code>.resource</code> directory as it contains your images and other attachments.</li>
|
||||
<li>Once it's done, open Joplin again and change the WebDAV URL.</li>
|
||||
<li>Synchronise to verify that everything is working.</li>
|
||||
<li>Do step 4 and 5 for all the other Joplin clients you need to sync.</li>
|
||||
<li>Do step 5 and 6 for all the other Joplin clients you need to sync.</li>
|
||||
</ol>
|
||||
<h1 id="how-can-i-easily-enter-markdown-tags-in-android-">How can I easily enter Markdown tags in Android?</h1>
|
||||
<p>You may use a special keyboard such as <a href="https://play.google.com/store/apps/details?id=kl.ime.oh&hl=en">Multiling O Keyboard</a>, which has shortcuts to create Markdown tags. <a href="https://discourse.joplin.cozic.net/t/android-create-new-list-item-with-enter/585/2?u=laurent">More information in this post</a>.</p>
|
||||
@@ -270,6 +270,20 @@
|
||||
<p>Short answer: no. The end to end encryption that Joplin implements is to protect the data during transmission and on the cloud service so that only you can access it.</p>
|
||||
<p>On the local device it is assumed that the data is safe due to the OS built-in security features. If additional security is needed it's always possible to put the notes on an encrypted Truecrypt drive for instance.</p>
|
||||
<p>If someone that you don't trust has access to the computer, they can put a keylogger anyway so any local encryption or PIN access would not be useful.</p>
|
||||
<h1 id="webdav-synchronisation-is-not-working">WebDAV synchronisation is not working</h1>
|
||||
<h2 id="-forbidden-error-in-strato">"Forbidden" error in Strato</h2>
|
||||
<p>For example:</p>
|
||||
<pre><code>MKCOL .sync/: Unknown error 2 (403): <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<html><head>
|
||||
<title>403 Forbidden</title>
|
||||
</head><body>
|
||||
<h1>Forbidden</h1>
|
||||
<p>You don't have permission to access /.sync/
|
||||
on this server.</p>
|
||||
</body></html>
|
||||
</code></pre><p>In this case, <a href="https://github.com/laurent22/joplin/issues/309">make sure you enter the correct WebDAV URL</a>.</p>
|
||||
<h2 id="nginx-sync-not-working">Nginx sync not working</h2>
|
||||
<p>As of now, Joplin is not compatible with the Nginx WebDAV server: <a href="https://github.com/laurent22/joplin/issues/808">https://github.com/laurent22/joplin/issues/808</a></p>
|
||||
<h1 id="why-is-it-named-joplin-">Why is it named Joplin?</h1>
|
||||
<p>The name comes from the composer and pianist <a href="https://en.wikipedia.org/wiki/Scott_Joplin">Scott Joplin</a>, which I often listen to. His name is also easy to remember and type so it fell like a good choice. And, to quote a user on Hacker News, "though Scott Joplin's ragtime musical style has a lot in common with some very informal music, his own approach was more educated, sophisticated, and precise. Every note was in its place for a reason, and he was known to prefer his pieces to be performed exactly as written. So you could say that compared to the people who came before him, his notes were more organized".</p>
|
||||
|
||||
|
@@ -270,17 +270,17 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Windows (32 and 64-bit)</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/Joplin-Setup-1.0.105.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a></td>
|
||||
<td>or Get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/JoplinPortable.exe'>Portable version</a><br>(to run from a USB key, etc.)</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.109/Joplin-Setup-1.0.109.exe'><img alt='Get it on Windows' height="40px" src='https://joplin.cozic.net/images/BadgeWindows.png'/></a></td>
|
||||
<td>or Get the <a href='https://github.com/laurent22/joplin/releases/download/v1.0.109/JoplinPortable.exe'>Portable version</a><br>(to run from a USB key, etc.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/Joplin-1.0.105.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.107/Joplin-1.0.107.dmg'><img alt='Get it on macOS' height="40px" src='https://joplin.cozic.net/images/BadgeMacOS.png'/></a></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux</td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.105/Joplin-1.0.105-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a></td>
|
||||
<td><a href='https://github.com/laurent22/joplin/releases/download/v1.0.109/Joplin-1.0.109-x86_64.AppImage'><img alt='Get it on Linux' height="40px" src='https://joplin.cozic.net/images/BadgeLinux.png'/></a></td>
|
||||
<td>An Arch Linux package<br><a href="#terminal-application">is also available</a>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -302,7 +302,7 @@
|
||||
<tr>
|
||||
<td>Android</td>
|
||||
<td><a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplin.cozic.net/images/BadgeAndroid.png'/></a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.131/joplin-v1.0.131.apk">Download APK File</a></td>
|
||||
<td>or <a href="https://github.com/laurent22/joplin-android/releases/download/android-v1.0.133/joplin-v1.0.133.apk">Download APK File</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>iOS</td>
|
||||
@@ -469,8 +469,9 @@ $$
|
||||
- [ ] Eggs
|
||||
</code></pre><p>The checkboxes can then be ticked in the mobile and desktop applications.</p>
|
||||
<h2 id="html-support">HTML support</h2>
|
||||
<p>Only the <code><br></code> tag is supported - it can be used to force a new line, which is convenient to insert new lines inside table cells. For security reasons, other HTML tags are not supported.</p>
|
||||
<h1 id="donations">Donations</h1>
|
||||
<p>It is generally recommended to enter the notes as Markdown as it makes the notes easier to edit. However for cases where certain features aren't supported (such as strikethrough or to highlight text), you can also use HTML code directly. For example this would be a valid note:</p>
|
||||
<pre><code>This is <s>strikethrough text</s> mixed with regular **Markdown**.
|
||||
</code></pre><h1 id="donations">Donations</h1>
|
||||
<p>Donations to Joplin support the development of the project. Developing quality applications mostly takes time, but there are also some expenses, such as digital certificates to sign the applications, app store fees, hosting, etc. Most of all, your donation will make it possible to keep up the current development standard.</p>
|
||||
<p>Please see the <a href="https://joplin.cozic.net/donate/">donation page</a> for information on how to support the development of Joplin.</p>
|
||||
<h1 id="community">Community</h1>
|
||||
@@ -566,7 +567,7 @@ $$
|
||||
<td>Français</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po">fr_FR</a></td>
|
||||
<td>Laurent Cozic</td>
|
||||
<td>98%</td>
|
||||
<td>100%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplin.cozic.net/images/flags/es/galicia.png" alt=""></td>
|
||||
@@ -656,8 +657,8 @@ $$
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/jp.png" alt=""></td>
|
||||
<td>日本語</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po">ja_JP</a></td>
|
||||
<td></td>
|
||||
<td>52%</td>
|
||||
<td>AWASHIRO Ikuya (ikunya@gmail.com)</td>
|
||||
<td>99%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><img src="https://joplin.cozic.net/images/flags/country-4x3/kr.png" alt=""></td>
|
||||
|
@@ -260,19 +260,19 @@
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Total Windows downloads</td>
|
||||
<td>45960</td>
|
||||
<td>59269</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total macOs downloads</td>
|
||||
<td>20467</td>
|
||||
<td>24054</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Total Linux downloads</td>
|
||||
<td>20984</td>
|
||||
<td>22610</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Windows %</td>
|
||||
<td>53%</td>
|
||||
<td>56%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>macOS %</td>
|
||||
@@ -280,7 +280,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Linux %</td>
|
||||
<td>24%</td>
|
||||
<td>21%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -297,20 +297,44 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.105">v1.0.105</a></td>
|
||||
<td>2018-09-05T11:29:36Z</td>
|
||||
<td>2</td>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.109">v1.0.109</a></td>
|
||||
<td>2018-09-27T18:01:41Z</td>
|
||||
<td>1</td>
|
||||
<td></td>
|
||||
<td>3 </td>
|
||||
<td></td>
|
||||
<td>1 </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.107">v1.0.107</a></td>
|
||||
<td>2018-09-16T19:51:07Z</td>
|
||||
<td>6392</td>
|
||||
<td>1969</td>
|
||||
<td>1686</td>
|
||||
<td>10047</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.106">v1.0.106</a></td>
|
||||
<td>2018-09-08T15:23:40Z</td>
|
||||
<td>4499</td>
|
||||
<td>1435</td>
|
||||
<td>285</td>
|
||||
<td>6219</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.105">v1.0.105</a></td>
|
||||
<td>2018-09-05T11:29:36Z</td>
|
||||
<td>4552</td>
|
||||
<td>1544</td>
|
||||
<td>1426</td>
|
||||
<td>7522</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.104">v1.0.104</a></td>
|
||||
<td>2018-06-28T20:25:36Z</td>
|
||||
<td>14867</td>
|
||||
<td>4642</td>
|
||||
<td>6835</td>
|
||||
<td>26344</td>
|
||||
<td>14943</td>
|
||||
<td>4649</td>
|
||||
<td>6882</td>
|
||||
<td>26474</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.103">v1.0.103</a></td>
|
||||
@@ -332,9 +356,9 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.100">v1.0.100</a></td>
|
||||
<td>2018-06-14T17:41:43Z</td>
|
||||
<td>848</td>
|
||||
<td>404</td>
|
||||
<td>224</td>
|
||||
<td>1476</td>
|
||||
<td>405</td>
|
||||
<td>225</td>
|
||||
<td>1478</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.99">v1.0.99</a></td>
|
||||
@@ -357,8 +381,8 @@
|
||||
<td>2018-05-26T16:36:39Z</td>
|
||||
<td>2671</td>
|
||||
<td>1194</td>
|
||||
<td>1145</td>
|
||||
<td>5010</td>
|
||||
<td>1151</td>
|
||||
<td>5016</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.95">v1.0.95</a></td>
|
||||
@@ -380,9 +404,9 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.93">v1.0.93</a></td>
|
||||
<td>2018-05-14T11:36:01Z</td>
|
||||
<td>1766</td>
|
||||
<td>842</td>
|
||||
<td>737</td>
|
||||
<td>3345</td>
|
||||
<td>845</td>
|
||||
<td>738</td>
|
||||
<td>3349</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.91">v1.0.91</a></td>
|
||||
@@ -411,10 +435,10 @@
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.83">v1.0.83</a></td>
|
||||
<td>2018-04-04T19:43:58Z</td>
|
||||
<td>4431</td>
|
||||
<td>4442</td>
|
||||
<td>2378</td>
|
||||
<td>2627</td>
|
||||
<td>9436</td>
|
||||
<td>9447</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.82">v1.0.82</a></td>
|
||||
@@ -436,9 +460,9 @@
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.79">v1.0.79</a></td>
|
||||
<td>2018-03-23T18:00:11Z</td>
|
||||
<td>919</td>
|
||||
<td>508</td>
|
||||
<td>509</td>
|
||||
<td>353</td>
|
||||
<td>1780</td>
|
||||
<td>1781</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.78">v1.0.78</a></td>
|
||||
@@ -512,30 +536,6 @@
|
||||
<td>83</td>
|
||||
<td>517</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v1.0.62">v1.0.62</a></td>
|
||||
<td>2018-02-12T20:19:58Z</td>
|
||||
<td>547</td>
|
||||
<td>274</td>
|
||||
<td>346</td>
|
||||
<td>1167</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.61">v0.10.61</a></td>
|
||||
<td>2018-02-08T18:27:39Z</td>
|
||||
<td>962</td>
|
||||
<td>603</td>
|
||||
<td>941</td>
|
||||
<td>2506</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://github.com/laurent22/joplin/releases/tag/v0.10.60">v0.10.60</a></td>
|
||||
<td>2018-02-06T13:09:56Z</td>
|
||||
<td>711</td>
|
||||
<td>495</td>
|
||||
<td>539</td>
|
||||
<td>1745</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
@@ -12,7 +12,7 @@ echo " |_| "
|
||||
# Get the latest version to download
|
||||
version=$(curl --silent "https://api.github.com/repos/laurent22/joplin/releases/latest" | grep -Po '"tag_name": "v\K.*?(?=")')
|
||||
# Delete previous version
|
||||
rm ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop
|
||||
rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop
|
||||
# Creates the folder where the binary will be stored
|
||||
mkdir -p ~/.joplin/
|
||||
# Download the latest version
|
||||
|
@@ -1,5 +1,41 @@
|
||||
# Joplin changelog
|
||||
|
||||
## [v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) - 2018-09-27T18:01:41Z
|
||||
|
||||
- New: Allow loading image resources in IMG html tags. For example, this is now possible: `<img src=":/a92ac34387ff467a8c839d201dcd39aa" width="50"/>`
|
||||
- Security: Fixed security issue by enabling contextIsolation and proxying IPC messages via preload script. Thank you Yaroslav Lobachevski for discovering the issue.
|
||||
- Fixes [#801](https://github.com/laurent22/joplin/issues/801): Replaced freegeoip which is no longer free with ip-api to enable again geo-location for notes.
|
||||
- Fixes [#802](https://github.com/laurent22/joplin/issues/802): Scale note text correctly when using zoom
|
||||
- Fixes [#805](https://github.com/laurent22/joplin/issues/805): Fixed app freezing when opening note in external editor and then creating new note
|
||||
- Clipper: Fixes [#809](https://github.com/laurent22/joplin/issues/809): Saves full URL with note, including query parameters
|
||||
- Clipper: Resolves [#681](https://github.com/laurent22/joplin/issues/681): Allow adding tags from Web Clipper
|
||||
- Clipper: Fixes [#672](https://github.com/laurent22/joplin/issues/672): Make sure selected notebook is saved and restored correctly
|
||||
- Clipper: Fixes [#817](https://github.com/laurent22/joplin/issues/817): Added support for PICTURE tags, which will fix issues with certain pages from which images were not being imported
|
||||
- Clipper: Fixed importing certain images with sources that contain brackets
|
||||
- Improved: Mostly an invisible change at this point, but the REST API has been refactored to allow adding more calls and to support third-party applications.
|
||||
|
||||
## [v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) - 2018-09-16T19:51:07Z
|
||||
|
||||
- New: Resolves [#755](https://github.com/laurent22/joplin/issues/755): Added note properties dialog box to view and edit created time, updated time, source URL and geolocation
|
||||
- Added Dutch (Netherlands) translation
|
||||
- Added Romanian translation
|
||||
- Fixes [#718](https://github.com/laurent22/joplin/issues/718): Allow recursively importing Markdown folder
|
||||
- Fix [#764](https://github.com/laurent22/joplin/issues/764): Fix equation tag positioning
|
||||
- Fixes [#710](https://github.com/laurent22/joplin/issues/710): Don't unwatch file when it is temporarily deleted
|
||||
- Resolves [#781](https://github.com/laurent22/joplin/issues/781): Allow creating notebooks with duplicate titles to allow two notebooks with same name to exist under different parents
|
||||
- Fixes [#799](https://github.com/laurent22/joplin/issues/799): Handle restricted_content error for Dropbox (skip files that cannot be uploaded to copyright or other Dropbox t&c violation)
|
||||
- Provided script to install on Ubuntu (with icon)
|
||||
|
||||
## [v1.0.106](https://github.com/laurent22/joplin/releases/tag/v1.0.106) - 2018-09-08T15:23:40Z
|
||||
|
||||
Note: this release is no longer signed to avoid issues with renewing certificates. If you get a warning or the application cannot be installed, please report on the forum on GitHub.
|
||||
|
||||
- Resolves [#761](https://github.com/laurent22/joplin/issues/761): Highlight single tick code segments
|
||||
- Resolves [#714](https://github.com/laurent22/joplin/issues/714): Allow starting application minimised in the tray icon
|
||||
- Fixes [#759](https://github.com/laurent22/joplin/issues/759): Add border around code block when exporting to PDF
|
||||
- Fixes [#697](https://github.com/laurent22/joplin/issues/697): Focus search text input after clearing search
|
||||
- Fixes [#709](https://github.com/laurent22/joplin/issues/709): Now that HTML is supported in notes, remove BR tag replacement hack to fix newline issues.
|
||||
|
||||
## [v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) - 2018-09-05T11:29:36Z
|
||||
|
||||
- Resolves [#679](https://github.com/laurent22/joplin/issues/679): Drag a note on a tag to associate the tag.
|
||||
@@ -235,33 +271,4 @@ IMPORTANT: If you use Nextcloud it is recommended to sync all your notes before
|
||||
## [v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) - 2018-02-14T19:40:36Z
|
||||
|
||||
- Improved the way settings are changed. Should also fixed issue with sync context being accidentally broken.
|
||||
- Improved WebDAV driver compatibility with some services (eg. Seafile)
|
||||
|
||||
## [v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) - 2018-02-12T20:19:58Z
|
||||
|
||||
- Fixes [#205](https://github.com/laurent22/joplin/issues/205): Importing Evernote notes while on import page re-imports previous import
|
||||
- Fixes [#209](https://github.com/laurent22/joplin/issues/209): Items with non-ASCII characters end up truncated on Nextcloud
|
||||
- Added Basque translation, fixed issue with handling invalid translations. Updated translation FR.
|
||||
|
||||
## [v0.10.61](https://github.com/laurent22/joplin/releases/tag/v0.10.61) - 2018-02-08T18:27:39Z
|
||||
|
||||
- New: Display message when creating new note or to-do so that it doesn't look like the previous note content got deleted.
|
||||
- New: Also support $ as delimiter for Katex expressions
|
||||
- New: Added sync config check to config screens
|
||||
- New: Allowing opening and saving resource images
|
||||
- New: Toolbar button to set tags
|
||||
- Update: Improved request repeating mechanism
|
||||
- Fix: Make sure alarms and resources are attached to right note when creating new note
|
||||
- Fix: Use mutex when saving model to avoid race conditions when decrypting and syncing at the same time
|
||||
|
||||
## [v0.10.60](https://github.com/laurent22/joplin/releases/tag/v0.10.60) - 2018-02-06T13:09:56Z
|
||||
|
||||
- New: WebDAV synchronisation target
|
||||
- New: Support for math typesetting [Katex](https://khan.github.io/KaTeX/)
|
||||
- New: Tray icon for Windows and macOS
|
||||
- Fixed: Don't allow adding notes to conflict notebook
|
||||
- Updated: Russian translation
|
||||
- Updated: French translation
|
||||
- New: List missing master keys in encryption screen
|
||||
- Fixed: Attaching images in Linux was no longer working
|
||||
- Fixed crash in macOS
|
||||
- Improved WebDAV driver compatibility with some services (eg. Seafile)
|
@@ -87,6 +87,7 @@ source_url | The URL the note comes from
|
||||
author | The note author
|
||||
parent_id | The notebook (ID) to move the note to
|
||||
base_url | If `body_html` is provided and contains relative URLs, provide the `base_url` parameter too so that all the URLs can be converted to absolute ones. The base URL is basically where the HTML was fetched from, minus the query (everything after the '?'). For example if the original page was `https://stackoverflow.com/search?q=%5Bjava%5D+test`, the base URL is `https://stackoverflow.com/search`.
|
||||
source_url | The *full URL* of the page.
|
||||
image_data_url | An image to attach to the note, in [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) format.
|
||||
crop_rect | If an image is provided, you can also specify an optional rectangle that will be used to crop the image. In format `{ x: x, y: y, width: width, height: height }`
|
||||
|
||||
|
@@ -6,13 +6,13 @@ It seems to be due to the setting `set term=ansi` in .vimrc. Removing it should
|
||||
|
||||
When changing the WebDAV URL, make sure that the new location has the same exact content as the old location (i.e. copy all the Joplin data over to the new location). Otherwise, if there's nothing on the new location, Joplin is going to think that you have deleted all your data and will proceed to delete it locally too. So to change the WebDAV URL, please follow these steps:
|
||||
|
||||
0. Make a backup of your Joplin data in case something goes wrong. Export to a JEX archive for example.
|
||||
1. Synchronise one last time all your data from a Joplin client (for example, from the desktop client)
|
||||
2. Close the Joplin client.
|
||||
3. On your WebDAV service, copy all the Joplin files from the old location to the new one. Make sure to also copy the `.resource` directory as it contains your images and other attachments.
|
||||
4. Once it's done, open Joplin again and change the WebDAV URL.
|
||||
5. Synchronise to verify that everything is working.
|
||||
6. Do step 4 and 5 for all the other Joplin clients you need to sync.
|
||||
1. Make a backup of your Joplin data in case something goes wrong. Export to a JEX archive for example.
|
||||
2. Synchronise one last time all your data from a Joplin client (for example, from the desktop client)
|
||||
3. Close the Joplin client.
|
||||
4. On your WebDAV service, copy all the Joplin files from the old location to the new one. Make sure to also copy the `.resource` directory as it contains your images and other attachments.
|
||||
5. Once it's done, open Joplin again and change the WebDAV URL.
|
||||
6. Synchronise to verify that everything is working.
|
||||
7. Do step 5 and 6 for all the other Joplin clients you need to sync.
|
||||
|
||||
# How can I easily enter Markdown tags in Android?
|
||||
|
||||
@@ -30,6 +30,27 @@ On the local device it is assumed that the data is safe due to the OS built-in s
|
||||
|
||||
If someone that you don't trust has access to the computer, they can put a keylogger anyway so any local encryption or PIN access would not be useful.
|
||||
|
||||
# WebDAV synchronisation is not working
|
||||
|
||||
## "Forbidden" error in Strato
|
||||
|
||||
For example:
|
||||
|
||||
MKCOL .sync/: Unknown error 2 (403): <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
|
||||
<html><head>
|
||||
<title>403 Forbidden</title>
|
||||
</head><body>
|
||||
<h1>Forbidden</h1>
|
||||
<p>You don't have permission to access /.sync/
|
||||
on this server.</p>
|
||||
</body></html>
|
||||
|
||||
In this case, [make sure you enter the correct WebDAV URL](https://github.com/laurent22/joplin/issues/309).
|
||||
|
||||
## Nginx sync not working
|
||||
|
||||
As of now, Joplin is not compatible with the Nginx WebDAV server: https://github.com/laurent22/joplin/issues/808
|
||||
|
||||
# Why is it named Joplin?
|
||||
|
||||
The name comes from the composer and pianist [Scott Joplin](https://en.wikipedia.org/wiki/Scott_Joplin), which I often listen to. His name is also easy to remember and type so it fell like a good choice. And, to quote a user on Hacker News, "though Scott Joplin's ragtime musical style has a lot in common with some very informal music, his own approach was more educated, sophisticated, and precise. Every note was in its place for a reason, and he was known to prefer his pieces to be performed exactly as written. So you could say that compared to the people who came before him, his notes were more organized".
|
||||
The name comes from the composer and pianist [Scott Joplin](https://en.wikipedia.org/wiki/Scott_Joplin), which I often listen to. His name is also easy to remember and type so it fell like a good choice. And, to quote a user on Hacker News, "though Scott Joplin's ragtime musical style has a lot in common with some very informal music, his own approach was more educated, sophisticated, and precise. Every note was in its place for a reason, and he was known to prefer his pieces to be performed exactly as written. So you could say that compared to the people who came before him, his notes were more organized".
|
@@ -2,33 +2,36 @@
|
||||
|
||||
Name | Value
|
||||
--- | ---
|
||||
Total Windows downloads | 45960
|
||||
Total macOs downloads | 20467
|
||||
Total Linux downloads | 20984
|
||||
Windows % | 53%
|
||||
Total Windows downloads | 59269
|
||||
Total macOs downloads | 24054
|
||||
Total Linux downloads | 22610
|
||||
Windows % | 56%
|
||||
macOS % | 23%
|
||||
Linux % | 24%
|
||||
Linux % | 21%
|
||||
|
||||
Version | Date | Windows | macOS | Linux | Total
|
||||
--- | --- | --- | --- | --- | ---
|
||||
[v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) | 2018-09-05T11:29:36Z | 2 | 1 | | 3
|
||||
[v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 14867 | 4642 | 6835 | 26344
|
||||
[v1.0.109](https://github.com/laurent22/joplin/releases/tag/v1.0.109) | 2018-09-27T18:01:41Z | 1 | | | 1
|
||||
[v1.0.107](https://github.com/laurent22/joplin/releases/tag/v1.0.107) | 2018-09-16T19:51:07Z | 6392 | 1969 | 1686 | 10047
|
||||
[v1.0.106](https://github.com/laurent22/joplin/releases/tag/v1.0.106) | 2018-09-08T15:23:40Z | 4499 | 1435 | 285 | 6219
|
||||
[v1.0.105](https://github.com/laurent22/joplin/releases/tag/v1.0.105) | 2018-09-05T11:29:36Z | 4552 | 1544 | 1426 | 7522
|
||||
[v1.0.104](https://github.com/laurent22/joplin/releases/tag/v1.0.104) | 2018-06-28T20:25:36Z | 14943 | 4649 | 6882 | 26474
|
||||
[v1.0.103](https://github.com/laurent22/joplin/releases/tag/v1.0.103) | 2018-06-21T19:38:13Z | 2001 | 852 | 664 | 3517
|
||||
[v1.0.101](https://github.com/laurent22/joplin/releases/tag/v1.0.101) | 2018-06-17T18:35:11Z | 1283 | 577 | 397 | 2257
|
||||
[v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 848 | 404 | 224 | 1476
|
||||
[v1.0.100](https://github.com/laurent22/joplin/releases/tag/v1.0.100) | 2018-06-14T17:41:43Z | 848 | 405 | 225 | 1478
|
||||
[v1.0.99](https://github.com/laurent22/joplin/releases/tag/v1.0.99) | 2018-06-10T13:18:23Z | 1228 | 575 | 370 | 2173
|
||||
[v1.0.97](https://github.com/laurent22/joplin/releases/tag/v1.0.97) | 2018-06-09T19:23:34Z | 290 | 131 | 51 | 472
|
||||
[v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2671 | 1194 | 1145 | 5010
|
||||
[v1.0.96](https://github.com/laurent22/joplin/releases/tag/v1.0.96) | 2018-05-26T16:36:39Z | 2671 | 1194 | 1151 | 5016
|
||||
[v1.0.95](https://github.com/laurent22/joplin/releases/tag/v1.0.95) | 2018-05-25T13:04:30Z | 381 | 186 | 81 | 648
|
||||
[v1.0.94](https://github.com/laurent22/joplin/releases/tag/v1.0.94) | 2018-05-21T20:52:59Z | 1094 | 553 | 354 | 2001
|
||||
[v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1766 | 842 | 737 | 3345
|
||||
[v1.0.93](https://github.com/laurent22/joplin/releases/tag/v1.0.93) | 2018-05-14T11:36:01Z | 1766 | 845 | 738 | 3349
|
||||
[v1.0.91](https://github.com/laurent22/joplin/releases/tag/v1.0.91) | 2018-05-10T14:48:04Z | 812 | 531 | 286 | 1629
|
||||
[v1.0.89](https://github.com/laurent22/joplin/releases/tag/v1.0.89) | 2018-05-09T13:05:05Z | 469 | 206 | 90 | 765
|
||||
[v1.0.85](https://github.com/laurent22/joplin/releases/tag/v1.0.85) | 2018-05-01T21:08:24Z | 1638 | 929 | 607 | 3174
|
||||
[v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 4431 | 2378 | 2627 | 9436
|
||||
[v1.0.83](https://github.com/laurent22/joplin/releases/tag/v1.0.83) | 2018-04-04T19:43:58Z | 4442 | 2378 | 2627 | 9447
|
||||
[v1.0.82](https://github.com/laurent22/joplin/releases/tag/v1.0.82) | 2018-03-31T19:16:31Z | 684 | 383 | 92 | 1159
|
||||
[v1.0.81](https://github.com/laurent22/joplin/releases/tag/v1.0.81) | 2018-03-28T08:13:58Z | 984 | 565 | 742 | 2291
|
||||
[v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 919 | 508 | 353 | 1780
|
||||
[v1.0.79](https://github.com/laurent22/joplin/releases/tag/v1.0.79) | 2018-03-23T18:00:11Z | 919 | 509 | 353 | 1781
|
||||
[v1.0.78](https://github.com/laurent22/joplin/releases/tag/v1.0.78) | 2018-03-17T15:27:18Z | 1302 | 836 | 841 | 2979
|
||||
[v1.0.77](https://github.com/laurent22/joplin/releases/tag/v1.0.77) | 2018-03-16T15:12:35Z | 165 | 87 | 25 | 277
|
||||
[v1.0.72](https://github.com/laurent22/joplin/releases/tag/v1.0.72) | 2018-03-14T09:44:35Z | 396 | 232 | 32 | 660
|
||||
@@ -37,7 +40,4 @@ Version | Date | Windows | macOS | Linux | Total
|
||||
[v1.0.66](https://github.com/laurent22/joplin/releases/tag/v1.0.66) | 2018-02-18T23:09:09Z | 313 | 107 | 72 | 492
|
||||
[v1.0.65](https://github.com/laurent22/joplin/releases/tag/v1.0.65) | 2018-02-17T20:02:25Z | 185 | 104 | 114 | 403
|
||||
[v1.0.64](https://github.com/laurent22/joplin/releases/tag/v1.0.64) | 2018-02-16T00:58:20Z | 1074 | 528 | 1116 | 2718
|
||||
[v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 291 | 143 | 83 | 517
|
||||
[v1.0.62](https://github.com/laurent22/joplin/releases/tag/v1.0.62) | 2018-02-12T20:19:58Z | 547 | 274 | 346 | 1167
|
||||
[v0.10.61](https://github.com/laurent22/joplin/releases/tag/v0.10.61) | 2018-02-08T18:27:39Z | 962 | 603 | 941 | 2506
|
||||
[v0.10.60](https://github.com/laurent22/joplin/releases/tag/v0.10.60) | 2018-02-06T13:09:56Z | 711 | 495 | 539 | 1745
|
||||
[v1.0.63](https://github.com/laurent22/joplin/releases/tag/v1.0.63) | 2018-02-14T19:40:36Z | 291 | 143 | 83 | 517
|
Reference in New Issue
Block a user