diff --git a/Clipper/joplin-webclipper/manifest.json b/Clipper/joplin-webclipper/manifest.json
index 8d69f88618..8e85d21713 100644
--- a/Clipper/joplin-webclipper/manifest.json
+++ b/Clipper/joplin-webclipper/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "Joplin Web Clipper",
- "version": "1.0",
+ "version": "1.0.1",
"description": "Gets and saves content from your browser to Joplin.",
diff --git a/Clipper/joplin-webclipper/package-lock.json b/Clipper/joplin-webclipper/package-lock.json
index 9f11367b40..d53f6ffae0 100644
--- a/Clipper/joplin-webclipper/package-lock.json
+++ b/Clipper/joplin-webclipper/package-lock.json
@@ -4,10 +4,42 @@
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "fs-extra": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
+ "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=",
+ "dev": true
+ },
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
"readability-node": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/readability-node/-/readability-node-0.1.0.tgz",
"integrity": "sha1-DUBacMLCFZRKf0qbX3UGzQWpsao="
+ },
+ "universalify": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz",
+ "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=",
+ "dev": true
}
}
}
diff --git a/Clipper/joplin-webclipper/package.json b/Clipper/joplin-webclipper/package.json
index 506cd5fa67..1dcc504269 100644
--- a/Clipper/joplin-webclipper/package.json
+++ b/Clipper/joplin-webclipper/package.json
@@ -10,5 +10,8 @@
"license": "MIT",
"dependencies": {
"readability-node": "^0.1.0"
+ },
+ "devDependencies": {
+ "fs-extra": "^6.0.1"
}
}
diff --git a/Clipper/joplin-webclipper/popup/src/App.js b/Clipper/joplin-webclipper/popup/src/App.js
index 84c3175197..847cac170f 100644
--- a/Clipper/joplin-webclipper/popup/src/App.js
+++ b/Clipper/joplin-webclipper/popup/src/App.js
@@ -2,7 +2,6 @@ import React, { Component } from 'react';
import './App.css';
const { connect } = require('react-redux');
-const Global = require('./Global');
const { bridge } = require('./bridge');
class AppComponent extends Component {
diff --git a/Clipper/joplin-webclipper/popup/src/index.js b/Clipper/joplin-webclipper/popup/src/index.js
index ef2d72242b..aef6e0a358 100644
--- a/Clipper/joplin-webclipper/popup/src/index.js
+++ b/Clipper/joplin-webclipper/popup/src/index.js
@@ -3,7 +3,7 @@ import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
-const { connect, Provider } = require('react-redux');
+const { Provider } = require('react-redux');
const { bridge } = require('./bridge');
const { createStore } = require('redux');
diff --git a/Clipper/joplin-webclipper/popup/src/randomClipperPort.js b/Clipper/joplin-webclipper/popup/src/randomClipperPort.js
index 9a1599d2e5..93d1d8c331 100644
--- a/Clipper/joplin-webclipper/popup/src/randomClipperPort.js
+++ b/Clipper/joplin-webclipper/popup/src/randomClipperPort.js
@@ -34,9 +34,6 @@ const reservedPorts = [1024, 1027, 1028, 1029, 1058, 1059, 1080, 1085, 1098, 109
// From https://github.com/coverslide/node-alea
const AleaModule = function () {
-
- 'use strict';
-
// importState to sync generator states
Alea.importState = function(i){
var random = new Alea();
@@ -54,8 +51,8 @@ const AleaModule = function () {
var s2 = 0;
var c = 1;
- if (args.length == 0) {
- args = [+new Date];
+ if (args.length === 0) {
+ args = [+new Date()];
}
var mash = Mash();
s0 = mash(' ');
diff --git a/ElectronClient/app/app.js b/ElectronClient/app/app.js
index 9638df901f..8d99ca6f84 100644
--- a/ElectronClient/app/app.js
+++ b/ElectronClient/app/app.js
@@ -23,6 +23,7 @@ const DecryptionWorker = require('lib/services/DecryptionWorker');
const InteropService = require('lib/services/InteropService');
const InteropServiceHelper = require('./InteropServiceHelper.js');
const ResourceService = require('lib/services/ResourceService');
+const ClipperServer = require('lib/ClipperServer');
const { bridge } = require('electron').remote.require('./bridge');
const Menu = bridge().Menu;
@@ -459,6 +460,14 @@ class Application extends BaseApplication {
}, {
type: 'separator',
screens: ['Main'],
+ },{
+ label: _('Web clipper options'),
+ click: () => {
+ this.dispatch({
+ type: 'NAV_GO',
+ routeName: 'ClipperConfig',
+ });
+ }
},{
label: _('Encryption options'),
click: () => {
@@ -662,6 +671,17 @@ class Application extends BaseApplication {
DecryptionWorker.instance().scheduleStart();
});
}
+
+ const clipperLogger = new Logger();
+ clipperLogger.addTarget('file', { path: Setting.value('profileDir') + '/log-clipper.txt' });
+ clipperLogger.addTarget('console');
+
+ ClipperServer.instance().setLogger(clipperLogger);
+ ClipperServer.instance().setDispatch(this.store().dispatch);
+
+ if (Setting.value('clipperServer.autoStart')) {
+ ClipperServer.instance().start();
+ }
}
}
diff --git a/ElectronClient/app/gui/ClipperConfigScreen.jsx b/ElectronClient/app/gui/ClipperConfigScreen.jsx
new file mode 100644
index 0000000000..5af43e92e2
--- /dev/null
+++ b/ElectronClient/app/gui/ClipperConfigScreen.jsx
@@ -0,0 +1,106 @@
+const React = require('react');
+const { connect } = require('react-redux');
+const { reg } = require('lib/registry.js');
+const { bridge } = require('electron').remote.require('./bridge');
+const { Header } = require('./Header.min.js');
+const { themeStyle } = require('../theme.js');
+const { _ } = require('lib/locale.js');
+const ClipperServer = require('lib/ClipperServer');
+const Setting = require('lib/models/Setting');
+
+class ClipperConfigScreenComponent extends React.Component {
+
+ disableClipperServer_click() {
+ Setting.setValue('clipperServer.autoStart', false);
+ ClipperServer.instance().stop();
+ }
+
+ enableClipperServer_click() {
+ Setting.setValue('clipperServer.autoStart', true);
+ ClipperServer.instance().start();
+ }
+
+ chromeButton_click() {
+ bridge().openExternal("https://chrome.google.com/webstore/detail/joplin-web-clipper/alofnhikmmkdbbbgpnglcpdollgjjfek")
+ }
+
+ firefoxButton_click() {
+
+ }
+
+ render() {
+ const style = this.props.style;
+ const theme = themeStyle(this.props.theme);
+
+ const headerStyle = {
+ width: style.width,
+ };
+
+ const stepBoxStyle = {
+ border: "1px solid #ccc",
+ padding: 15,
+ paddingTop: 0,
+ marginBottom: 15,
+ };
+
+ let webClipperStatusComps = [];
+
+ if (this.props.clipperServerAutoStart) {
+ webClipperStatusComps.push(
{_('The web clipper service is already enabled and set to auto-start.')}
)
+ if (this.props.clipperServer.startState === 'started') {
+ webClipperStatusComps.push({_('Status: Started on port %d', this.props.clipperServer.port)}
)
+ } else {
+ webClipperStatusComps.push({_('Status: %s', this.props.clipperServer.startState)}
)
+ }
+ webClipperStatusComps.push({_('Disable Web Clipper Service')} )
+ } else {
+ webClipperStatusComps.push({_('The web clipper service is not enabled.')}
)
+ webClipperStatusComps.push({_('Enable Web Clipper Service')} )
+ }
+
+ const firefoxComp = (
+
+
+
+ );
+
+ return (
+
+
+
+
{_('Joplin Web Clipper allows saving web pages and screenshots from your browser to Joplin.')}
+
{_('In order to use the web clipper, you need to do the following:')}
+
+
+
{_('Step 1: Enable the clipper service')}
+
{_('This service allows the browser extension to communicate with Joplin. When enabling it your firewall may ask you to give permission to Joplin to listen to a particular port.')}
+
+ {webClipperStatusComps}
+
+
+
+
+
{_('Step 2: Install the extension')}
+
{_('Download and install the relevant extension for your browser:')}
+
+
+
+
+
+
+ );
+ }
+
+}
+
+const mapStateToProps = (state) => {
+ return {
+ theme: state.settings.theme,
+ clipperServer: state.clipperServer,
+ clipperServerAutoStart: state.settings['clipperServer.autoStart'],
+ };
+};
+
+const ClipperConfigScreen = connect(mapStateToProps)(ClipperConfigScreenComponent);
+
+module.exports = { ClipperConfigScreen };
\ No newline at end of file
diff --git a/ElectronClient/app/gui/Root.jsx b/ElectronClient/app/gui/Root.jsx
index ade1b0b027..6bd5f3bec7 100644
--- a/ElectronClient/app/gui/Root.jsx
+++ b/ElectronClient/app/gui/Root.jsx
@@ -13,6 +13,7 @@ const { StatusScreen } = require('./StatusScreen.min.js');
const { ImportScreen } = require('./ImportScreen.min.js');
const { ConfigScreen } = require('./ConfigScreen.min.js');
const { EncryptionConfigScreen } = require('./EncryptionConfigScreen.min.js');
+const { ClipperConfigScreen } = require('./ClipperConfigScreen.min.js');
const { Navigator } = require('./Navigator.min.js');
const { app } = require('../app');
@@ -86,6 +87,7 @@ class RootComponent extends React.Component {
Config: { screen: ConfigScreen, title: () => _('Options') },
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
EncryptionConfig: { screen: EncryptionConfigScreen, title: () => _('Encryption Options') },
+ ClipperConfig: { screen: ClipperConfigScreen, title: () => _('Clipper Options') },
};
return (
diff --git a/ElectronClient/app/package-lock.json b/ElectronClient/app/package-lock.json
index d146acfc7c..c1abc14955 100644
--- a/ElectronClient/app/package-lock.json
+++ b/ElectronClient/app/package-lock.json
@@ -5452,6 +5452,11 @@
"semver": "5.4.1"
}
},
+ "server-destroy": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz",
+ "integrity": "sha1-8Tv5KOQrnD55OD5hzDmYtdFObN0="
+ },
"set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
diff --git a/ElectronClient/app/package.json b/ElectronClient/app/package.json
index 576bafa957..091a438a13 100644
--- a/ElectronClient/app/package.json
+++ b/ElectronClient/app/package.json
@@ -114,6 +114,7 @@
"read-chunk": "^2.1.0",
"readability-node": "^0.1.0",
"redux": "^3.7.2",
+ "server-destroy": "^1.0.1",
"smalltalk": "^2.5.1",
"sprintf-js": "^1.1.1",
"sqlite3": "^3.1.13",
diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js
index 8dd5e43e5c..b5b22e94c7 100644
--- a/ReactNativeClient/lib/BaseApplication.js
+++ b/ReactNativeClient/lib/BaseApplication.js
@@ -34,7 +34,6 @@ const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
const EncryptionService = require('lib/services/EncryptionService');
const DecryptionWorker = require('lib/services/DecryptionWorker');
const BaseService = require('lib/services/BaseService');
-const ClipperServer = require('lib/ClipperServer');
SyncTargetRegistry.addClass(SyncTargetFilesystem);
SyncTargetRegistry.addClass(SyncTargetOneDrive);
@@ -493,13 +492,6 @@ class BaseApplication {
// await this.testing();process.exit();
- const clipperLogger = new Logger();
- clipperLogger.addTarget('file', { path: profileDir + '/log-clipper.txt' });
- clipperLogger.addTarget('console');
- this.clipperServer_ = new ClipperServer();
- this.clipperServer_.setLogger(clipperLogger);
- this.clipperServer_.start();
-
return argv;
}
diff --git a/ReactNativeClient/lib/ClipperServer.js b/ReactNativeClient/lib/ClipperServer.js
index 13e9afb72f..3740b535fc 100644
--- a/ReactNativeClient/lib/ClipperServer.js
+++ b/ReactNativeClient/lib/ClipperServer.js
@@ -12,11 +12,21 @@ 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');
class ClipperServer {
constructor() {
this.logger_ = new Logger();
+ this.startState_ = 'idle';
+ this.server_ = null;
+ this.port_ = null;
+ }
+
+ static instance() {
+ if (this.instance_) return this.instance_;
+ this.instance_ = new ClipperServer();
+ return this.instance_;
}
setLogger(l) {
@@ -27,6 +37,41 @@ class ClipperServer {
return this.logger_;
}
+ setDispatch(d) {
+ this.dispatch_ = d;
+ }
+
+ dispatch(action) {
+ if (!this.dispatch_) throw new Error('dispatch not set!');
+ this.dispatch_(action);
+ }
+
+ setStartState(v) {
+ if (this.startState_ === v) return;
+ this.startState_ = v;
+ this.dispatch({
+ type: 'CLIPPER_SERVER_SET',
+ startState: v,
+ });
+ }
+
+ setPort(v) {
+ if (this.port_ === v) return;
+ this.port_ = v;
+ this.dispatch({
+ type: 'CLIPPER_SERVER_SET',
+ port: v,
+ });
+ }
+
+ // startState() {
+ // return this.startState_;
+ // }
+
+ // port() {
+ // return this.port_;
+ // }
+
htmlToMdParser() {
if (this.htmlToMdParser_) return this.htmlToMdParser_;
this.htmlToMdParser_ = new HtmlToMd();
@@ -167,18 +212,22 @@ class ClipperServer {
}
async start() {
- let port = null;
+ this.setPort(null);
+
+ this.setStartState('starting');
try {
- port = await this.findAvailablePort();
+ const p = await this.findAvailablePort();
+ this.setPort(p);
} catch (error) {
+ this.setStartState('idle');
this.logger().error(error);
return;
}
- const server = require('http').createServer();
+ this.server_ = require('http').createServer();
- server.on('request', (request, response) => {
+ this.server_.on('request', (request, response) => {
const writeCorsHeaders = (code) => {
response.writeHead(code, {
@@ -253,13 +302,24 @@ class ClipperServer {
}
});
- server.on('close', () => {
+ this.server_.on('close', () => {
});
- this.logger().info('Starting Clipper server on port ' + port);
+ enableServerDestroy(this.server_);
- server.listen(port);
+ this.logger().info('Starting Clipper server on port ' + this.port_);
+
+ this.server_.listen(this.port_);
+
+ this.setStartState('started');
+ }
+
+ async stop() {
+ this.server_.destroy();
+ this.server_ = null;
+ this.setStartState('idle');
+ this.setPort(null);
}
}
diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js
index 3c2f298ce1..1453d17d7d 100644
--- a/ReactNativeClient/lib/models/Setting.js
+++ b/ReactNativeClient/lib/models/Setting.js
@@ -102,6 +102,7 @@ class Setting extends BaseModel {
'style.zoom': {value: "100", type: Setting.TYPE_INT, public: true, appTypes: ['desktop'], label: () => _('Global zoom percentage'), minimum: "50", maximum: "500", step: "10"},
'style.editor.fontFamily': {value: "", type: Setting.TYPE_STRING, public: true, appTypes: ['desktop'], label: () => _('Editor font family'), description: () => _('The font name will not be checked. If incorrect or empty, it will default to a generic monospace font.')},
'autoUpdateEnabled': { value: true, type: Setting.TYPE_BOOL, public: true, appTypes: ['desktop'], label: () => _('Automatically update the application') },
+ 'clipperServer.autoStart': { value: false, type: Setting.TYPE_BOOL, public: false },
'sync.interval': { value: 300, type: Setting.TYPE_INT, isEnum: true, public: true, label: () => _('Synchronisation interval'), options: () => {
return {
0: _('Disabled'),
diff --git a/ReactNativeClient/lib/randomClipperPort.js b/ReactNativeClient/lib/randomClipperPort.js
index 9a1599d2e5..93d1d8c331 100644
--- a/ReactNativeClient/lib/randomClipperPort.js
+++ b/ReactNativeClient/lib/randomClipperPort.js
@@ -34,9 +34,6 @@ const reservedPorts = [1024, 1027, 1028, 1029, 1058, 1059, 1080, 1085, 1098, 109
// From https://github.com/coverslide/node-alea
const AleaModule = function () {
-
- 'use strict';
-
// importState to sync generator states
Alea.importState = function(i){
var random = new Alea();
@@ -54,8 +51,8 @@ const AleaModule = function () {
var s2 = 0;
var c = 1;
- if (args.length == 0) {
- args = [+new Date];
+ if (args.length === 0) {
+ args = [+new Date()];
}
var mash = Mash();
s0 = mash(' ');
diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js
index ffe9c0343b..e3c4067902 100644
--- a/ReactNativeClient/lib/reducer.js
+++ b/ReactNativeClient/lib/reducer.js
@@ -27,6 +27,10 @@ const defaultState = {
hasDisabledSyncItems: false,
newNote: null,
collapsedFolderIds: [],
+ clipperServer: {
+ startState: 'idle',
+ port: null,
+ },
};
const stateUtils = {};
@@ -525,7 +529,16 @@ const reducer = (state = defaultState, action) => {
newState = Object.assign({}, state);
newState.newNote = action.item;
- break;
+ break;
+
+ case 'CLIPPER_SERVER_SET':
+
+ newState = Object.assign({}, state);
+ const clipperServer = Object.assign({}, newState.clipperServer);
+ if ('startState' in action) clipperServer.startState = action.startState;
+ if ('port' in action) clipperServer.port = action.port;
+ newState.clipperServer = clipperServer;
+ break;
}
} catch (error) {
diff --git a/Tools/release-clipper.js b/Tools/release-clipper.js
new file mode 100644
index 0000000000..3f49401fb9
--- /dev/null
+++ b/Tools/release-clipper.js
@@ -0,0 +1,46 @@
+const fs = require('fs-extra');
+const { execCommand } = require('./tool-utils.js');
+
+const clipperDir = __dirname + '/../Clipper/joplin-webclipper';
+
+async function copyDir(baseSourceDir, sourcePath, baseDestDir) {
+ await fs.mkdirp(baseDestDir + '/' + sourcePath);
+ await fs.copy(baseSourceDir + '/' + sourcePath, baseDestDir + '/' + sourcePath);
+}
+
+async function copyToDist(distDir) {
+ await copyDir(clipperDir, 'popup/build', distDir);
+ await copyDir(clipperDir, 'content_scripts', distDir);
+ await copyDir(clipperDir, 'icons', distDir);
+ await fs.copy(clipperDir + '/background.js', distDir + '/background.js');
+ await fs.copy(clipperDir + '/main.js', distDir + '/main.js');
+ await fs.copy(clipperDir + '/manifest.json', distDir + '/manifest.json');
+
+ await fs.remove(distDir + '/popup/build/manifest.json');
+}
+
+async function main() {
+ process.chdir(clipperDir + '/popup');
+
+ console.info(await execCommand('npm run build'));
+
+ const dists = [
+ {
+ dir: clipperDir + '/dist/chrometest',
+ name: 'chrome',
+ }
+ ];
+
+ for (let i = 0; i < dists.length; i++) {
+ const dist = dists[i];
+ await copyToDist(dist.dir);
+ process.chdir(dist.dir);
+ console.info(await execCommand('7z a -tzip ' + dist.name + '.zip *'));
+ }
+}
+
+main().catch((error) => {
+ console.error('Fatal error');
+ console.error(error);
+ process.exit(1);
+});
\ No newline at end of file