diff --git a/app/css/hyperterm.css b/app/css/hyperterm.css index f4610ed9..39abf185 100644 --- a/app/css/hyperterm.css +++ b/app/css/hyperterm.css @@ -63,3 +63,23 @@ header { .resize-indicator.showing { opacity: 1; } + +.update-indicator { + background: rgba(255, 51, 76, .8); + padding: 6px 14px; + color: #fff; + font: 11px Menlo; + position: fixed; + bottom: 20px; + right: 20px; + opacity: 0; + transition: opacity 150ms ease-in; +} + +.update-indicator a { + color: #fff; +} + +.update-indicator.showing { + opacity: 1; +} diff --git a/app/hyperterm.js b/app/hyperterm.js index b44b24ed..4d2d3fa8 100644 --- a/app/hyperterm.js +++ b/app/hyperterm.js @@ -6,6 +6,7 @@ import classes from 'classnames'; import getTextMetrics from './text-metrics'; import shallowCompare from 'react-addons-shallow-compare'; import React, { Component } from 'react'; +import UpdateChecker from './update-checker'; export default class HyperTerm extends Component { constructor () { @@ -19,7 +20,8 @@ export default class HyperTerm extends Component { active: null, activeMarkers: [], mac: /Mac/.test(navigator.userAgent), - resizeIndicatorShowing: false + resizeIndicatorShowing: false, + updateVersion: null }; // we set this to true when the first tab @@ -34,6 +36,7 @@ export default class HyperTerm extends Component { this.onResize = this.onResize.bind(this); this.onChange = this.onChange.bind(this); + this.openExternal = this.openExternal.bind(this); this.focusActive = this.focusActive.bind(this); this.onHeaderMouseDown = this.onHeaderMouseDown.bind(this); @@ -80,9 +83,19 @@ export default class HyperTerm extends Component {
{ this.state.cols }x{ this.state.rows }
+
+ Update available ({ this.state.updateVersion }). + {' '} + Download +
; } + openExternal (ev) { + ev.preventDefault(); + this.rpc.emit('open external', { url: ev.target.href }); + } + requestTab () { this.rpc.emit('new', this.getDimensions()); } @@ -156,6 +169,7 @@ export default class HyperTerm extends Component { componentDidMount () { this.rpc = new RPC(); + this.updateChecker = new UpdateChecker(this.onUpdateAvailable.bind(this)); this.setState(this.getDimensions()); // open a new tab upon mounting @@ -218,6 +232,10 @@ export default class HyperTerm extends Component { Mousetrap.bind('command+alt+right', this.moveRight); } + onUpdateAvailable (updateVersion) { + this.setState({ updateVersion }); + } + moveTo (n) { if (this.state.sessions[n]) { this.setActive(n); @@ -359,5 +377,6 @@ export default class HyperTerm extends Component { this.rpc.destroy(); clearTimeout(this.resizeIndicatorTimeout); Mousetrap.reset(); + this.updateChecker.destroy(); } } diff --git a/app/package.json b/app/package.json index b1e51117..7f9c90f8 100644 --- a/app/package.json +++ b/app/package.json @@ -7,7 +7,9 @@ "mousetrap": "1.6.0", "classnames": "2.2.5", "react": "15.1.0", - "react-dom": "15.1.0" + "react-dom": "15.1.0", + "semver-compare": "^1.0.0", + "json-loader": "^0.5.4" }, "devDependencies": { "eslint": "2.13.1", @@ -30,10 +32,19 @@ ], "rules": { "yoda": 0, - "semi": [2, "always"], + "semi": [ + 2, + "always" + ], "no-unused-vars": 2, "no-extra-semi": 2, - "semi-spacing": [2, { "before": false, "after": true }], + "semi-spacing": [ + 2, + { + "before": false, + "after": true + } + ], "react/jsx-uses-react": 1, "react/jsx-uses-vars": 1 }, @@ -54,4 +65,4 @@ "lint": "eslint *.js", "build": "NODE_ENV=production webpack" } -} +} \ No newline at end of file diff --git a/app/update-checker.js b/app/update-checker.js new file mode 100644 index 00000000..0e8a907b --- /dev/null +++ b/app/update-checker.js @@ -0,0 +1,58 @@ +/*global fetch:false*/ +import { version as currentVersion } from '../package'; +import compare from 'semver-compare'; + +export default class UpdateChecker { + + constructor (fn, { interval = 5000 } = {}) { + this.callback = fn; + this.interval = interval; + this.check(); + this.lastKnown = null; + } + + check () { + const done = () => { + this.checkTimer = setTimeout(() => { + this.check(); + }, this.interval); + }; + + console.log('checking for update'); + fetch('https://hyperterm.now.sh/data.json') + .then((res) => { + if (200 !== res.status) { + console.error('Update check error. Status (%d)', res.status); + return done(); + } + + res.json() + .then(({ version }) => { + if (this.lastKnown !== version) { + this.lastKnown = version; + + if (1 === compare(version, currentVersion)) { + console.log('update found'); + this.callback(version); + } else { + console.log('no update. latest:', version); + } + } + done(); + }) + .catch((err) => { + console.error('Update JSON parse error', err.stack); + done(); + }); + }).catch((err) => { + console.error('Update check error', err.stack); + done(); + }); + } + + destroy () { + this.aborted = true; + clearTimeout(this.checkTimer); + } + +} diff --git a/app/webpack.config.js b/app/webpack.config.js index 8b427534..fa00d3c4 100644 --- a/app/webpack.config.js +++ b/app/webpack.config.js @@ -20,6 +20,10 @@ module.exports = { 'babel-loader' ] }, + { + test: /\.json/, + loader: 'json-loader' + }, { test: /\.css$/, loader: 'style-loader!css-loader' diff --git a/index.js b/index.js index f29d80f9..59f4e77e 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, Menu } = require('electron'); +const { app, BrowserWindow, shell, Menu } = require('electron'); const createRPC = require('./rpc'); const Session = require('./session'); const genUid = require('uid2'); @@ -81,6 +81,10 @@ app.on('ready', () => { sessions.get(uid).write(data); }); + rpc.on('open external', ({ url }) => { + shell.openExternal(url); + }); + const deleteSessions = () => { sessions.forEach((session, key) => { session.removeAllListeners();