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

Compare commits

..

65 Commits

Author SHA1 Message Date
Laurent Cozic
01470e8d3b Android release v1.0.141 2018-10-03 08:30:19 +01:00
Laurent Cozic
bda2fe6717 Merge branch 'master' of github.com:laurent22/joplin 2018-10-03 08:19:15 +01:00
Laurent Cozic
d1f4c5be18 Disable non-working ShareExtension on iOS 2018-10-03 08:17:37 +01:00
Laurent Cozic
377adea51d Android release v1.0.140 2018-10-02 18:19:27 +01:00
Laurent Cozic
cda3d20834 Android release v1.0.138 2018-10-02 18:10:37 +01:00
Laurent Cozic
d11870b1eb Trying to get RN 0.57.1 to work 2018-10-02 19:02:38 +00:00
Laurent Cozic
53bda3eea7 Trying RN 0.57.1 upgrade 2018-10-02 18:57:31 +00:00
Laurent Cozic
30165e8d6a Electron: Fixes #798: Enable Select All shortcut in macOS 2018-10-02 17:45:39 +01:00
Laurent Cozic
2202eb6570 Merge branch 'master' of github.com:laurent22/joplin 2018-10-01 20:55:52 +01:00
Laurent Cozic
720927f488 All: Fixes #832: Enex import: Don't add extra line breaks at the beginning of list item when it contains a block element 2018-10-01 20:55:24 +01:00
Helmut K. C. Tessarek
2858c0fce0 CLI v1.0.116 2018-09-30 18:28:54 -04:00
Laurent Cozic
36c3521f40 Update website 2018-09-30 21:15:21 +01:00
Laurent Cozic
98a3b99d17 Electron release v1.0.111 2018-09-30 20:45:03 +01:00
Laurent Cozic
95a06c4531 All: Fixes #818: Use sort options when displayed tagged notes 2018-09-30 20:43:46 +01:00
Laurent Cozic
6ea77b36ce Electron: Resolves #820: Allow dragging and dropping a note in another note to create a link 2018-09-30 20:15:30 +01:00
Laurent Cozic
0cd7ebf9d3 Electron: Fixes resources being incorrectly auto-deleted when inside an IMG tag 2018-09-30 19:24:02 +01:00
Laurent Cozic
a816498fc6 Android release v1.0.135 2018-09-30 10:52:30 +01:00
Laurent Cozic
549c1a6767 Android: Make camera features optional to, maybe, enable Chromebooks 2018-09-30 10:48:10 +01:00
Laurent Cozic
f87d1f11b0 API: Allow downloading a resource data 2018-09-30 10:15:46 +01:00
Laurent Cozic
fb913bc33c Mobile: Fixes #654: Fixed auto-title logic so that set title doesn't get overwritten in some conditions for new notes 2018-09-29 15:57:18 +01:00
Laurent Cozic
53d7a51cb0 Removed API doc from translations and updated FR translation 2018-09-29 13:29:07 +01:00
Laurent Cozic
12da48c756 Electron release v1.0.110 2018-09-29 13:17:48 +01:00
Laurent Cozic
a0a6bdb684 Update website 2018-09-29 13:15:36 +01:00
Laurent Cozic
eb4aa2c026 API: Added more calls 2018-09-29 12:54:44 +01:00
Laurent Cozic
a9e789f845 Clipper: Fixed screenshot text box label colour 2018-09-29 12:53:16 +01:00
Laurent Cozic
89b76918bd Merge branch 'master' of github.com:laurent22/joplin 2018-09-28 21:04:23 +01:00
Laurent Cozic
e98575643c API: Added documentation generator and built documentation 2018-09-28 21:03:28 +01:00
Laurent Cozic
7c9e7743f1 Merge pull request #825 from sensor-freak/patch-2
Fixed some german translations
2018-09-28 20:58:32 +01:00
sensor-freak
435aa4845b Fixed some german translations 2018-09-28 21:43:17 +02:00
Laurent Cozic
9841488ce4 Updated translations 2018-09-28 19:25:56 +01:00
Laurent Cozic
9c907989a5 Merge branch 'master' of github.com:laurent22/joplin 2018-09-28 19:25:18 +01:00
Laurent Cozic
f684d8e59a Merge pull request #824 from sensor-freak/patch-1
Update de_DE.json
2018-09-28 19:25:05 +01:00
Laurent Cozic
a1ad6c9712 API: Added more API calls 2018-09-28 19:24:57 +01:00
Laurent Cozic
b6ca3090df Merge pull request #822 from foxmask/master
Forbidden is 403 - typo :)
2018-09-28 19:23:48 +01:00
Helmut K. C. Tessarek
ff2d793fbb CLI v1.0.115 2018-09-28 14:08:21 -04:00
sensor-freak
fcfb7f1111 Update de_DE.json
Fix and add some german translations
2018-09-28 07:39:13 +02:00
FoxMaSk
6125cde223 Forbidden is 403 - typo :) 2018-09-27 21:38:16 +02:00
Laurent Cozic
c83391e624 Update website 2018-09-27 19:02:01 +01:00
Laurent Cozic
a3a818ea74 Clipper release v1.0.7 2018-09-27 19:00:04 +01:00
Laurent Cozic
54a4965503 Android release v1.0.133 2018-09-27 18:54:14 +01:00
Laurent Cozic
2233d88c01 Electron release v1.0.109 2018-09-27 18:50:38 +01:00
Laurent Cozic
9680ab74a3 All: Allow loading image resources in IMG html tags 2018-09-27 18:42:34 +01:00
Laurent Cozic
ef711af5b5 Api: Added method to get notes 2018-09-27 18:35:10 +00:00
Laurent Cozic
8a619e4b8b All: Refactored REST API to make it testable and to allow further extension 2018-09-27 09:14:05 +01:00
Laurent Cozic
bc09d2c640 Clipper: Fixes #817: Added support for PICTURE tags, which will fix issues with certain pages from which images were not being imported 2018-09-25 21:19:54 +01:00
Laurent Cozic
f82dfde6f4 Clipper: Fixed importing certain images with sources that contain brackets 2018-09-24 20:15:23 +01:00
Laurent Cozic
312c7f2d27 Electron: Fixed regression following security fix - links were being opened inside Joplin instead of in external browser 2018-09-24 20:14:21 +01:00
Laurent Cozic
953cc327c6 Electron: Fixes #805: Fixed app freezing when opening note in external editor and then creating new note 2018-09-24 07:10:00 +01:00
Laurent Cozic
14cff96713 Mobile: Fix potential crash when going back to OneDrive login screen 2018-09-23 20:45:34 +01:00
Laurent Cozic
34b9af2ce0 Electron: Fixes #802: Scale note text correctly when using zoom 2018-09-23 19:48:50 +01:00
Laurent Cozic
6a6ee280c3 All: Fixes #801: Replaced freegeoip which is no longer free with ip-api 2018-09-23 19:45:13 +01:00
Laurent Cozic
861387707a CLI: Fixes #795: Display tree of notebooks correctly 2018-09-23 19:33:44 +01:00
Laurent Cozic
830e665366 Updated translation 2018-09-23 19:15:49 +01:00
Laurent Cozic
f14ae68ea0 Merge pull request #812 from ikunya/update-japo
Update Japanese Translations.
2018-09-23 19:15:16 +01:00
Laurent Cozic
c7084bf27e Updated French translation 2018-09-23 19:15:04 +01:00
Laurent Cozic
fc8ffcbe46 Clipper: Fixes #672: Make sure selected notebook is saved and restored correctly 2018-09-23 18:44:39 +01:00
Laurent Cozic
77f089654e Clipper: Resolves #681: Allow adding tags from Web Clipper 2018-09-23 18:03:11 +01:00
Laurent Cozic
e7a12bb0dd Clipper: Fixes #809: Saves full URL with note, including query parameters 2018-09-22 17:41:09 +01:00
AWASHIRO Ikuya
22fe3a4e44 Update Japanese Translations. 2018-09-22 23:34:07 +09:00
Laurent Cozic
afb8b92528 Clipper: Fixed init sequence 2018-09-22 11:21:39 +01:00
Laurent Cozic
5178f99100 Electron release v1.0.108 2018-09-21 18:20:34 +01:00
Laurent Cozic
72af564382 Electron: Fixed security issue by enabling contextIsolation and proxying IPC messages via preload script 2018-09-21 18:20:06 +01:00
Laurent Cozic
0a2b83998c Merge pull request #811 from mvonmaltitz/master
Fix ubuntu install script
2018-09-21 12:00:54 +01:00
Marcel von Maltitz
73e79213dc Fix ubuntu install script
If no former version is present, the script fails since the rm commands do not succeed.
Added -f to rm in order to ignore non-existent directories to be deleted.
2018-09-21 12:55:01 +02:00
Laurent Cozic
e31ffc9474 Update website 2018-09-16 20:51:28 +01:00
164 changed files with 6389 additions and 2478 deletions

View File

@@ -13,6 +13,7 @@ tests/fuzzing.*
tests/fuzzing -*
tests/logs/*
tests/cli-integration/
tests/tmp/
*.mo
*.*~
tests/sync

View File

@@ -0,0 +1,276 @@
const { BaseCommand } = require('./base-command.js');
const { _ } = require('lib/locale.js');
const { cliUtils } = require('./cli-utils.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const MasterKey = require('lib/models/MasterKey');
const BaseItem = require('lib/models/BaseItem');
const BaseModel = require('lib/BaseModel');
const Setting = require('lib/models/Setting.js');
const { toTitleCase } = require('lib/string-utils.js');
const { reg } = require('lib/registry.js');
const markdownUtils = require('lib/markdownUtils');
const { Database } = require('lib/database.js');
class Command extends BaseCommand {
usage() {
return 'apidoc';
}
description() {
return 'Build the API doc';
}
createPropertiesTable(tableFields) {
const headers = [
{ name: 'name', label: 'Name' },
{ name: 'type', label: 'Type', filter: (value) => {
return Database.enumName('fieldType', value);
}},
{ name: 'description', label: 'Description' },
];
return markdownUtils.createMarkdownTable(headers, tableFields);
}
async action(args) {
const models = [
{
type: BaseModel.TYPE_NOTE,
},
{
type: BaseModel.TYPE_FOLDER,
},
{
type: BaseModel.TYPE_RESOURCE,
},
{
type: BaseModel.TYPE_TAG,
},
];
const lines = [];
lines.push('# Joplin API');
lines.push('');
lines.push('When the Web Clipper service is enabled, Joplin exposes a [REST API](https://en.wikipedia.org/wiki/Representational_state_transfer) which allows third-party applications to access Joplin\'s data and to create, modify or delete notes, notebooks, resources or tags.');
lines.push('');
lines.push('In order to use it, you\'ll first need to find on which port the service is running. To do so, open the Web Clipper Options in Joplin and if the service is running it should tell you on which port. Normally it runs on port **41184**. If you want to find it programmatically, you may follow this kind of algorithm:');
lines.push('');
lines.push('```javascript');
lines.push('let port = null;');
lines.push('for (let portToTest = 41184; portToTest <= 41194; portToTest++) {');
lines.push(' const result = pingPort(portToTest); // Call GET /ping');
lines.push(' if (result == \'JoplinClipperServer\') {');
lines.push(' port = portToTest; // Found the port');
lines.push(' break;');
lines.push(' }');
lines.push('}');
lines.push('```');
lines.push('# Authorisation')
lines.push('');
lines.push('To prevent unauthorised applications from accessing the API, the calls must be authentified. To do so, you must provide a token as a query parameter for each API call. You can get this token from the Joplin desktop application, on the Web Clipper Options screen.');
lines.push('');
lines.push('This would be an example of valid cURL call using a token:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes?token=ABCD123ABCD123ABCD123ABCD123ABCD123');
lines.push('');
lines.push('In the documentation below, the token will not be specified every time however you will need to include it.');
lines.push('# Using the API');
lines.push('');
lines.push('All the calls, unless noted otherwise, receives and send **JSON data**. For example to create a new note:');
lines.push('');
lines.push('\tcurl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://localhost:41184/notes');
lines.push('');
lines.push('In the documentation below, the calls may include special parameters such as :id or :note_id. You would replace this with the item ID or note ID.');
lines.push('');
lines.push('For example, for the endpoint `DELETE /tags/:id/notes/:note_id`, to remove the tag with ID "ABCD1234" from the note with ID "EFGH789", you would run for example:');
lines.push('');
lines.push('\tcurl -X DELETE http://localhost:41184/tags/ABCD1234/notes/EFGH789');
lines.push('');
lines.push('The four verbs supported by the API are the following ones:');
lines.push('');
lines.push('* **GET**: To retrieve items (notes, notebooks, etc.).');
lines.push('* **POST**: To create new items. In general most item properties are optional. If you omit any, a default value will be used.');
lines.push('* **PUT**: To update an item. Note in a REST API, traditionally PUT is used to completely replace an item, however in this API it will only replace the properties that are provided. For example if you PUT {"title": "my new title"}, only the "title" property will be changed. The other properties will be left untouched (they won\'t be cleared nor changed).');
lines.push('* **DELETE**: To delete items.');
lines.push('');
lines.push('# Filtering data');
lines.push('');
lines.push('You can change the fields that will be returned by the API using the `fields=` query parameter, which takes a list of comma separated fields. For example, to get the longitude and latitude of a note, use this:');
lines.push('');
lines.push('\tcurl http://localhost:41184/notes/ABCD123?fields=longitude,latitude');
lines.push('');
lines.push('To get the IDs only of all the tags:');
lines.push('');
lines.push('\tcurl http://localhost:41184/tags?fields=id');
lines.push('');
lines.push('# About the property types');
lines.push('');
lines.push('* Text is UTF-8.');
lines.push('* All date/time are Unix timestamps in milliseconds.');
lines.push('* Booleans are integer values 0 or 1.');
lines.push('');
lines.push('# Testing if the service is available');
lines.push('');
lines.push('Call **GET /ping** to check if the service is available. It should return "JoplinClipperServer" if it works.');
lines.push('');
for (let i = 0; i < models.length; i++) {
const model = models[i];
const ModelClass = BaseItem.getClassByItemType(model.type);
const tableName = ModelClass.tableName();
let tableFields = reg.db().tableFields(tableName, { includeDescription: true });
const singular = tableName.substr(0, tableName.length - 1);
if (model.type === BaseModel.TYPE_NOTE) {
tableFields = tableFields.slice();
tableFields.push({
name: 'body_html',
type: Database.enumId('fieldType', 'text'),
description: 'Note body, in HTML format',
});
tableFields.push({
name: 'base_url',
type: Database.enumId('fieldType', 'text'),
description: '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`.',
});
tableFields.push({
name: 'image_data_url',
type: Database.enumId('fieldType', 'text'),
description: '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.',
});
tableFields.push({
name: 'crop_rect',
type: Database.enumId('fieldType', 'text'),
description: '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 }`',
});
}
lines.push('# ' + toTitleCase(tableName));
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('This is actually a notebook. Internally notebooks are called "folders".');
lines.push('');
}
lines.push('## Properties');
lines.push('');
lines.push(this.createPropertiesTable(tableFields));
lines.push('');
lines.push('## GET /' + tableName);
lines.push('');
lines.push('Gets all ' + tableName);
lines.push('');
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('The folders are returned as a tree. The sub-notebooks of a notebook, if any, are under the `children` key.');
lines.push('');
}
lines.push('## GET /' + tableName + '/:id');
lines.push('');
lines.push('Gets ' + singular + ' with ID :id');
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## GET /tags/:id/notes');
lines.push('');
lines.push('Gets all the notes with this tag.');
lines.push('');
}
if (model.type === BaseModel.TYPE_NOTE) {
lines.push('## GET /notes/:id/tags');
lines.push('');
lines.push('Gets all the tags attached to this note.');
lines.push('');
}
if (model.type === BaseModel.TYPE_FOLDER) {
lines.push('## GET /folders/:id/notes');
lines.push('');
lines.push('Gets all the notes inside this folder.');
lines.push('');
}
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('## GET /resources/:id/file');
lines.push('');
lines.push('Gets the actual file associated with this resource.');
lines.push('');
}
lines.push('## POST /' + tableName);
lines.push('');
lines.push('Creates a new ' + singular);
lines.push('');
if (model.type === BaseModel.TYPE_RESOURCE) {
lines.push('Creating a new resource is special because you also need to upload the file. Unlike other API calls, this one must have the "multipart/form-data" Content-Type. The file data must be passed to the "data" form field, and the other properties to the "props" form field. An example of a valid call with cURL would be:');
lines.push('');
lines.push('\tcurl -F \'data=@/path/to/file.jpg\' -F \'props={"title":"my resource title"}\' http://localhost:41184/resources');
lines.push('');
lines.push('The "data" field is required, while the "props" one is not. If not specified, default values will be used.');
lines.push('');
}
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## POST /tags/:id/notes');
lines.push('');
lines.push('Post a note to this endpoint to add the tag to the note. The note data must at least contain an ID property (all other properties will be ignored).');
lines.push('');
}
if (model.type === BaseModel.TYPE_NOTE) {
lines.push('You can either specify the note body as Markdown by setting the `body` parameter, or in HTML by setting the `body_html`.');
lines.push('');
lines.push('Examples:');
lines.push('');
lines.push('* Create a note from some Markdown text');
lines.push('');
lines.push(' curl --data \'{ "title": "My note", "body": "Some note in **Markdown**"}\' http://127.0.0.1:41184/notes');
lines.push('');
lines.push('* Create a note from some HTML');
lines.push('');
lines.push(' curl --data \'{ "title": "My note", "body_html": "Some note in <b>HTML</b>"}\' http://127.0.0.1:41184/notes');
lines.push('');
lines.push('* Create a note and attach an image to it:');
lines.push('');
lines.push(' curl --data \'{ "title": "Image test", "body": "Here is Joplin icon:", "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="}\' http://127.0.0.1:41184/notes');
lines.push('');
}
lines.push('## PUT /' + tableName + '/:id');
lines.push('');
lines.push('Sets the properties of the ' + singular + ' with ID :id');
lines.push('');
lines.push('## DELETE /' + tableName + '/:id');
lines.push('');
lines.push('Deletes the ' + singular + ' with ID :id');
lines.push('');
if (model.type === BaseModel.TYPE_TAG) {
lines.push('## DELETE /tags/:id/notes/:note_id');
lines.push('');
lines.push('Remove the tag from the note.');
lines.push('');
}
}
this.stdout(lines.join('\n'));
}
}
module.exports = Command;

View File

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

View File

@@ -716,6 +716,9 @@ msgstr "Sí"
msgid "No"
msgstr "No"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
"El servei de desa-retalls de webs és actiu i configurat per a iniciar-se "
@@ -766,6 +769,21 @@ msgstr "Pas 2: Instal·leu l'extensió"
msgid "Download and install the relevant extension for your browser:"
msgstr "Baixeu i instal·leu l'extensió adient per al vostre navegador:"
#, fuzzy
msgid "Advanced options"
msgstr "Mostra les opcions avançades"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Comprova la configuració de la sincronització"

View File

@@ -698,6 +698,9 @@ msgstr "Ano"
msgid "No"
msgstr "Ne"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -741,6 +744,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Ukázat pokročilé volby"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Zkontrolujte nastavení synchronizace"

View File

@@ -703,6 +703,9 @@ msgstr "Ja"
msgid "No"
msgstr "Nej"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -746,6 +749,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Vis avancerede indstillinger"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Check synkroniserings Indstillinger"

View File

@@ -13,7 +13,7 @@ msgstr ""
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.8\n"
"X-Generator: Poedit 2.1.1\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "To delete a tag, untag the associated notes."
@@ -727,6 +727,9 @@ msgstr "Ja"
msgid "No"
msgstr "Nein"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
"Der Webclipperservice ist bereits aktiviert und auf Autostart eingestellt."
@@ -778,6 +781,21 @@ msgstr ""
"Lade die entsprechende Erweiterung für deinen Browser herunter und "
"installiere sie:"
#, fuzzy
msgid "Advanced options"
msgstr "Erweiterte Optionen anzeigen"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Überprüfen der Synchronisationseinstellungen"
@@ -963,7 +981,7 @@ msgstr ""
"\"Neues Notizbuch\" drückst."
msgid "Location"
msgstr ""
msgstr "Ablageort"
msgid "URL"
msgstr ""
@@ -1022,7 +1040,7 @@ msgid "In: %s"
msgstr "In: %s"
msgid "Note properties"
msgstr ""
msgstr "Notiz-Eigenschaften"
msgid "Hyperlink"
msgstr "Weblink"
@@ -1310,7 +1328,7 @@ msgid "Note: Does not work in all desktop environments."
msgstr "Hinweis: Funktioniert nicht in allen Desktopumgebungen."
msgid "Start application minimised in the tray icon"
msgstr ""
msgstr "Starte die Anwendung minimiert im Tray"
msgid "Global zoom percentage"
msgstr "Zoomstufe der Benutzeroberfläche"
@@ -1387,6 +1405,9 @@ msgid ""
"to it before syncing, otherwise all files will be removed! See the FAQ for "
"more details: %s"
msgstr ""
"Achtung: Stelle sicher, dass Du vor der Synchronisation alle Inhalte an den "
"neuen Ablageort kopiert hast, sonst werden alle Dateien gelöscht! Lies auch "
"die FAQs hierzu: %s"
msgid "Nextcloud username"
msgstr "Nextcloud Benutzername"
@@ -1428,6 +1449,7 @@ msgstr "Ungültiger Optionswert: \"%s\". Mögliche Werte sind: %s."
#, javascript-format
msgid "The tag \"%s\" already exists. Please choose a different name."
msgstr ""
"Das Schlagwort \"%s\" existiert bereits. Bitte wähle einen anderen Namen."
msgid "Joplin Export File"
msgstr "Joplin Export Datei"

View File

@@ -635,6 +635,9 @@ msgstr ""
msgid "No"
msgstr ""
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -678,6 +681,20 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
msgid "Advanced options"
msgstr ""
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""

View File

@@ -712,6 +712,9 @@ msgstr "Sí"
msgid "No"
msgstr "No"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
"El servicio de recorte web está habilitado y configurado para que inicie "
@@ -762,6 +765,21 @@ msgstr "Paso 2: Instalar la extensión"
msgid "Download and install the relevant extension for your browser:"
msgstr "Descargar e instalar para su navegador:"
#, fuzzy
msgid "Advanced options"
msgstr "Mostrar opciones avanzadas"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Comprobar sincronización"

View File

@@ -713,6 +713,9 @@ msgstr ""
msgid "No"
msgstr "E"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -756,6 +759,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Erakutsi aukera aurreratuak"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Sinkronizazioa utzi"

View File

@@ -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\""
@@ -712,6 +711,9 @@ msgstr "Oui"
msgid "No"
msgstr "Non"
msgid "Token has been copied to the clipboard!"
msgstr "Le code d'authentification a été copié dans le presse-papiers !"
msgid "The web clipper service is enabled and set to auto-start."
msgstr "Le service du Web Clipper est activé et démarrera automatiquement."
@@ -762,6 +764,22 @@ msgstr ""
"Téléchargez et installez le module complémentaire correspondant à votre "
"navigateur :"
msgid "Advanced options"
msgstr "Options avancées"
msgid "Authorisation token:"
msgstr "Code d'authentification :"
msgid "Copy token"
msgstr "Copier le code"
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
"Ce code d'authentification est nécessaire uniquement pour permettre aux "
"logiciels tiers d'accéder aux données de Joplin."
msgid "Check synchronisation configuration"
msgstr "Vérifier config synchronisation"
@@ -949,10 +967,10 @@ msgstr ""
"carnet\"."
msgid "Location"
msgstr ""
msgstr "Lieu"
msgid "URL"
msgstr ""
msgstr "URL"
msgid "Open..."
msgstr "Ouvrir..."
@@ -1008,7 +1026,7 @@ msgid "In: %s"
msgstr "Dans : %s"
msgid "Note properties"
msgstr ""
msgstr "Propriétés de la note"
msgid "Hyperlink"
msgstr "Lien"
@@ -1294,7 +1312,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 +1389,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 +1432,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"

View File

@@ -703,6 +703,9 @@ msgstr "Si"
msgid "No"
msgstr "Non"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -746,6 +749,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Mostrar opcións avanzadas"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Comprobar a configuración da sincronización"

View File

@@ -711,6 +711,9 @@ msgstr ""
msgid "No"
msgstr "N"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -754,6 +757,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Prikaži napredne opcije"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Prekini sinkronizaciju"

View File

@@ -709,6 +709,9 @@ msgstr "Sì"
msgid "No"
msgstr "No"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
"Il servizio Web clipper è abilitato e impostato per l'avvio automatico."
@@ -758,6 +761,21 @@ msgstr "Passaggio 2: installare l'estensione"
msgid "Download and install the relevant extension for your browser:"
msgstr "Scarica e installa l'estensione adatta per il tuo browser:"
#, fuzzy
msgid "Advanced options"
msgstr "Mostra opzioni avanzate"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Controlla la configurazione della sincronizzazione"

File diff suppressed because it is too large Load Diff

View File

@@ -635,6 +635,9 @@ msgstr ""
msgid "No"
msgstr ""
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -678,6 +681,20 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
msgid "Advanced options"
msgstr ""
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""

View File

@@ -695,6 +695,9 @@ msgstr "예"
msgid "No"
msgstr "아니오"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr "웹 수집기 서비스가 활성화되었고 자동으로 시작하도록 설정되었습니다."
@@ -742,6 +745,21 @@ msgstr "2단계: 확장 기능 설치"
msgid "Download and install the relevant extension for your browser:"
msgstr "브라우저에 적절한 확장 기능을 다운로드 및 설치하세요:"
#, fuzzy
msgid "Advanced options"
msgstr "고급 옵션"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "동기화 설정 확인"

View File

@@ -715,6 +715,9 @@ msgstr ""
msgid "No"
msgstr "N"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -758,6 +761,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Toon geavanceerde opties"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
#, fuzzy
msgid "Check synchronisation configuration"
msgstr "Annuleer synchronisatie"

View File

@@ -712,6 +712,9 @@ msgstr "Ja"
msgid "No"
msgstr "Nee"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr "De webclipper-dienst is ingeschakeld en wordt automatisch opgestart."
@@ -761,6 +764,21 @@ msgstr "Stap 2: Installeer de extensie"
msgid "Download and install the relevant extension for your browser:"
msgstr "Download en installeer de bijbehorende extensie in je browser:"
#, fuzzy
msgid "Advanced options"
msgstr "Geavanceerde opties tonen"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Synchronisatieconfiguratie controleren"

View File

@@ -706,6 +706,9 @@ msgstr "Ja"
msgid "No"
msgstr "Nei"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -749,6 +752,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Vis avanserte valg"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Sjekk synkroniseringskonfigurasjonen"

View File

@@ -708,6 +708,9 @@ msgstr "Sim"
msgid "No"
msgstr "Não"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
"O serviço de web clipper está habilitado e configurado para auto-start."
@@ -757,6 +760,21 @@ msgstr "Passo 2: Instalar a extensão"
msgid "Download and install the relevant extension for your browser:"
msgstr "Baixe e instale a extensão relevante para seu browser:"
#, fuzzy
msgid "Advanced options"
msgstr "Mostrar opções avançadas"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Verificar a configuração da sincronização"

View File

@@ -648,6 +648,9 @@ msgstr "Da"
msgid "No"
msgstr "Nu"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -691,6 +694,21 @@ msgstr "Pasul 2: Instalați extensia"
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Afișați opțiunile avansate"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr ""

View File

@@ -710,6 +710,9 @@ msgstr "Да"
msgid "No"
msgstr "Нет"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -753,6 +756,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Показывать расширенные настройки"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Проверить настройки синхронизации"

View File

@@ -709,6 +709,9 @@ msgstr "Da"
msgid "No"
msgstr "Ne"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr ""
@@ -752,6 +755,21 @@ msgstr ""
msgid "Download and install the relevant extension for your browser:"
msgstr ""
#, fuzzy
msgid "Advanced options"
msgstr "Pokaži napredne možnosti"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Preveri nastavitve sinhronizacije"

View File

@@ -716,6 +716,9 @@ msgstr "Ja"
msgid "No"
msgstr "Nej"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr "Web clipper-tjänsten är aktiverad och inställd för automatisk start."
@@ -764,6 +767,21 @@ msgstr "Steg 2: Installera tillägget"
msgid "Download and install the relevant extension for your browser:"
msgstr "Hämta och installera det relevanta tillägget för din webbläsare:"
#, fuzzy
msgid "Advanced options"
msgstr "Visa avancerade inställningar"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "Kontrollera synkroniseringskonfigurationen"

View File

@@ -675,6 +675,9 @@ msgstr "是"
msgid "No"
msgstr "否"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr "网页剪辑服务已启用并设置为自动启动。"
@@ -720,6 +723,21 @@ msgstr "步骤二:安装扩展"
msgid "Download and install the relevant extension for your browser:"
msgstr "为您的浏览器下载并安装相关的扩展:"
#, fuzzy
msgid "Advanced options"
msgstr "显示高级选项"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "检查同步配置"

View File

@@ -675,6 +675,9 @@ msgstr "是"
msgid "No"
msgstr "否"
msgid "Token has been copied to the clipboard!"
msgstr ""
msgid "The web clipper service is enabled and set to auto-start."
msgstr "Web clipper 服務已啟用,並設置為自動啟動。"
@@ -720,6 +723,21 @@ msgstr "步驟 2: 安裝插件"
msgid "Download and install the relevant extension for your browser:"
msgstr "下載並安裝瀏覽器插件:"
#, fuzzy
msgid "Advanced options"
msgstr "顯示進階選項"
msgid "Authorisation token:"
msgstr ""
msgid "Copy token"
msgstr ""
msgid ""
"This authorisation token is only needed to allow third-party applications to "
"access Joplin."
msgstr ""
msgid "Check synchronisation configuration"
msgstr "檢測同步設置"

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
],
"owner": "Laurent Cozic"
},
"version": "1.0.114",
"version": "1.0.116",
"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",

View File

@@ -22,7 +22,6 @@ trap finish EXIT
cd "$ROOT_DIR"
npm test tests-build/ArrayUtils.js
npm test tests-build/encryption.js
npm test tests-build/EnexToMd.js
npm test tests-build/HtmlToMd.js
npm test tests-build/markdownUtils.js
@@ -32,5 +31,7 @@ npm test tests-build/models_Tag.js
npm test tests-build/models_Setting.js
npm test tests-build/services_InteropService.js
npm test tests-build/services_ResourceService.js
npm test tests-build/synchronizer.js
npm test tests-build/urlUtils.js
npm test tests-build/urlUtils.js
npm test tests-build/encryption.js
npm test tests-build/services_rest_Api.js
npm test tests-build/synchronizer.js

View File

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

View File

@@ -0,0 +1,5 @@
<ul>
<li><div>This note has an unordered list</div></li>
<li><div>List item</div></li>
<li><div>List item</div></li>
</ul>

View File

@@ -0,0 +1,3 @@
- This note has an unordered list
- List item
- List item

View File

@@ -0,0 +1 @@
<a href="http://example.com/That is not right"/>Testing</a>

View File

@@ -0,0 +1 @@
[Testing](http://example.com/That%20is%20not%20right)

View 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&amp;w=588&amp;h=900&amp;fit=crop&amp;dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;fit=crop&amp;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&amp;w=588&amp;h=900&amp;fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;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&amp;w=588&amp;h=900&amp;fit=crop&amp;dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;fit=crop&amp;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&amp;w=588&amp;h=900&amp;fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;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&amp;w=450&amp;h=688&amp;fit=crop&amp;dpr=1.5 675w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=450&amp;h=688&amp;fit=crop&amp;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&amp;w=450&amp;h=688&amp;fit=crop 450w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=450&amp;h=688&amp;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&amp;w=320&amp;h=489&amp;fit=crop&amp;dpr=1.5 480w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=320&amp;h=489&amp;fit=crop&amp;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&amp;w=320&amp;h=489&amp;fit=crop 320w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=320&amp;h=489&amp;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&amp;w=588&amp;h=900&amp;fit=crop&amp;dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;fit=crop&amp;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&amp;w=588&amp;h=900&amp;fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;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&amp;w=588&amp;h=900&amp;fit=crop&amp;dpr=1.5 882w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;fit=crop&amp;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&amp;w=588&amp;h=900&amp;fit=crop 588w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=588&amp;h=900&amp;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&amp;w=450&amp;h=688&amp;fit=crop&amp;dpr=1.5 675w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=450&amp;h=688&amp;fit=crop&amp;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&amp;w=450&amp;h=688&amp;fit=crop 450w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=450&amp;h=688&amp;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&amp;w=320&amp;h=489&amp;fit=crop&amp;dpr=1.5 480w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=320&amp;h=489&amp;fit=crop&amp;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&amp;w=320&amp;h=489&amp;fit=crop 320w" srcset="https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&amp;w=320&amp;h=489&amp;fit=crop 320w">
<!--[if IE 9]></video><![endif]-->
<img class=" lazyloaded" title="" alt="" id="img-id-0" src="http://example.com/test.gif">
</picture>

View File

@@ -0,0 +1 @@
Some pictures: ![](https://static2.cbrimages.com/wp-content/uploads/2018/09/Die-01-cvrA.jpg?q=35&w=588&h=900&fit=crop&dpr=1.5) ![](http://example.com/test.gif)

View File

@@ -39,6 +39,7 @@ describe('markdownUtils', function() {
['![something](http://test.com/img.png)', ['http://test.com/img.png']],
['![something](http://test.com/img.png) ![something2](http://test.com/img2.png)', ['http://test.com/img.png', 'http://test.com/img2.png']],
['![something](http://test.com/img.png "Some description")', ['http://test.com/img.png']],
['![something](https://test.com/ohoh_(123).png)', ['https://test.com/ohoh_(123).png']],
];
for (let i = 0; i < testCases.length; i++) {

View File

@@ -34,6 +34,12 @@ describe('models_Note', function() {
expect(items.length).toBe(2);
expect(items[0].type_).toBe(BaseModel.TYPE_NOTE);
expect(items[1].type_).toBe(BaseModel.TYPE_RESOURCE);
const resource = items[1];
note2.body += '<img alt="bla" src=":/' + resource.id + '"/>';
note2.body += '<img src=\':/' + resource.id + '\' />';
items = await Note.linkedItems(note2.body);
expect(items.length).toBe(4);
}));
});

View File

@@ -79,7 +79,6 @@ describe('services_ResourceService', function() {
let note2 = await Note.save({ title: 'ma deuxième note', parent_id: folder1.id });
note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
const resourcePath = Resource.fullPath(resource1);
await service.indexNoteResources();
@@ -106,4 +105,23 @@ describe('services_ResourceService', function() {
expect((await Resource.all()).length).toBe(1);
}));
it('should not delete resource if it is used in an IMG tag', asyncTest(async () => {
const service = new ResourceService();
let folder1 = await Folder.save({ title: "folder1" });
let note1 = await Note.save({ title: 'ma note', parent_id: folder1.id });
note1 = await shim.attachFileToNote(note1, __dirname + '/../tests/support/photo.jpg');
let resource1 = (await Resource.all())[0];
await service.indexNoteResources();
await Note.save({ id: note1.id, body: 'This is HTML: <img src=":/' + resource1.id + '"/>' });
await service.indexNoteResources();
await service.deleteOrphanResources(0);
expect(!!(await Resource.load(resource1.id))).toBe(true);
}));
});

View File

@@ -0,0 +1,275 @@
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 Tag = require('lib/models/Tag');
const Resource = require('lib/models/Resource');
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
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 update folders', async (done) => {
let f1 = await Folder.save({ title: "mon carnet" });
const response = await api.route('PUT', 'folders/' + f1.id, null, JSON.stringify({
title: 'modifié',
}));
let f1b = await Folder.load(f1.id);
expect(f1b.title).toBe('modifié');
done();
});
it('should delete folders', async (done) => {
let f1 = await Folder.save({ title: "mon carnet" });
await api.route('DELETE', 'folders/' + f1.id);
let f1b = await Folder.load(f1.id);
expect(!f1b).toBe(true);
done();
});
it('should create folders', async (done) => {
const response = await api.route('POST', 'folders', null, JSON.stringify({
title: 'from api',
}));
expect(!!response.id).toBe(true);
let f = await Folder.all();
expect(f.length).toBe(1);
expect(f[0].title).toBe('from api');
done();
});
it('should get one folder', async (done) => {
let f1 = await Folder.save({ title: "mon carnet" });
const response = await api.route('GET', 'folders/' + f1.id);
expect(response.id).toBe(f1.id);
const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'folders/doesntexist'));
expect(hasThrown).toBe(true);
done();
});
it('should get the folder notes', async (done) => {
let f1 = await Folder.save({ title: "mon carnet" });
const response2 = await api.route('GET', 'folders/' + f1.id + '/notes');
expect(response2.length).toBe(0);
const n1 = await Note.save({ title: 'un', parent_id: f1.id });
const n2 = await Note.save({ title: 'deux', parent_id: f1.id });
const response = await api.route('GET', 'folders/' + f1.id + '/notes');
expect(response.length).toBe(2);
done();
});
it('should fail on invalid paths', async (done) => {
const hasThrown = await checkThrowAsync(async () => await api.route('GET', 'schtroumpf'));
expect(hasThrown).toBe(true);
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/' + 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');
let 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);
hasThrown = await checkThrowAsync(async () => await api.route('POST', 'notes', null, JSON.stringify({title:'testing'})));
expect(hasThrown).toBe(true);
done();
});
it('should add tags to notes', async (done) => {
const tag = await Tag.save({ title: "mon étiquette" });
const note = await Note.save({ title: "ma note" });
const response = await api.route('POST', 'tags/' + tag.id + '/notes', null, JSON.stringify({
id: note.id,
}));
const noteIds = await Tag.noteIds(tag.id);
expect(noteIds[0]).toBe(note.id);
done();
});
it('should remove tags from notes', async (done) => {
const tag = await Tag.save({ title: "mon étiquette" });
const note = await Note.save({ title: "ma note" });
await Tag.addNote(tag.id, note.id);
const response = await api.route('DELETE', 'tags/' + tag.id + '/notes/' + note.id);
const noteIds = await Tag.noteIds(tag.id);
expect(noteIds.length).toBe(0);
done();
});
it('should list all tag notes', async (done) => {
const tag = await Tag.save({ title: "mon étiquette" });
const tag2 = await Tag.save({ title: "mon étiquette 2" });
const note1 = await Note.save({ title: "ma note un" });
const note2 = await Note.save({ title: "ma note deux" });
await Tag.addNote(tag.id, note1.id);
await Tag.addNote(tag.id, note2.id);
const response = await api.route('GET', 'tags/' + tag.id + '/notes');
expect(response.length).toBe(2);
expect('id' in response[0]).toBe(true);
expect('title' in response[0]).toBe(true);
const response2 = await api.route('GET', 'notes/' + note1.id + '/tags');
expect(response2.length).toBe(1);
await Tag.addNote(tag2.id, note1.id);
const response3 = await api.route('GET', 'notes/' + note1.id + '/tags');
expect(response3.length).toBe(2);
done();
});
});

View File

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

View File

@@ -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 || '',
};
}
@@ -137,6 +138,7 @@
messageComp.style.maxWidth = messageCompWidth + 'px'
messageComp.style.border = '1px solid black'
messageComp.style.background = 'white'
messageComp.style.color = 'black';
messageComp.style.top = '10px'
messageComp.style.textAlign = 'center';
messageComp.style.padding = '10px'
@@ -214,6 +216,7 @@
crop_rect: selectionArea,
url: location.origin + location.pathname,
parent_id: command.parent_id,
tags: command.tags,
};
browser_.runtime.sendMessage({

View File

@@ -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": {

View File

@@ -1,5 +1,7 @@
<!DOCTYPE html>
<!-- NOTE: I think this is not used at all -->
<html>
<head>
<meta charset="utf-8">

View File

@@ -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 {

View File

@@ -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,
};
};

View File

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

View File

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

View File

@@ -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;
}
@@ -418,6 +420,10 @@ class Application extends BaseApplication {
label: _('Paste'),
role: 'paste',
accelerator: 'CommandOrControl+V',
}, {
label: _('Select all'),
role: 'selectall',
accelerator: 'CommandOrControl+A',
}, {
type: 'separator',
screens: ['Main'],

File diff suppressed because one or more lines are too long

View File

@@ -142,7 +142,20 @@ class NoteTextComponent extends React.Component {
}
this.onDrop_ = async (event) => {
const files = event.dataTransfer.files;
const dt = event.dataTransfer;
if (dt.types.indexOf("text/x-jop-note-ids") >= 0) {
const noteIds = JSON.parse(dt.getData("text/x-jop-note-ids"));
const linkText = [];
for (let i = 0; i < noteIds.length; i++) {
const note = await Note.load(noteIds[i]);
linkText.push(Note.markdownTag(note));
}
this.wrapSelectionWithStrings("", "", '', linkText.join('\n'));
}
const files = dt.files;
if (!files || !files.length) return;
const filesToAttach = [];
@@ -748,7 +761,7 @@ class NoteTextComponent extends React.Component {
this.forceUpdate();
}, 100);
},
postMessageSyntax: 'ipcRenderer.sendToHost',
postMessageSyntax: 'ipcProxySendToHost',
};
const theme = themeStyle(this.props.theme);
@@ -1025,6 +1038,9 @@ class NoteTextComponent extends React.Component {
end: { row: p.row, column: p.column + middleText.length },
};
// BUG!! If replacementText contains newline characters, the logic
// to select the new text will not work.
this.updateEditorWithDelay((editor) => {
if (middleText && newRange) {
const range = this.selectionRange_;
@@ -1404,6 +1420,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); } }
/>

View File

@@ -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'),
});

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.107",
"version": "1.0.111",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -40,18 +40,31 @@
"integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w=="
},
"acorn": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.2.tgz",
"integrity": "sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw=="
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
"integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
},
"acorn-globals": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.1.0.tgz",
"integrity": "sha512-KjZwU26uG3u6eZcfGbTULzFcsoz6pegNKtHPksZPOUsiKo5bUmiBPa38FuHZ/Eun+XYh/JCCkS9AS3Lu4McQOQ==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.0.tgz",
"integrity": "sha512-hMtHj3s5RnuhvHPowpBYvJVj3rAar82JiDQHvGs1zO0l10ocX/xEdBShNHTJaboucJUsScghp74pH3s7EnHHQw==",
"requires": {
"acorn": "^5.0.0"
"acorn": "^6.0.1",
"acorn-walk": "^6.0.1"
},
"dependencies": {
"acorn": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz",
"integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg=="
}
}
},
"acorn-walk": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz",
"integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg=="
},
"ajv": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.0.tgz",
@@ -914,9 +927,9 @@
}
},
"browser-process-hrtime": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.2.tgz",
"integrity": "sha1-Ql1opY00R/AqBKqJQYf86K+Le44="
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
"integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw=="
},
"buffer-from": {
"version": "1.1.0",
@@ -1783,6 +1796,11 @@
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"depd": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
"integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
},
"detect-indent": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz",
@@ -3519,6 +3537,18 @@
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz",
"integrity": "sha1-DfKTUfByEWNRXfueVUPl9u7VFi8="
},
"http-errors": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.1.tgz",
"integrity": "sha512-jWEUgtZWGSMba9I1N3gc1HmvpBUaNC9vDdA46yScAdp+C5rdEuKWUBLWTQpW9FwSWSbYYs++b6SDCxf9UEJzfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.0",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
@@ -3909,9 +3939,9 @@
"integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
},
"joplin-turndown": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.8.tgz",
"integrity": "sha512-RPZJSZEplVPL3UiJNkaKsFAG8bCGofsKIiH24s8/4qcy1xYnEufvg++rHm7rxi/0VCtpSkRBlWHSs1/srJZvoA==",
"version": "4.0.9",
"resolved": "https://registry.npmjs.org/joplin-turndown/-/joplin-turndown-4.0.9.tgz",
"integrity": "sha512-8MOxX4t5Ai22muHhXPMGNoKc/AB7gSo0eUvNh6dyd6b3vcSiMIRZE8UHpMjS9ruJQ+8e+8TtJXc0nfbexeHwrA==",
"requires": {
"jsdom": "^11.9.0"
}
@@ -4483,6 +4513,32 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multiparty": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/multiparty/-/multiparty-4.2.1.tgz",
"integrity": "sha512-AvESCnNoQlZiOfP9R4mxN8M9csy2L16EIbWIkt3l4FuGti9kXBS8QVzlfyg4HEnarJhrzZilgNFlZtqmoiAIIA==",
"requires": {
"fd-slicer": "1.1.0",
"http-errors": "~1.7.0",
"safe-buffer": "5.1.2",
"uid-safe": "2.1.5"
},
"dependencies": {
"fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=",
"requires": {
"pend": "~1.2.0"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
}
}
},
"nan": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz",
@@ -4888,8 +4944,7 @@
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=",
"dev": true
"integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA="
},
"performance-now": {
"version": "2.1.0",
@@ -5059,6 +5114,11 @@
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz",
"integrity": "sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
"randomatic": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.0.0.tgz",
@@ -5539,6 +5599,11 @@
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"setprototypeof": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"shebang-command": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
@@ -6538,6 +6603,11 @@
}
}
},
"statuses": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
},
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
@@ -6816,6 +6886,11 @@
"repeat-string": "^1.6.1"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
},
"tough-cookie": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz",
@@ -6898,6 +6973,14 @@
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.5.tgz",
"integrity": "sha512-JoLI4g5zv5qNyT09f4YAvEZIIV1oOjqnewYg5D38dkQljIzpPT296dbIGvKro3digYI1bkb7W6EP1y4uDlmzLg=="
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"union-value": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
@@ -7182,9 +7265,9 @@
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
},
"whatwg-mimetype": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.1.0.tgz",
"integrity": "sha512-FKxhYLytBQiUKjkYteN71fAUA3g6KpNXoho1isLiLSB3N1G4F35Q5vUxWfKFhBwi5IWF27VE6WxhrnnC+m0Mew=="
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz",
"integrity": "sha512-5YSO1nMd5D1hY3WzAQV3PzZL83W3YeyR1yW9PcH26Weh1t+Vzh9B6XkDh7aXm83HBZ4nSMvkjvN2H2ySWIvBgw=="
},
"whatwg-url": {
"version": "6.5.0",

View File

@@ -1,6 +1,6 @@
{
"name": "Joplin",
"version": "1.0.107",
"version": "1.0.111",
"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",
@@ -105,6 +105,7 @@
"mermaid": "^8.0.0-rc.8",
"mime": "^2.3.1",
"moment": "^2.22.2",
"multiparty": "^4.2.1",
"node-fetch": "^1.7.3",
"node-notifier": "^5.2.1",
"promise": "^8.0.1",

View File

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

View File

@@ -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.111/Joplin-Setup-1.0.111.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.111/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.111/Joplin-1.0.111.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.111/Joplin-1.0.111-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.132/joplin-v1.0.132.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.141/joplin-v1.0.141.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
@@ -71,6 +71,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- [How to enable end-to-end encryption](https://github.com/laurent22/joplin/blob/master/readme/e2ee.md)
- [End-to-end encryption spec](https://github.com/laurent22/joplin/blob/master/readme/spec.md)
- [How to enable debug mode](https://github.com/laurent22/joplin/blob/master/readme/debugging.md)
- [API documentation](https://github.com/laurent22/joplin/blob/master/readme/api.md)
- [FAQ](https://github.com/laurent22/joplin/blob/master/readme/faq.md)
- About
@@ -274,7 +275,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
@@ -311,29 +314,29 @@ Current translations:
<!-- LOCALE-TABLE-AUTO-GENERATED -->
&nbsp; | Language | Po File | Last translator | Percent done
---|---|---|---|---
![](https://joplin.cozic.net/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 64%
![](https://joplin.cozic.net/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ca.po) | jmontane, 2018 | 91%
![](https://joplin.cozic.net/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 52%
![](https://joplin.cozic.net/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 81%
![](https://joplin.cozic.net/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér (mjjzf@syntaktisk. | 83%
![](https://joplin.cozic.net/images/flags/es/basque_country.png) | Basque | [eu](https://github.com/laurent22/joplin/blob/master/CliClient/locales/eu.po) | juan.abasolo@ehu.eus | 63%
![](https://joplin.cozic.net/images/flags/es/catalonia.png) | Catalan | [ca](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ca.po) | jmontane, 2018 | 90%
![](https://joplin.cozic.net/images/flags/country-4x3/hr.png) | Croatian | [hr_HR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/hr_HR.po) | Hrvoje Mandić (trbuhom@net.hr) | 51%
![](https://joplin.cozic.net/images/flags/country-4x3/cz.png) | Czech | [cs_CZ](https://github.com/laurent22/joplin/blob/master/CliClient/locales/cs_CZ.po) | Lukas Helebrandt (lukas@aiya.cz) | 80%
![](https://joplin.cozic.net/images/flags/country-4x3/dk.png) | Dansk | [da_DK](https://github.com/laurent22/joplin/blob/master/CliClient/locales/da_DK.po) | Morten Juhl-Johansen Zölde-Fejér (mjjzf@syntaktisk. | 82%
![](https://joplin.cozic.net/images/flags/country-4x3/de.png) | Deutsch | [de_DE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/de_DE.po) | Michael Sonntag (ms@editorei.de) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/gb.png) | English | [en_GB](https://github.com/laurent22/joplin/blob/master/CliClient/locales/en_GB.po) | | 100%
![](https://joplin.cozic.net/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín (f@mrtn.es) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 98%
![](https://joplin.cozic.net/images/flags/es/galicia.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 81%
![](https://joplin.cozic.net/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/nl.png) | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/es.png) | Español | [es_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/es_ES.po) | Fernando Martín (f@mrtn.es) | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/fr.png) | Français | [fr_FR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/fr_FR.po) | Laurent Cozic | 100%
![](https://joplin.cozic.net/images/flags/es/galicia.png) | Galician | [gl_ES](https://github.com/laurent22/joplin/blob/master/CliClient/locales/gl_ES.po) | Marcos Lans (marcoslansgarza@gmail.com) | 80%
![](https://joplin.cozic.net/images/flags/country-4x3/it.png) | Italiano | [it_IT](https://github.com/laurent22/joplin/blob/master/CliClient/locales/it_IT.po) | | 95%
![](https://joplin.cozic.net/images/flags/country-4x3/nl.png) | Nederlands | [nl_NL](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_NL.po) | Heimen Stoffels (vistausss@outlook.com) | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/be.png) | Nederlands | [nl_BE](https://github.com/laurent22/joplin/blob/master/CliClient/locales/nl_BE.po) | | 64%
![](https://joplin.cozic.net/images/flags/country-4x3/no.png) | Norwegian | [no](https://github.com/laurent22/joplin/blob/master/CliClient/locales/no.po) | | 87%
![](https://joplin.cozic.net/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ro.po) | | 64%
![](https://joplin.cozic.net/images/flags/country-4x3/si.png) | Slovenian | [sl_SI](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sl_SI.po) | | 80%
![](https://joplin.cozic.net/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sv.po) | Jonatan Nyberg (jonatan@autistici.org) | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 80%
![](https://joplin.cozic.net/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po) | penguinsam (samliu@gmail.com) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | | 52%
![](https://joplin.cozic.net/images/flags/country-4x3/kr.png) | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/no.png) | Norwegian | [no](https://github.com/laurent22/joplin/blob/master/CliClient/locales/no.po) | | 86%
![](https://joplin.cozic.net/images/flags/country-4x3/br.png) | Português (Brasil) | [pt_BR](https://github.com/laurent22/joplin/blob/master/CliClient/locales/pt_BR.po) | Renato Nunes Bastos (rnbastos@gmail.com) | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/ro.png) | Română | [ro](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ro.po) | | 63%
![](https://joplin.cozic.net/images/flags/country-4x3/si.png) | Slovenian | [sl_SI](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sl_SI.po) | | 79%
![](https://joplin.cozic.net/images/flags/country-4x3/se.png) | Svenska | [sv](https://github.com/laurent22/joplin/blob/master/CliClient/locales/sv.po) | Jonatan Nyberg (jonatan@autistici.org) | 96%
![](https://joplin.cozic.net/images/flags/country-4x3/ru.png) | Русский | [ru_RU](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ru_RU.po) | Artyom Karlov (artyom.karlov@gmail.com) | 79%
![](https://joplin.cozic.net/images/flags/country-4x3/cn.png) | 中文 (简体) | [zh_CN](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_CN.po) | | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/tw.png) | 中文 (繁體) | [zh_TW](https://github.com/laurent22/joplin/blob/master/CliClient/locales/zh_TW.po) | penguinsam (samliu@gmail.com) | 97%
![](https://joplin.cozic.net/images/flags/country-4x3/jp.png) | 日本語 | [ja_JP](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ja_JP.po) | AWASHIRO Ikuya (ikunya@gmail.com) | 98%
![](https://joplin.cozic.net/images/flags/country-4x3/kr.png) | 한국말 | [ko](https://github.com/laurent22/joplin/blob/master/CliClient/locales/ko.po) | | 97%
<!-- LOCALE-TABLE-AUTO-GENERATED -->
# Known bugs

View File

@@ -1,3 +1,3 @@
{
"presets": ["react-native"]
"presets": ["module:metro-react-native-babel-preset"]
}

49
ReactNativeClient/App.js Normal file
View File

@@ -0,0 +1,49 @@
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/
import React, {Component} from 'react';
import {Platform, StyleSheet, Text, View} from 'react-native';
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
android:
'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
type Props = {};
export default class App extends Component<Props> {
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});

View File

@@ -83,15 +83,15 @@ def enableSeparateBuildPerCPUArchitecture = false
def enableProguardInReleaseBuilds = false
android {
compileSdkVersion 23
buildToolsVersion "23.0.1"
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
applicationId "net.cozic.joplin"
minSdkVersion 16
targetSdkVersion 26
versionCode 2097310
versionName "1.0.132"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097319
versionName "1.0.141"
ndk {
abiFilters "armeabi-v7a", "x86"
}
@@ -144,9 +144,9 @@ dependencies {
compile project(':react-native-image-picker')
compile project(':react-native-vector-icons')
compile project(':react-native-fs')
compile fileTree(dir: "libs", include: ["*.jar"])
compile "com.android.support:appcompat-v7:23.0.1"
compile "com.facebook.react:react-native:+" // From node_modules
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:${rootProject.ext.supportLibVersion}"
implementation "com.facebook.react:react-native:+" // From node_modules
compile project(':react-native-sqlite-storage')
compile project(':react-native-fetch-blob')
compile project(':react-native-document-picker')

View File

@@ -15,56 +15,3 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Disabling obfuscation is useful if you collect stack traces from production crashes
# (unless you are using a system that supports de-obfuscate the stack traces).
-dontobfuscate
# React Native
# Keep our interfaces so they can be used by other ProGuard rules.
# See http://sourceforge.net/p/proguard/bugs/466/
-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
-keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
# Do not strip any method/class that is annotated with @DoNotStrip
-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keep @com.facebook.common.internal.DoNotStrip class *
-keepclassmembers class * {
@com.facebook.proguard.annotations.DoNotStrip *;
@com.facebook.common.internal.DoNotStrip *;
}
-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
void set*(***);
*** get*();
}
-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
-keep class * extends com.facebook.react.bridge.NativeModule { *; }
-keepclassmembers,includedescriptorclasses class * { native <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.UIProp <fields>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp <methods>; }
-keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup <methods>; }
-dontwarn com.facebook.react.**
# TextLayoutBuilder uses a non-public Android constructor within StaticLayout.
# See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details.
-dontwarn android.text.StaticLayout
# okhttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
# okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**

View File

@@ -11,6 +11,11 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove"/>
<!-- Make these features optional to enable Chromebooks -->
<!-- https://github.com/laurent22/joplin/issues/37 -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<!-- ==================================== -->
<!-- START react-native-push-notification -->
<!-- ==================================== -->

View File

@@ -1,11 +1,19 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = "27.0.3"
minSdkVersion = 16
compileSdkVersion = 27
targetSdkVersion = 26
supportLibVersion = "27.1.1"
}
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.3'
classpath 'com.android.tools.build:gradle:3.1.4'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@@ -22,6 +30,25 @@ allprojects {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
jcenter()
jcenter() // Was added by me - still needed?
google()
}
}
subprojects {
afterEvaluate {project ->
if (project.hasProperty("android")) {
android {
compileSdkVersion 26
buildToolsVersion "27.0.3"
}
}
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.4'
distributionUrl = distributionUrl.replace("bin", "all")
}

View File

@@ -16,5 +16,3 @@
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
android.useDeprecatedNdk=true

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
##############################################################################
##
@@ -6,47 +6,6 @@
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
@@ -61,9 +20,49 @@ while [ -h "$PRG" ] ; do
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -90,7 +89,7 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -114,6 +113,7 @@ fi
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
@@ -154,11 +154,19 @@ if $cygwin ; then
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
APP_ARGS=$(save "$@")
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
fi
exec "$JAVACMD" "$@"

View File

@@ -8,14 +8,14 @@
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -46,10 +46,9 @@ echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
@rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
@@ -60,11 +59,6 @@ set _SKIP=2
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line

View File

@@ -1,51 +1,3 @@
const { main } = require('./main.js');
main();
// const React = require('react'); const Component = React.Component;
// import {
// AppRegistry,
// StyleSheet,
// Text,
// View
// } from 'react-native';
// module.exports = default class Joplin extends Component {;
// render() {
// return (
// <View style={styles.container}>
// <Text style={styles.welcome}>
// Welcome to React Native!
// </Text>
// <Text style={styles.instructions}>
// To get started, edit index.ios.js
// </Text>
// <Text style={styles.instructions}>
// Press Cmd+R to reload,{'\n'}
// Cmd+D or shake for dev menu
// </Text>
// </View>
// );
// }
// }
// const styles = StyleSheet.create({
// container: {
// flex: 1,
// justifyContent: 'center',
// alignItems: 'center',
// backgroundColor: '#F5FCFF',
// },
// welcome: {
// fontSize: 20,
// textAlign: 'center',
// margin: 10,
// },
// instructions: {
// textAlign: 'center',
// color: '#333333',
// marginBottom: 5,
// },
// });
// AppRegistry.registerComponent('Joplin', () => Joplin);
main();

View File

@@ -5,6 +5,7 @@
};
objectVersion = 46;
objects = {
/* Begin PBXBuildFile section */
00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; };
00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; };
@@ -27,6 +28,7 @@
5E9157361DD0AC6A00FF2AA8 /* libRCTAnimation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E9157331DD0AC6500FF2AA8 /* libRCTAnimation.a */; };
725A77EC604947A0AFF12C2B /* libRNFetchBlob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F5E37D05726A4A08B2EE323A /* libRNFetchBlob.a */; };
73F8D08845494D1396B6CD0B /* Entypo.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1F79F2CD7CED446B986A6252 /* Entypo.ttf */; };
82C61D3DAE0A4666883001E9 /* libRNFileViewer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCDC2774CD86466F897D88E2 /* libRNFileViewer.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
929CA5ABC80E4D398AFC4E44 /* libSQLite.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 87BABCF4ED0A406B9546CCE9 /* libSQLite.a */; };
A47605BC216B4DB3B3C2ED42 /* FontAwesome.ttf in Resources */ = {isa = PBXBuildFile; fileRef = E2638D52624B477FABB52B8F /* FontAwesome.ttf */; };
@@ -43,7 +45,6 @@
EC11356C90E9419799A2626F /* EvilIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 51BCEC3BC28046C8BB19531F /* EvilIcons.ttf */; };
F3D0BB525E6C490294D73075 /* libRNSecureRandom.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */; };
FBF57CE2F0F448FA9A8985E2 /* libsqlite3.0.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0EB8BCAEA9AA41CAAE460443 /* libsqlite3.0.tbd */; };
82C61D3DAE0A4666883001E9 /* libRNFileViewer.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCDC2774CD86466F897D88E2 /* libRNFileViewer.a */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -334,6 +335,13 @@
remoteGlobalIDString = 9936F32F1F5F2E5B0010BF04;
remoteInfo = "privatedata-tvOS";
};
4D8B719B2163E8C500136BBC /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 59F5448FAF7345F8B568BD00 /* RNFileViewer.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = 134814201AA4EA6300B7C361;
remoteInfo = RNFileViewer;
};
4DA7F80C1FC1DA9C00353191 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = A4716DB8654B431D894F89E1 /* RNImagePicker.xcodeproj */;
@@ -415,6 +423,7 @@
4DDA31011FC88EEA00B5A80D /* RCTPushNotification.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTPushNotification.xcodeproj; path = "../node_modules/react-native/Libraries/PushNotificationIOS/RCTPushNotification.xcodeproj"; sourceTree = "<group>"; };
508DD20D1EA341CA8F730F22 /* libRCTImageResizer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRCTImageResizer.a; sourceTree = "<group>"; };
51BCEC3BC28046C8BB19531F /* EvilIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = EvilIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/EvilIcons.ttf"; sourceTree = "<group>"; };
59F5448FAF7345F8B568BD00 /* RNFileViewer.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNFileViewer.xcodeproj; path = "../node_modules/react-native-file-viewer/ios/RNFileViewer.xcodeproj"; sourceTree = "<group>"; };
5E91572D1DD0AC6500FF2AA8 /* RCTAnimation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTAnimation.xcodeproj; path = "../node_modules/react-native/Libraries/NativeAnimation/RCTAnimation.xcodeproj"; sourceTree = "<group>"; };
69B8EE98BFBC4AABA4885BB0 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = "<group>"; };
711CBD21F0894B83A2D8E234 /* RNVectorIcons.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNVectorIcons.xcodeproj; path = "../node_modules/react-native-vector-icons/RNVectorIcons.xcodeproj"; sourceTree = "<group>"; };
@@ -429,6 +438,7 @@
B8C44254005A4B80AD5CA558 /* RNFetchBlob.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RNFetchBlob.xcodeproj; path = "../node_modules/react-native-fetch-blob/ios/RNFetchBlob.xcodeproj"; sourceTree = "<group>"; };
C77F905EF22646F39B673572 /* MaterialCommunityIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = MaterialCommunityIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/MaterialCommunityIcons.ttf"; sourceTree = "<group>"; };
CC3CF4044A914711B8D30D1A /* Zocial.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Zocial.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Zocial.ttf"; sourceTree = "<group>"; };
CCDC2774CD86466F897D88E2 /* libRNFileViewer.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFileViewer.a; sourceTree = "<group>"; };
CCDE9E9AF09B45F391B1C2AF /* SQLite.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = SQLite.xcodeproj; path = "../node_modules/react-native-sqlite-storage/src/ios/SQLite.xcodeproj"; sourceTree = "<group>"; };
DF1C50EBC11E46A3AF87F80A /* RCTImageResizer.xcodeproj */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = "wrapper.pb-project"; name = RCTImageResizer.xcodeproj; path = "../node_modules/react-native-image-resizer/ios/RCTImageResizer.xcodeproj"; sourceTree = "<group>"; };
E2638D52624B477FABB52B8F /* FontAwesome.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = FontAwesome.ttf; path = "../node_modules/react-native-vector-icons/Fonts/FontAwesome.ttf"; sourceTree = "<group>"; };
@@ -436,8 +446,6 @@
F5E37D05726A4A08B2EE323A /* libRNFetchBlob.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNFetchBlob.a; sourceTree = "<group>"; };
FD370E24D76E461D960DD85D /* Feather.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Feather.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Feather.ttf"; sourceTree = "<group>"; };
FF411B45E68B4A8CBCC35777 /* Ionicons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = Ionicons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/Ionicons.ttf"; sourceTree = "<group>"; };
59F5448FAF7345F8B568BD00 /* RNFileViewer.xcodeproj */ = {isa = PBXFileReference; name = "RNFileViewer.xcodeproj"; path = "../node_modules/react-native-file-viewer/ios/RNFileViewer.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
CCDC2774CD86466F897D88E2 /* libRNFileViewer.a */ = {isa = PBXFileReference; name = "libRNFileViewer.a"; path = "libRNFileViewer.a"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -602,6 +610,7 @@
381C047F2739439CB3E6452A /* libRNVectorIcons.a */,
44A39642217548C8ADA91CBA /* libRNImagePicker.a */,
22647ACF9A4C45918C44C599 /* libRNSecureRandom.a */,
CCDC2774CD86466F897D88E2 /* libRNFileViewer.a */,
);
name = "Recovered References";
sourceTree = "<group>";
@@ -655,6 +664,14 @@
name = Products;
sourceTree = "<group>";
};
4D8B71982163E8C500136BBC /* Products */ = {
isa = PBXGroup;
children = (
4D8B719C2163E8C500136BBC /* libRNFileViewer.a */,
);
name = Products;
sourceTree = "<group>";
};
4DA7F8091FC1DA9C00353191 /* Products */ = {
isa = PBXGroup;
children = (
@@ -898,6 +915,10 @@
ProductGroup = 4D2A85AF1FBCE3AC0028537D /* Products */;
ProjectRef = B8C44254005A4B80AD5CA558 /* RNFetchBlob.xcodeproj */;
},
{
ProductGroup = 4D8B71982163E8C500136BBC /* Products */;
ProjectRef = 59F5448FAF7345F8B568BD00 /* RNFileViewer.xcodeproj */;
},
{
ProductGroup = 4D2A85B11FBCE3AC0028537D /* Products */;
ProjectRef = 9D8705D0D07C4A098FD912EB /* RNFS.xcodeproj */;
@@ -1214,6 +1235,13 @@
remoteRef = 4D7F8DA620A32BA0008B757D /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4D8B719C2163E8C500136BBC /* libRNFileViewer.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = libRNFileViewer.a;
remoteRef = 4D8B719B2163E8C500136BBC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
4DA7F80D1FC1DA9C00353191 /* libRNImagePicker.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@@ -1350,7 +1378,7 @@
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)..\node_modules\neact-native-image-pickerios",
"$(SRCROOT)..\node_modules\neact-native-securerandomios",
"$(SRCROOT)\..\node_modules\react-native-file-viewer\ios",
"$(SRCROOT)..\node_modules\neact-native-file-viewerios",
);
INFOPLIST_FILE = Joplin/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
@@ -1392,7 +1420,7 @@
"$(SRCROOT)/../node_modules/react-native-vector-icons/RNVectorIconsManager",
"$(SRCROOT)..\node_modules\neact-native-image-pickerios",
"$(SRCROOT)..\node_modules\neact-native-securerandomios",
"$(SRCROOT)\..\node_modules\react-native-file-viewer\ios",
"$(SRCROOT)..\node_modules\neact-native-file-viewerios",
);
INFOPLIST_FILE = Joplin/Info.plist;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";

View File

@@ -215,7 +215,7 @@ class BaseApplication {
if (parentType === Folder.modelType()) {
notes = await Note.previews(parentId, options);
} else if (parentType === Tag.modelType()) {
notes = await Tag.notes(parentId);
notes = await Tag.notes(parentId, options);
} else if (parentType === BaseModel.TYPE_SEARCH) {
let fields = Note.previewFields();
let search = BaseModel.byId(state.searches, parentId);
@@ -494,6 +494,12 @@ class BaseApplication {
setLocale(Setting.value('locale'));
}
if (!Setting.value('api.token')) {
EncryptionService.instance().randomHexString(64).then((token) => {
Setting.setValue('api.token', token);
});
}
time.setDateFormat(Setting.value('dateFormat'));
time.setTimeFormat(Setting.value('timeFormat'));

View File

@@ -1,18 +1,12 @@
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');
const ApiResponse = require('lib/services/rest/ApiResponse');
const multiparty = require('multiparty');
class ClipperServer {
@@ -21,6 +15,9 @@ class ClipperServer {
this.startState_ = 'idle';
this.server_ = null;
this.port_ = null;
this.api_ = new Api(() => {
return Setting.value('api.token');
});
}
static instance() {
@@ -31,6 +28,7 @@ class ClipperServer {
setLogger(l) {
this.logger_ = l;
this.api_.setLogger(l);
}
logger() {
@@ -64,133 +62,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');
@@ -222,13 +93,14 @@ class ClipperServer {
this.server_.on('request', async (request, response) => {
const writeCorsHeaders = (code, contentType = "application/json") => {
response.writeHead(code, {
const writeCorsHeaders = (code, contentType = "application/json", additionalHeaders = null) => {
const headers = Object.assign({}, {
"Content-Type": contentType,
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
});
}, additionalHeaders ? additionalHeaders : {});
response.writeHead(code, headers);
}
const writeResponseJson = (code, object) => {
@@ -243,67 +115,81 @@ class ClipperServer {
response.end();
}
const requestId = Date.now();
this.logger().info('Request (' + requestId + '): ' + request.method + ' ' + request.url);
const writeResponseInstance = (code, instance) => {
if (instance.type === 'attachment') {
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
'Content-disposition': 'attachment; filename=' + filename,
'Content-Length': instance.body.length,
});
response.end(instance.body);
} else {
throw new Error('Not implemented');
}
}
const writeResponse = (code, response) => {
if (response instanceof ApiResponse) {
writeResponseInstance(code, response);
} else 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 = '', files = []) => {
try {
const response = await this.api_.route(request.method, url.pathname, url.query, body, files);
writeResponse(200, response);
} catch (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') {
let body = '';
const contentType = request.headers['content-type'] ? request.headers['content-type'] : '';
request.on('data', (data) => {
body += data;
});
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 });
}
});
} else {
return writeResponseJson(404, { errorCode: 'not_found' });
}
} else if (request.method === 'OPTIONS') {
if (request.method === 'OPTIONS') {
writeCorsHeaders(200);
response.end();
} else {
return writeResponseJson(405, { errorCode: 'method_not_allowed' });
if (contentType.indexOf('multipart/form-data') === 0) {
const form = new multiparty.Form();
form.parse(request, function(error, fields, files) {
if (error) {
writeResponse(error.httpCode ? error.httpCode : 500, error.message);
return;
} else {
execRequest(
request,
fields && fields.props && fields.props.length ? fields.props[0] : '',
files && files.data ? files.data : []
);
}
});
} else {
if (request.method === 'POST') {
let body = '';
request.on('data', (data) => {
body += data;
});
request.on('end', async () => {
execRequest(request, body);
});
} else {
execRequest(request);
}
}
}
});
this.server_.on('close', () => {
});
enableServerDestroy(this.server_);
this.logger().info('Starting Clipper server on port ' + this.port_);

View File

@@ -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 {

View File

@@ -116,6 +116,9 @@ class NoteBodyViewer extends Component {
scalesPageToFit={Platform.OS !== 'ios'}
style={webViewStyle}
source={source}
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
mixedContentMode="always"
allowFileAccess={true}
onLoadEnd={() => this.onLoadEnd()}
onError={() => reg.logger().error('WebView error') }
onMessage={(event) => {

View File

@@ -218,13 +218,14 @@ class NoteScreenComponent extends BaseScreenComponent {
BackButtonService.removeHandler(this.backHandler);
NavService.removeHandler(this.navHandler);
if (this.state.fromShare) {
if (Platform.OS !== 'ios' && this.state.fromShare) {
ShareExtension.close();
}
}
title_changeText(text) {
shared.noteComponent_change(this, 'title', text);
this.setState({ newAndNoTitleChangeNoteId: null });
}
body_changeText(text) {

View File

@@ -114,7 +114,7 @@ class NotesScreenComponent extends BaseScreenComponent {
if (props.notesParentType == 'Folder') {
notes = await Note.previews(props.selectedFolderId, options);
} else {
notes = await Tag.notes(props.selectedTagId); // TODO: should also return previews
notes = await Tag.notes(props.selectedTagId, options); // TODO: should also return previews
}
this.props.dispatch({

View File

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

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