diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 7c10da9..23a1b81 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "DoubanFMac", - "version": "1.0.0-0", + "version": "1.0.0", "dependencies": { "abab": { "version": "1.0.3", @@ -246,18 +246,6 @@ "from": "aws-sign2@>=0.5.0 <0.6.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.5.0.tgz" }, - "aws4": { - "version": "1.3.2", - "from": "aws4@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", - "dependencies": { - "lru-cache": { - "version": "4.0.1", - "from": "lru-cache@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz" - } - } - }, "babel": { "version": "5.8.38", "from": "babel@>=5.4.7 <6.0.0", @@ -1632,7 +1620,8 @@ "dependencies": { "esprima": { "version": "2.7.2", - "from": "esprima@>=2.7.1 <3.0.0" + "from": "esprima@2.7.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" }, "estraverse": { "version": "1.9.3", @@ -1765,7 +1754,8 @@ }, "minimist": { "version": "0.0.8", - "from": "minimist@0.0.8" + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" }, "mkdirp": { "version": "0.5.0", @@ -3089,11 +3079,6 @@ "from": "is-svg@>=1.1.1 <2.0.0", "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-1.1.1.tgz" }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - }, "is-utf8": { "version": "0.2.1", "from": "is-utf8@>=0.2.0 <0.3.0", @@ -3153,7 +3138,8 @@ "dependencies": { "esprima": { "version": "2.7.2", - "from": "esprima@>=2.7.0 <3.0.0" + "from": "esprima@2.7.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" } } }, @@ -3275,11 +3261,6 @@ "from": "jsonpointer@2.0.0", "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" }, - "jsprim": { - "version": "1.2.2", - "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz" - }, "jstransform": { "version": "10.1.0", "from": "jstransform@>=10.0.1 <11.0.0", @@ -3727,7 +3708,14 @@ "mkdirp": { "version": "0.5.1", "from": "mkdirp@>=0.5.0 <0.6.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz" + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "dependencies": { + "minimist": { + "version": "0.0.8", + "from": "minimist@0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + } + } }, "mkpath": { "version": "0.1.0", @@ -5167,7 +5155,14 @@ "npmconf": { "version": "2.1.2", "from": "npmconf@>=2.1.1 <3.0.0", - "resolved": "https://registry.npmjs.org/npmconf/-/npmconf-2.1.2.tgz" + "resolved": "https://registry.npmjs.org/npmconf/-/npmconf-2.1.2.tgz", + "dependencies": { + "semver": { + "version": "4.3.6", + "from": "semver@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0||>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" + } + } }, "npmlog": { "version": "2.0.3", @@ -5988,7 +5983,8 @@ "dependencies": { "esprima": { "version": "2.7.2", - "from": "esprima@>=2.6.0 <3.0.0" + "from": "esprima@2.7.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" } } }, @@ -6112,9 +6108,9 @@ "resolved": "https://registry.npmjs.org/seamless-immutable/-/seamless-immutable-5.1.1.tgz" }, "semver": { - "version": "4.3.6", - "from": "semver@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0||>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-4.3.6.tgz" + "version": "5.1.0", + "from": "semver@*", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.1.0.tgz" }, "send": { "version": "0.13.1", @@ -6260,23 +6256,6 @@ "from": "sprintf-js@>=1.0.2 <1.1.0", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" }, - "sshpk": { - "version": "1.7.4", - "from": "sshpk@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz", - "dependencies": { - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" - }, - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - } - } - }, "stable": { "version": "0.1.5", "from": "stable@>=0.1.3 <0.2.0", @@ -6771,11 +6750,6 @@ "from": "wordwrap@>=0.0.2 <0.1.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" }, - "wrap-ansi": { - "version": "2.0.0", - "from": "wrap-ansi@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.0.0.tgz" - }, "wrappy": { "version": "1.0.1", "from": "wrappy@>=1.0.0 <2.0.0", diff --git a/package.json b/package.json index 3b3d7a8..6d30a2e 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "redux-thunk": "^1.0.2", "sass-flex-mixin": "^1.0.0", "seamless-immutable": "^5.1.0", + "semver": "^5.1.0", "url-loader": "^0.5.7" }, "devEngines": { diff --git a/src/containers/HomePage/HomePage.js b/src/containers/HomePage/HomePage.js index 5535055..7b9a366 100644 --- a/src/containers/HomePage/HomePage.js +++ b/src/containers/HomePage/HomePage.js @@ -4,15 +4,20 @@ import { show } from 'redux-modal'; import { connect } from 'react-redux'; import { logout, verify } from 'reducers/auth'; import { fetch as fetchCaptcha } from 'reducers/captcha'; +import { check } from 'reducers/updater'; +import { shell } from 'electron'; import Navbar from './Navbar/Navbar'; +import styles from './HomePage.scss'; +import updaterIcon from './updater.gif'; @connect( state => ({ currentUser: state.auth.user, song: state.channel.song, + outdated: state.updater.outdated, }), dispatch => ({ - ...bindActionCreators({ show, fetchCaptcha, logout, verify }, dispatch) + ...bindActionCreators({ show, fetchCaptcha, logout, verify, check }, dispatch) }) ) export default class HomePage extends Component { @@ -21,8 +26,10 @@ export default class HomePage extends Component { location: PropTypes.object.isRequired, currentUser: PropTypes.object, song: PropTypes.object, + outdated: PropTypes.bool.isRequired, show: PropTypes.func.isRequired, logout: PropTypes.func.isRequired, + check: PropTypes.func.isRequired, verify: PropTypes.func.isRequired, fetchCaptcha: PropTypes.func.isRequired, } @@ -31,8 +38,19 @@ export default class HomePage extends Component { router: React.PropTypes.object } + constructor(props) { + super(props); + + const updaterInterval = setInterval(() => { + props.check(); + }, 1000 * 60 * 60 * 24 * 7); + + this.state = { updaterInterval }; + } + componentDidMount() { this.props.verify(); + this.props.check(); } componentWillReceiveProps(nextProps) { @@ -41,6 +59,14 @@ export default class HomePage extends Component { } } + componentWillUnmount() { + const { updaterInterval } = this.state; + + if (updaterInterval) { + clearInterval(updaterInterval); + } + } + notice = (song) => { if (document.hasFocus()) return; @@ -60,8 +86,24 @@ export default class HomePage extends Component { this.props.logout(); } + downloadLatestVersion = () => { + shell.openExternal( + 'http://doubanfmac.oss-cn-hangzhou.aliyuncs.com/DoubanFMac.dmg' + ); + } + + renderUpdateBar = () => { + return ( +
+ +
+ ); + } + render() { - const { children, currentUser, location } = this.props; + const { children, currentUser, location, outdated } = this.props; return (
@@ -72,6 +114,12 @@ export default class HomePage extends Component { logoutUser={this.logoutUser} /> {children} +
+ { outdated && this.renderUpdateBar() } +
+ 问题反馈 +
+
); } diff --git a/src/containers/HomePage/HomePage.scss b/src/containers/HomePage/HomePage.scss new file mode 100644 index 0000000..2fe69eb --- /dev/null +++ b/src/containers/HomePage/HomePage.scss @@ -0,0 +1,38 @@ +@import '~sass-flex-mixin/flex'; + +.footer { + @extend %flexbox; + @include justify-content(flex-end); + + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 0.5rem; +} + +.updaterBar { + @extend %flexbox; + @include flex-grow(1); + @include align-items(center); + + :global { + .icon { + width: 1.5rem; + margin-left: 1rem; + } + } +} + +.contactBar { + width: 4rem; +} + +.link { + font-size: 0.5rem; + color: #fff; + background: #ceecd2; + padding: 0.2rem; + margin-right: 1rem; + text-decoration: none; +} diff --git a/src/containers/HomePage/Navbar/Navbar.scss b/src/containers/HomePage/Navbar/Navbar.scss index 4519d89..4095029 100644 --- a/src/containers/HomePage/Navbar/Navbar.scss +++ b/src/containers/HomePage/Navbar/Navbar.scss @@ -3,7 +3,7 @@ @mixin navItem($fontSize: 0.8rem) { color: grey; font-size: $fontSize; - font-weight: thin; + font-weight: 300; text-decoration: none; } diff --git a/src/containers/HomePage/updater.gif b/src/containers/HomePage/updater.gif new file mode 100755 index 0000000..cd4d9c1 Binary files /dev/null and b/src/containers/HomePage/updater.gif differ diff --git a/src/containers/HomePage/updater.gif.old.gif b/src/containers/HomePage/updater.gif.old.gif new file mode 100755 index 0000000..66cb344 Binary files /dev/null and b/src/containers/HomePage/updater.gif.old.gif differ diff --git a/src/reducers/__tests__/updater.spec.js b/src/reducers/__tests__/updater.spec.js new file mode 100644 index 0000000..2f8d87a --- /dev/null +++ b/src/reducers/__tests__/updater.spec.js @@ -0,0 +1,68 @@ +import configureMockStore from 'redux-mock-store'; +import { expect } from 'chai'; +import nock from 'nock'; +import Immutable from 'seamless-immutable'; +import { apiMiddleware } from 'redux-api-middleware'; +import thunk from 'redux-thunk'; +import apiMiddlewareHook from '../../middlewares/apiMiddlewareHook'; +import camelizeState from '../../middlewares/camelizeState'; +import _last from 'lodash/last'; +import updater, { FETCH_SUCCESS, check } from '../updater'; + +const middlewares = [ + thunk, apiMiddlewareHook, apiMiddleware, camelizeState +]; +const mockStore = configureMockStore(middlewares); + +describe('Updater Actions', function actions() { + afterEach(() => { + nock.cleanAll(); + }); + + it('FETCH_SUCCESS', function fetchSuccess(done) { + nock('https://api.github.com') + .get('/repos/Darmody/DoubanFMac/releases/latest') + .reply(200, { tagName: 'v1.0.0' }); + + const store = mockStore({ + updater: { + currentVersion: '1.0.0' + } + }); + store.dispatch(check()); + setTimeout(() => { + expect(_last(store.getActions()).type).to.equal(FETCH_SUCCESS); + done(); + }, 20); + }); +}); + +describe('Updater Reducers', function reducers() { + it('FETCH_SUCCESS', function fetchSuccess() { + expect( + updater(Immutable({ + currentVersion: '1.0.0', + outdated: false, + }), { + type: FETCH_SUCCESS, + payload: { tagName: 'v1.0.1' } + }) + ).to.deep.equal({ + currentVersion: '1.0.0', + outdated: true, + }); + + expect( + updater(Immutable({ + currentVersion: '1.0.0', + outdated: false, + }), { + type: FETCH_SUCCESS, + payload: { tagName: 'v1.0.0' } + }) + ).to.deep.equal({ + currentVersion: '1.0.0', + outdated: false, + }); + }); +}); diff --git a/src/reducers/index.js b/src/reducers/index.js index 4470fe7..71ee110 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -6,6 +6,7 @@ import auth from './auth'; import captcha from './captcha'; import channel from './channel'; import favorite from './favorite'; +import updater from './updater'; const rootReducer = combineReducers({ routing, @@ -15,6 +16,7 @@ const rootReducer = combineReducers({ captcha, channel, favorite, + updater, }); export default rootReducer; diff --git a/src/reducers/updater.js b/src/reducers/updater.js new file mode 100644 index 0000000..3894ddc --- /dev/null +++ b/src/reducers/updater.js @@ -0,0 +1,35 @@ +import Immutable from 'seamless-immutable'; +import { CALL_API } from 'redux-api-middleware'; +import { handleActions } from 'redux-actions'; +import semver from 'semver'; +import pkg from '../../package.json'; + +export const FETCH_REQUEST = 'UPDATER/FETCH_REQUEST'; +export const FETCH_SUCCESS = 'UPDATER/FETCH_SUCCESS'; +export const FETCH_FAILURE = 'UPDATER/FETCH_FAILURE'; + +const initialState = Immutable({ + outdated: false, + currentVersion: pkg.version +}); + +export default handleActions({ + [FETCH_SUCCESS]: (state, action) => { + const latestVersion = semver.clean(action.payload.tagName); + return state.merge({ outdated: semver.gt(latestVersion, state.currentVersion) }); + } +}, initialState); + +export function check() { + return { + [CALL_API]: { + endpoint: 'https://api.github.com/repos/Darmody/DoubanFMac/releases/latest', + method: 'GET', + types: [ + FETCH_REQUEST, + FETCH_SUCCESS, + FETCH_FAILURE + ] + } + }; +}