diff --git a/README.md b/README.md index e648c9f..2c4cc57 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,9 @@ $ npm i && make browser $ cd ../../.. $ webpack -w -$ DEBUG=1 electron app/ +$ electron app/ +$ DEBUG=1 electron app/ # with Developer toolbar +$ DEBUG=1 REDUX=1 electron app/ # with Redux Developer toolbar ``` diff --git a/app/package.json b/app/package.json index dda4f2e..3191292 100644 --- a/app/package.json +++ b/app/package.json @@ -21,5 +21,10 @@ "react-router": "^2.0.1", "redux": "^3.3.1", "sqlite3": "^3.1.3" + }, + "devDependencies": { + "redux-devtools": "^3.2.0", + "redux-devtools-dock-monitor": "^1.1.1", + "redux-devtools-log-monitor": "^1.0.9" } } diff --git a/package.json b/package.json index 95aa201..9cc4597 100644 --- a/package.json +++ b/package.json @@ -50,16 +50,13 @@ "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", "css-loader": "^0.23.1", + "electron-builder": "^3.5.0", + "electron-prebuilt": "^0.37.5", "node-sass": "^3.4.2", - "redux-devtools": "^3.1.1", - "redux-devtools-dock-monitor": "^1.1.0", - "redux-devtools-log-monitor": "^1.0.5", "sass-loader": "^3.2.0", "style-loader": "^0.13.1", "url-loader": "^0.5.7", - "webpack": "^1.12.14", - "electron-prebuilt": "^0.37.5", - "electron-builder": "^3.3.1" + "webpack": "^1.12.14" }, "scripts": { "postinstall": "./node_modules/.bin/install-app-deps", diff --git a/src/app.js b/src/app.js index 530ed1b..2ca10f8 100644 --- a/src/app.js +++ b/src/app.js @@ -12,12 +12,17 @@ import {Provider} from 'react-redux' import MainLayout from './components/layout/Main' import MainPage from './components/page/Main' -// import DevTools from './assets/js/DevTools' +import DevTools from './assets/js/DevTools' import Player from './context/Player'; -// window._store = createStore(reducer, DevTools.instrument()); -window._store = createStore(reducer); +console.log(process.env.REDUX); + +if (process.env.REDUX) { + window._store = createStore(reducer, DevTools.instrument()); +} else { + window._store = createStore(reducer); +} Player.getInstance(); diff --git a/src/assets/js/DevTools.js b/src/assets/js/DevTools.js index 4c20d69..7571939 100644 --- a/src/assets/js/DevTools.js +++ b/src/assets/js/DevTools.js @@ -1,4 +1,5 @@ import React from 'react'; +import Immutable from 'immutable' // Exported from redux-devtools import { createDevTools } from 'redux-devtools'; @@ -7,6 +8,8 @@ import { createDevTools } from 'redux-devtools'; import LogMonitor from 'redux-devtools-log-monitor'; import DockMonitor from 'redux-devtools-dock-monitor'; +let selectDevToolsState = (state = {}) => Immutable.fromJS(state).toJS(); + // createDevTools takes a monitor and produces a DevTools component const DevTools = createDevTools( // Monitors are individually adjustable with props. @@ -14,7 +17,7 @@ const DevTools = createDevTools( // Here, we put LogMonitor inside a DockMonitor. - + ); diff --git a/src/components/layout/Main/index.js b/src/components/layout/Main/index.js index 891a02b..61d8f04 100644 --- a/src/components/layout/Main/index.js +++ b/src/components/layout/Main/index.js @@ -1,7 +1,7 @@ import './styles/style.scss' import React from 'react' import Controls from '../../widget/Controls' -// import DevTools from '../../../assets/js/DevTools' +import DevTools from '../../../assets/js/DevTools' class Layout extends React.Component { constructor() { @@ -22,12 +22,11 @@ class Layout extends React.Component {
+ {process.env.REDUX ? : false}
) } } -// - export default Layout; diff --git a/src/components/widget/AlbumList/index.js b/src/components/widget/AlbumList/index.js index be287c5..6de7876 100644 --- a/src/components/widget/AlbumList/index.js +++ b/src/components/widget/AlbumList/index.js @@ -1,6 +1,7 @@ //import './styles/style.scss' import React from 'react' import {connect} from 'react-redux' +import I from 'immutable' import database from '../../../context/db' import utils from '../../../assets/js/Utils' @@ -8,13 +9,6 @@ import utils from '../../../assets/js/Utils' class AlbumList extends React.Component { constructor() { super(); - - this.state = { - albums: [] - }; - - // cache - this.albumsCovers = {}; } /** @@ -22,7 +16,7 @@ class AlbumList extends React.Component { * * @returns {Promise} */ - getAlbumList(props) { + getAlbumList(selectedArtist) { const sql = `SELECT playlist.album, COUNT(playlist.title) AS tracks, @@ -37,12 +31,9 @@ class AlbumList extends React.Component { GROUP BY playlist.album`; return new Promise((resolve, reject) => { - const selectedArtist = props.store.selected.artist; - database.open((db) => { db.all(sql, {$artist: selectedArtist}, function(error, results) { if (results) { - console.debug(results); resolve(results); } else { console.error(error); @@ -63,17 +54,17 @@ class AlbumList extends React.Component { } render() { - let albumList = this.state.albums.map((value, index) => { - const classList = 'list-group-item ' + (this.props.store.selected.album === value.album ? 'active' : ''); - const coverArt = this.getCoverAsURL(value.id, value.coverArt); + let albumList = this.props.library.get('albums').map((value, index) => { + const classList = 'list-group-item ' + (this.props.selected.get('album') === value.get('album') ? 'active' : ''); + const coverArt = utils.getURLfromBlob(value.get('coverArt')); return ( -
  • +
  • - {value.album} -

    {value.tracks} songs

    + {value.get('album')} +

    {value.get('tracks')} songs

  • ); @@ -86,29 +77,29 @@ class AlbumList extends React.Component { ) } - getCoverAsURL(id, coverData) { - return this.albumsCovers[id] || (this.albumsCovers[id] = utils.getURLfromBlob(coverData)); - } - componentWillReceiveProps(nextProps) { - if (this.props.store.selected.artist !== nextProps.store.selected.artist) { - this.getAlbumList(nextProps).then((data) => { - this.setState({ - albums: data + const selectedArtist = nextProps.selected.get('artist'); + + if (this.props.selected.get('artist') !== selectedArtist) { + this.getAlbumList(selectedArtist).then((data) => { + this.props.dispatch({ + type: 'SET_LIBRARY_ALBUMS', + value: data }); }); } } shouldComponentUpdate(nextProps) { - return this.props.store.selected.artist || - (this.props.store.selected.artist !== nextProps.store.selected.artist); + return !I.is(this.props.library.get('albums'), nextProps.library.get('albums')) || + !I.is(this.props.selected.get('album'), nextProps.selected.get('album')); } } const mapStatesToProps = (store) => { return { - store: store + selected: store.get('selected'), + library: store.get('library') } }; diff --git a/src/components/widget/ArtistList/AlbumCover.js b/src/components/widget/ArtistList/AlbumCover.js index c0fdcf8..00e29d3 100644 --- a/src/components/widget/ArtistList/AlbumCover.js +++ b/src/components/widget/ArtistList/AlbumCover.js @@ -4,47 +4,33 @@ import utils from '../../../assets/js/Utils' class AlbumCover extends React.Component { constructor() { super(); - - this.albumsCovers = {}; - this.state = { - covers: null - }; } render() { - const covers = this.state.covers && this.state.covers.length ? this.state.covers : [null]; + let covers = []; + + for (let i = 1; i < 5; ++i) { + let cover = this.props.data.get(`coverArt${i}`); + + if (cover) { + covers.push(cover); + } + } + + covers = covers && covers.length ? covers : [null]; return (
    {covers.map((value, index) => { - const id = value ? `${this.props.id}${index}` : 0; - const cover = this.getCoverAsURL(id, value); + const cover = utils.getURLfromBlob(value); return })}
    ) } - getCoverAsURL(id, coverData) { - return this.albumsCovers[id] || (this.albumsCovers[id] = utils.getURLfromBlob(coverData)); - } - - componentWillReceiveProps() { - } - - componentDidMount() { - let covers = []; - for (let i = 1; i < 5; ++i) { - let cover = this.props[`coverArt${i}`]; - - if (cover) { - covers.push(cover); - } - } - - this.setState({ - covers: covers - }); + shouldComponentUpdate(nextProps) { + return !nextProps.data.equals(this.props.data); } } diff --git a/src/components/widget/ArtistList/index.js b/src/components/widget/ArtistList/index.js index 1e5f5de..641717a 100644 --- a/src/components/widget/ArtistList/index.js +++ b/src/components/widget/ArtistList/index.js @@ -2,6 +2,7 @@ import './styles/style.scss' import React from 'react' import {connect} from 'react-redux' +import I from 'immutable' import AlbumCover from './AlbumCover' @@ -10,10 +11,6 @@ import database from '../../../context/db' class ArtistList extends React.Component { constructor() { super(); - - this.state = { - artists: [] - }; } /** @@ -37,7 +34,6 @@ class ArtistList extends React.Component { database.open((db) => { db.all(sql, (error, results) => { if (results) { - console.debug(results); resolve(results); } else { console.error(error); @@ -58,16 +54,16 @@ class ArtistList extends React.Component { } render() { - let artistsList = this.state.artists.map((value, index) => { - let classList = 'list-group-item ' + (this.props.store.selected.artist === value.artist ? 'active' : ''); + let artistsList = this.props.library.get('artists').map((value, index) => { + let classList = 'list-group-item ' + (this.props.selected.get('artist') === value.get('artist') ? 'active' : ''); return ( -
  • - +
  • +
    - {value.artist} -

    {value.albums} albums

    + {value.get('artist')} +

    {value.get('albums')} albums

  • ); @@ -80,11 +76,14 @@ class ArtistList extends React.Component { ) } - getListOfArtists() { + getListOfArtists(callback) { this.getPlayList().then((data) => { - this.setState({ - artists: data + this.props.dispatch({ + type: 'SET_LIBRARY_ARTISTS', + value: data }); + + callback && callback(); }); } @@ -93,20 +92,26 @@ class ArtistList extends React.Component { } componentWillReceiveProps(nextProps) { - if (nextProps.store.libraryUpdated) { - this.getListOfArtists(); + if (nextProps.libraryUpdated) { + this.getListOfArtists(() => { + this.props.dispatch({ + type: 'LIBRARY_DID_UPDATE' + }); + }); } } shouldComponentUpdate(nextProps) { - return (!this.props.store.selected.artist || - (this.props.store.selected.artist !== nextProps.store.selected.artist)); + return !I.is(this.props.library.get('artists'), nextProps.library.get('artists')) || + !I.is(this.props.selected.get('artist'), nextProps.selected.get('artist')); } } const mapStatesToProps = (store) => { return { - store: store + selected: store.get('selected'), + library: store.get('library'), + libraryUpdated: store.get('libraryUpdated') } }; diff --git a/src/components/widget/Controls/index.js b/src/components/widget/Controls/index.js index 36209ea..4622375 100644 --- a/src/components/widget/Controls/index.js +++ b/src/components/widget/Controls/index.js @@ -81,7 +81,7 @@ class Controls extends React.Component { const mapStatesToProps = (store) => { return { - isPlaying: store.isPlaying + isPlaying: store.get('isPlaying') } }; diff --git a/src/components/widget/LibraryProcess/index.js b/src/components/widget/LibraryProcess/index.js index c7b5808..a0a761f 100644 --- a/src/components/widget/LibraryProcess/index.js +++ b/src/components/widget/LibraryProcess/index.js @@ -71,7 +71,6 @@ class LibraryProcess extends React.Component { fse.walk(files) .on('data', (item) => { - console.log(item); if (!item.stats.isDirectory() && item.path.match(this.extensions)) { items.push(item.path); } @@ -103,8 +102,6 @@ class LibraryProcess extends React.Component { }; let onMetadata = (metadata) => { - console.log(metadata); - const covertArt = metadata.coverArt ? metadata.coverArt.data : undefined; trackInfoCallback({ @@ -116,15 +113,13 @@ class LibraryProcess extends React.Component { diskNumber: metadata.diskNumber, trackNumber: metadata.trackNumber, coverArt: covertArt - }); + }, callback); this.releaseResource(asset, { onError, onMetadata, onData }); - - callback(); }; /** @@ -139,8 +134,6 @@ class LibraryProcess extends React.Component { }, 300); }; - console.log(file, asset); - asset.on('error', onError); asset.get('metadata', onMetadata); asset.once('data', onData); @@ -164,7 +157,7 @@ class LibraryProcess extends React.Component { asset = null; } - writeToDB(fileInfo) { + writeToDB(fileInfo, callback) { this.db.run('INSERT INTO `playlist` VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [null, fileInfo.artist, fileInfo.albumArtist, fileInfo.album, fileInfo.title, fileInfo.file, fileInfo.diskNumber, fileInfo.trackNumber], (error) => { @@ -178,8 +171,9 @@ class LibraryProcess extends React.Component { if (error) { console.error(error); } - }); + callback(); + }); }); } diff --git a/src/components/widget/SongList/SongListComponent.js b/src/components/widget/SongList/SongListComponent.js index bbf3381..5d65b42 100644 --- a/src/components/widget/SongList/SongListComponent.js +++ b/src/components/widget/SongList/SongListComponent.js @@ -32,16 +32,15 @@ class SongListComponent extends React.Component { setTimeout(() => { this.player.play(file); + }, 300); - this.props.dispatch({ - type: 'PLAY', - value: { - title: title, - file: file - } - }); - }, 500); - + this.props.dispatch({ + type: 'PLAY', + value: { + title: title, + file: file + } + }); } render() { @@ -55,7 +54,7 @@ class SongListComponent extends React.Component { diff --git a/src/components/widget/SongList/TrackItem.js b/src/components/widget/SongList/TrackItem.js index 449b827..a94af8f 100644 --- a/src/components/widget/SongList/TrackItem.js +++ b/src/components/widget/SongList/TrackItem.js @@ -4,11 +4,11 @@ const TrackItem = (props) => { let classList = () => { var output = []; - if (props.selectedFile === props.trackInfo.file) { + if (props.selectedFile === props.trackInfo.get('file')) { output.push('selected'); } - if (props.playingFile === props.trackInfo.file) { + if (props.playingFile === props.trackInfo.get('file')) { output.push('active'); } @@ -17,8 +17,8 @@ const TrackItem = (props) => { return ( - {props.trackInfo.trackNumber} - {props.trackInfo.title} + {props.trackInfo.get('trackNumber')} + {props.trackInfo.get('title')} ); }; diff --git a/src/components/widget/SongList/TrackList.js b/src/components/widget/SongList/TrackList.js index 6d11f64..e2cb85e 100644 --- a/src/components/widget/SongList/TrackList.js +++ b/src/components/widget/SongList/TrackList.js @@ -6,14 +6,14 @@ import TrackItem from './TrackItem' const TrackList = (props) => { return ( - {props.trackList.map((value, index) => { + {props.data.trackList.map((value, index) => { return (); })} diff --git a/src/components/widget/SongList/index.js b/src/components/widget/SongList/index.js index f3970ee..88c563b 100644 --- a/src/components/widget/SongList/index.js +++ b/src/components/widget/SongList/index.js @@ -1,5 +1,6 @@ import React from 'react' import {connect} from 'react-redux' +import I from 'immutable' import database from '../../../context/db' @@ -9,19 +10,14 @@ import SongListComponent from './songListComponent' class SongList extends React.Component { constructor() { super(); - - this.state = { - tracks: [] - }; } getPlayList(props) { return new Promise((resolve, reject) => { database.open((db) => { db.all('SELECT * FROM playlist WHERE albumArtist = ? and album = ?', - [props.selected.artist, props.selected.album], (error, results) => { + [props.selected.get('artist'), props.selected.get('album')], (error, results) => { if (results) { - console.debug(results); resolve(results); } else { console.error(error); @@ -36,29 +32,38 @@ class SongList extends React.Component { render() { return () } componentWillReceiveProps(nextProps) { - if (this.props.selected.artist !== nextProps.selected.artist || - this.props.selected.album !== nextProps.selected.album) { + if ((this.props.selected.get('album') && this.props.selected.get('artist')) && + this.props.selected.get('artist') !== nextProps.selected.get('artist') || + this.props.selected.get('album') !== nextProps.selected.get('album')) { this.getPlayList(nextProps).then((data) => { - this.setState({ - tracks: data + this.props.dispatch({ + type: 'SET_LIBRARY_TRACKS', + value: data }); }); } } + shouldComponentUpdate(nextState) { + return !I.is(nextState.library.get('tracks'), this.props.library.get('tracks')) || + nextState.selected.get('file') !== this.props.selected.get('file') || + nextState.playing.get('file') !== this.props.playing.get('file'); + } + } const mapStatesToProps = (store) => { return { - selected: store.selected, - playing: store.playing + selected: store.get('selected'), + playing: store.get('playing'), + library: store.get('library') } }; diff --git a/src/context/Player.js b/src/context/Player.js index 6f6b186..3e0cc09 100644 --- a/src/context/Player.js +++ b/src/context/Player.js @@ -36,7 +36,7 @@ class Player { if (!this.state.isPlaying) { if (!this.player || this.player.isStop) { - let track = file || store.playing.track || store.selected.track; + let track = file || store.getIn(['playing', 'file']) || store.getIn(['selected', 'file']); if (!track) { throw new Error('Select a file first!'); @@ -68,7 +68,7 @@ class Player { toggle() { let store = this.store.getState(); - if (store.isPlaying) { + if (store.get('isPlaying')) { this.pause(); } else { this.play(); diff --git a/src/reducers/index.js b/src/reducers/index.js index 46bb45d..e8f5b80 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,4 +1,6 @@ -const initObject = { +import Immutable from 'immutable'; + +const initObject = Immutable.fromJS({ isPlaying: false, libraryUpdated: false, playing: { @@ -12,68 +14,60 @@ const initObject = { album: null, title: null, file: null + }, + library: { + artists: [], + albums: [], + tracks: [] } -}; +}); function mainReducer(state = initObject, action) { switch (action.type) { case 'PLAY': - return Object.assign({}, state, { - isPlaying: true, - playing: Object.assign({}, state.playing, { - file: action.value.file, - title: action.value.title - }), - selected: Object.assign({}, state.selected, { - file: action.value.file, - title: action.value.title - }) - }); + return state.set('isPlaying', true). + setIn(['playing', 'file'], action.value.file). + setIn(['playing', 'title'], action.value.title). + setIn(['selected', 'file'], action.value.file). + setIn(['selected', 'title'], action.value.title); case 'STOP': - return Object.assign({}, state, { - isPlaying: false - }); + return state.set('isPlaying', false); case 'TOGGLE_PLAY': - return Object.assign({}, state, { - isPlaying: !state.isPlaying - }); + return state.set('isPlaying', !state.get('isPlaying')); case 'SET_SELECTED_ARTIST': - return Object.assign({}, state, { - selected: Object.assign({}, state.selected, { - artist: action.value - }) - }); + return state.setIn(['selected', 'artist'], action.value); case 'SET_SELECTED_ALBUM': - return Object.assign({}, state, { - selected: Object.assign({}, state.selected, { - album: action.value - }) - }); + return state.setIn(['selected', 'album'], action.value); case 'SET_SELECTED_TRACK': - return Object.assign({}, state, { - selected: Object.assign({}, state.selected, { - title: action.value.title, - file: action.value.file - }) - }); + return state.setIn(['selected', 'title'], action.value.title). + setIn(['selected', 'file'], action.value.file); case 'SET_PLAYING_TRACK': - return Object.assign({}, state, { - playing: Object.assign({}, state.playing, { - title: action.value.title, - file: action.value.file - }) - }); + return state.setIn(['playing', 'title'], action.value.title). + setIn(['playing', 'file'], action.value.file); case 'LIBRARY_UPDATED': - return Object.assign({}, state, { - libraryUpdated: true - }); + return state.set('libraryUpdated', true). + set('selected', Immutable.Map()); + + case 'LIBRARY_DID_UPDATE': + return state.set('libraryUpdated', false); + + case 'SET_LIBRARY_ARTISTS': + return state.setIn(['library', 'artists'], Immutable.fromJS(action.value)). + setIn(['library', 'albums'], Immutable.List()); + + case 'SET_LIBRARY_ALBUMS': + return state.setIn(['library', 'albums'], Immutable.fromJS(action.value)). + setIn(['library', 'tracks'], Immutable.List()); + + case 'SET_LIBRARY_TRACKS': + return state.setIn(['library', 'tracks'], Immutable.fromJS(action.value)); default: return state;