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() + } else { + webClipperStatusComps.push(

{_('The web clipper service is not enabled.')}

) + webClipperStatusComps.push() + } + + 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