Browse Source

Basic webplayer

Now able to play a single file, in a format supported by your browser.

* Playlists not yet supported.
* Volume is a simple on/off switch.
* Repeat / Random not yet supported.
Phyks (Lucas Verney) 5 years ago
parent
commit
4d4ce6c14e

+ 1
- 1
app/actions/APIActions.js View File

@@ -5,7 +5,7 @@ import { CALL_API } from "../middleware/api";
5 5
 
6 6
 import { artist, track, album } from "../models/api";
7 7
 
8
-export const DEFAULT_LIMIT = 30;  /** Default max number of elements to retrieve. */
8
+export const DEFAULT_LIMIT = 32;  /** Default max number of elements to retrieve. */
9 9
 
10 10
 export default function (action, requestType, successType, failureType) {
11 11
     const itemName = action.rstrip("s");

+ 1
- 0
app/actions/index.js View File

@@ -11,3 +11,4 @@ export var { loadSongs } = APIAction("songs", API_REQUEST, API_SUCCESS, API_FAIL
11 11
 
12 12
 export * from "./paginate";
13 13
 export * from "./store";
14
+export * from "./webplayer";

+ 92
- 0
app/actions/webplayer.js View File

@@ -0,0 +1,92 @@
1
+export const PLAY_PAUSE = "PLAY_PAUSE";
2
+/**
3
+ * true to play, false to pause.
4
+ */
5
+export function togglePlaying(playPause) {
6
+    return (dispatch, getState) => {
7
+        let isPlaying = false;
8
+        if (typeof playPause !== "undefined") {
9
+            isPlaying = playPause;
10
+        } else {
11
+            isPlaying = !(getState().webplayer.isPlaying);
12
+        }
13
+        dispatch({
14
+            type: PLAY_PAUSE,
15
+            payload: {
16
+                isPlaying: isPlaying
17
+            }
18
+        });
19
+    };
20
+}
21
+
22
+export const PUSH_PLAYLIST = "PUSH_PLAYLIST";
23
+export function playTrack(trackID) {
24
+    return (dispatch, getState) => {
25
+        const track = getState().api.entities.getIn(["track", trackID]);
26
+        const album = getState().api.entities.getIn(["album", track.get("album")]);
27
+        const artist = getState().api.entities.getIn(["artist", track.get("artist")]);
28
+        dispatch({
29
+            type: PUSH_PLAYLIST,
30
+            payload: {
31
+                playlist: [trackID],
32
+                tracks: [
33
+                    [trackID, track]
34
+                ],
35
+                albums: [
36
+                    [album.get("id"), album]
37
+                ],
38
+                artists: [
39
+                    [artist.get("id"), artist]
40
+                ]
41
+            }
42
+        });
43
+        dispatch(togglePlaying(true));
44
+    };
45
+}
46
+
47
+export const CHANGE_TRACK = "CHANGE_TRACK";
48
+export function playPrevious() {
49
+    // TODO: Playlist overflow
50
+    return (dispatch, getState) => {
51
+        let { index } = getState().webplayer;
52
+        dispatch({
53
+            type: CHANGE_TRACK,
54
+            payload: {
55
+                index: index - 1
56
+            }
57
+        });
58
+    };
59
+}
60
+export function playNext() {
61
+    // TODO: Playlist overflow
62
+    return (dispatch, getState) => {
63
+        let { index } = getState().webplayer;
64
+        dispatch({
65
+            type: CHANGE_TRACK,
66
+            payload: {
67
+                index: index + 1
68
+            }
69
+        });
70
+    };
71
+}
72
+
73
+export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
74
+export function toggleRandom() {
75
+    return {
76
+        type: TOGGLE_RANDOM
77
+    };
78
+}
79
+
80
+export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
81
+export function toggleRepeat() {
82
+    return {
83
+        type: TOGGLE_REPEAT
84
+    };
85
+}
86
+
87
+export const TOGGLE_MUTE = "TOGGLE_MUTE";
88
+export function toggleMute() {
89
+    return {
90
+        type: TOGGLE_MUTE
91
+    };
92
+}

+ 14
- 8
app/components/Album.jsx View File

@@ -1,6 +1,6 @@
1 1
 import React, { Component, PropTypes } from "react";
2 2
 import CSSModules from "react-css-modules";
3
-import { defineMessages, FormattedMessage } from "react-intl";
3
+import { defineMessages, FormattedMessage, injectIntl, intlShape } from "react-intl";
4 4
 import FontAwesome from "react-fontawesome";
5 5
 import Immutable from "immutable";
6 6
 
@@ -12,13 +12,14 @@ import css from "../styles/Album.scss";
12 12
 
13 13
 const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages)));
14 14
 
15
-class AlbumTrackRowCSS extends Component {
15
+class AlbumTrackRowCSSIntl extends Component {
16 16
     render () {
17
+        const { formatMessage } = this.props.intl;
17 18
         const length = formatLength(this.props.track.get("time"));
18 19
         return (
19 20
             <tr>
20 21
                 <td>
21
-                    <button styleName="play">
22
+                    <button styleName="play" title={formatMessage(albumMessages["app.common.play"])} onClick={() => this.props.playAction(this.props.track.get("id"))}>
22 23
                         <span className="sr-only">
23 24
                             <FormattedMessage {...albumMessages["app.common.play"]} />
24 25
                         </span>
@@ -33,18 +34,21 @@ class AlbumTrackRowCSS extends Component {
33 34
     }
34 35
 }
35 36
 
36
-AlbumTrackRowCSS.propTypes = {
37
-    track: PropTypes.instanceOf(Immutable.Map).isRequired
37
+AlbumTrackRowCSSIntl.propTypes = {
38
+    playAction: PropTypes.func.isRequired,
39
+    track: PropTypes.instanceOf(Immutable.Map).isRequired,
40
+    intl: intlShape.isRequired
38 41
 };
39 42
 
40
-export let AlbumTrackRow = CSSModules(AlbumTrackRowCSS, css);
43
+export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
41 44
 
42 45
 
43 46
 class AlbumTracksTableCSS extends Component {
44 47
     render () {
45 48
         let rows = [];
49
+        const playAction = this.props.playAction;
46 50
         this.props.tracks.forEach(function (item) {
47
-            rows.push(<AlbumTrackRow track={item} key={item.get("id")} />);
51
+            rows.push(<AlbumTrackRow playAction={playAction} track={item} key={item.get("id")} />);
48 52
         });
49 53
         return (
50 54
             <table className="table table-hover" styleName="songs">
@@ -57,6 +61,7 @@ class AlbumTracksTableCSS extends Component {
57 61
 }
58 62
 
59 63
 AlbumTracksTableCSS.propTypes = {
64
+    playAction: PropTypes.func.isRequired,
60 65
     tracks: PropTypes.instanceOf(Immutable.List).isRequired
61 66
 };
62 67
 
@@ -75,7 +80,7 @@ class AlbumRowCSS extends Component {
75 80
                 <div className="col-xs-9 col-sm-10 table-responsive">
76 81
                     {
77 82
                         this.props.songs.size > 0 ?
78
-                            <AlbumTracksTable tracks={this.props.songs} /> :
83
+                            <AlbumTracksTable playAction={this.props.playAction} tracks={this.props.songs} /> :
79 84
                             null
80 85
                     }
81 86
                 </div>
@@ -85,6 +90,7 @@ class AlbumRowCSS extends Component {
85 90
 }
86 91
 
87 92
 AlbumRowCSS.propTypes = {
93
+    playAction: PropTypes.func.isRequired,
88 94
     album: PropTypes.instanceOf(Immutable.Map).isRequired,
89 95
     songs: PropTypes.instanceOf(Immutable.List).isRequired
90 96
 };

+ 4
- 3
app/components/Artist.jsx View File

@@ -26,7 +26,7 @@ class ArtistCSS extends Component {
26 26
             </div>
27 27
         );
28 28
 
29
-        if (this.props.isFetching && !this.props.artist) {
29
+        if (this.props.isFetching && !this.props.artist.size > 0) {
30 30
             // Loading
31 31
             return loading;
32 32
         }
@@ -37,7 +37,7 @@ class ArtistCSS extends Component {
37 37
         }
38 38
 
39 39
         let albumsRows = [];
40
-        const { albums, songs } = this.props;
40
+        const { albums, songs, playAction } = this.props;
41 41
         const artistAlbums = this.props.artist.get("albums");
42 42
         if (albums && songs && artistAlbums && artistAlbums.size > 0) {
43 43
             this.props.artist.get("albums").forEach(function (album) {
@@ -45,7 +45,7 @@ class ArtistCSS extends Component {
45 45
                 const albumSongs = album.get("tracks").map(
46 46
                     id => songs.get(id)
47 47
                 );
48
-                albumsRows.push(<AlbumRow album={album} songs={albumSongs} key={album.get("id")} />);
48
+                albumsRows.push(<AlbumRow playAction={playAction} album={album} songs={albumSongs} key={album.get("id")} />);
49 49
             });
50 50
         }
51 51
         else {
@@ -76,6 +76,7 @@ class ArtistCSS extends Component {
76 76
 }
77 77
 
78 78
 ArtistCSS.propTypes = {
79
+    playAction: PropTypes.func.isRequired,
79 80
     isFetching: PropTypes.bool.isRequired,
80 81
     error: PropTypes.string,
81 82
     artist: PropTypes.instanceOf(Immutable.Map),

+ 14
- 8
app/components/Songs.jsx View File

@@ -1,7 +1,7 @@
1 1
 import React, { Component, PropTypes } from "react";
2 2
 import { Link} from "react-router";
3 3
 import CSSModules from "react-css-modules";
4
-import { defineMessages, FormattedMessage } from "react-intl";
4
+import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
5 5
 import FontAwesome from "react-fontawesome";
6 6
 import Immutable from "immutable";
7 7
 import Fuse from "fuse.js";
@@ -18,15 +18,16 @@ import css from "../styles/Songs.scss";
18 18
 
19 19
 const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
20 20
 
21
-class SongsTableRowCSS extends Component {
21
+class SongsTableRowCSSIntl extends Component {
22 22
     render () {
23
+        const { formatMessage } = this.props.intl;
23 24
         const length = formatLength(this.props.song.get("time"));
24 25
         const linkToArtist = "/artist/" + this.props.song.getIn(["artist", "id"]);
25 26
         const linkToAlbum = "/album/" + this.props.song.getIn(["album", "id"]);
26 27
         return (
27 28
             <tr>
28 29
                 <td>
29
-                    <button styleName="play">
30
+                    <button styleName="play" title={formatMessage(songsMessages["app.common.play"])} onClick={() => this.props.playAction(this.props.song.get("id"))}>
30 31
                         <span className="sr-only">
31 32
                             <FormattedMessage {...songsMessages["app.common.play"]} />
32 33
                         </span>
@@ -43,11 +44,13 @@ class SongsTableRowCSS extends Component {
43 44
     }
44 45
 }
45 46
 
46
-SongsTableRowCSS.propTypes = {
47
-    song: PropTypes.instanceOf(Immutable.Map).isRequired
47
+SongsTableRowCSSIntl.propTypes = {
48
+    playAction: PropTypes.func.isRequired,
49
+    song: PropTypes.instanceOf(Immutable.Map).isRequired,
50
+    intl: intlShape.isRequired
48 51
 };
49 52
 
50
-export let SongsTableRow = CSSModules(SongsTableRowCSS, css);
53
+export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
51 54
 
52 55
 
53 56
 class SongsTableCSS extends Component {
@@ -67,8 +70,9 @@ class SongsTableCSS extends Component {
67 70
         }
68 71
 
69 72
         let rows = [];
73
+        const { playAction } = this.props;
70 74
         displayedSongs.forEach(function (song) {
71
-            rows.push(<SongsTableRow song={song} key={song.get("id")} />);
75
+            rows.push(<SongsTableRow playAction={playAction} song={song} key={song.get("id")} />);
72 76
         });
73 77
         let loading = null;
74 78
         if (rows.length == 0 && this.props.isFetching) {
@@ -112,6 +116,7 @@ class SongsTableCSS extends Component {
112 116
 }
113 117
 
114 118
 SongsTableCSS.propTypes = {
119
+    playAction: PropTypes.func.isRequired,
115 120
     songs: PropTypes.instanceOf(Immutable.List).isRequired,
116 121
     filterText: PropTypes.string
117 122
 };
@@ -145,7 +150,7 @@ export default class FilterablePaginatedSongsTable extends Component {
145 150
             <div>
146 151
                 { error }
147 152
                 <FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
148
-                <SongsTable isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} />
153
+                <SongsTable playAction={this.props.playAction} isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} />
149 154
                 <Pagination {...this.props.pagination} />
150 155
             </div>
151 156
         );
@@ -153,6 +158,7 @@ export default class FilterablePaginatedSongsTable extends Component {
153 158
 }
154 159
 
155 160
 FilterablePaginatedSongsTable.propTypes = {
161
+    playAction: PropTypes.func.isRequired,
156 162
     isFetching: PropTypes.bool.isRequired,
157 163
     error: PropTypes.string,
158 164
     songs: PropTypes.instanceOf(Immutable.List).isRequired,

+ 34
- 15
app/components/elements/WebPlayer.jsx View File

@@ -23,15 +23,24 @@ class WebPlayerCSSIntl extends Component {
23 23
     artOpacityHandler (ev) {
24 24
         if (ev.type == "mouseover") {
25 25
             this.refs.art.style.opacity = "1";
26
+            this.refs.artText.style.display = "none";
26 27
         } else {
27 28
             this.refs.art.style.opacity = "0.75";
29
+            this.refs.artText.style.display = "block";
28 30
         }
29 31
     }
30 32
 
31 33
     render () {
32 34
         const { formatMessage } = this.props.intl;
33 35
 
36
+        const song = this.props.currentTrack;
37
+        if (!song) {
38
+            return (<div></div>);
39
+        }
40
+
34 41
         const playPause = this.props.isPlaying ? "pause" : "play";
42
+        const volumeMute = this.props.isMute ? "volume-off" : "volume-up";
43
+
35 44
         const randomBtnStyles = ["randomBtn"];
36 45
         const repeatBtnStyles = ["repeatBtn"];
37 46
         if (this.props.isRandom) {
@@ -46,36 +55,38 @@ class WebPlayerCSSIntl extends Component {
46 55
                 <div className="col-xs-12">
47 56
                     <div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
48 57
                         <div className="col-xs-12">
49
-                            <img src={this.props.song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
50
-                            <h2>{this.props.song.get("title")}</h2>
51
-                            <h3>
52
-                                <span className="text-capitalize">
53
-                                    <FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
54
-                                </span> {this.props.song.get("artist")}
55
-                            </h3>
58
+                            <img src={song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
59
+                            <div ref="artText">
60
+                                <h2>{song.get("title")}</h2>
61
+                                <h3>
62
+                                    <span className="text-capitalize">
63
+                                        <FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
64
+                                    </span> { this.props.currentArtist.get("name") }
65
+                                </h3>
66
+                            </div>
56 67
                         </div>
57 68
                     </div>
58 69
 
59 70
                     <div className="row text-center" styleName="controls">
60 71
                         <div className="col-xs-12">
61
-                            <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])}>
72
+                            <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])} onClick={this.props.onPrev}>
62 73
                                 <FontAwesome name="step-backward" />
63 74
                             </button>
64
-                            <button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])}>
75
+                            <button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])} onClick={this.props.onPlayPause}>
65 76
                                 <FontAwesome name={playPause} />
66 77
                             </button>
67
-                            <button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])}>
78
+                            <button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])} onClick={this.props.onSkip}>
68 79
                                 <FontAwesome name="step-forward" />
69 80
                             </button>
70 81
                         </div>
71 82
                         <div className="col-xs-12">
72
-                            <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])}>
73
-                                <FontAwesome name="volume-up" />
83
+                            <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}>
84
+                                <FontAwesome name={volumeMute} />
74 85
                             </button>
75
-                            <button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat}>
86
+                            <button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={this.props.onRepeat}>
76 87
                                 <FontAwesome name="repeat" />
77 88
                             </button>
78
-                            <button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom}>
89
+                            <button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom} onClick={this.props.onRandom}>
79 90
                                 <FontAwesome name="random" />
80 91
                             </button>
81 92
                             <button styleName="playlistBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.playlist"])} title={formatMessage(webplayerMessages["app.webplayer.playlist"])}>
@@ -90,10 +101,18 @@ class WebPlayerCSSIntl extends Component {
90 101
 }
91 102
 
92 103
 WebPlayerCSSIntl.propTypes = {
93
-    song: PropTypes.instanceOf(Immutable.Map).isRequired,
94 104
     isPlaying: PropTypes.bool.isRequired,
95 105
     isRandom: PropTypes.bool.isRequired,
96 106
     isRepeat: PropTypes.bool.isRequired,
107
+    isMute: PropTypes.bool.isRequired,
108
+    currentTrack: PropTypes.instanceOf(Immutable.Map),
109
+    currentArtist: PropTypes.instanceOf(Immutable.Map),
110
+    onPlayPause: PropTypes.func.isRequired,
111
+    onPrev: PropTypes.func.isRequired,
112
+    onSkip: PropTypes.func.isRequired,
113
+    onRandom: PropTypes.func.isRequired,
114
+    onRepeat: PropTypes.func.isRequired,
115
+    onMute: PropTypes.func.isRequired,
97 116
     intl: intlShape.isRequired
98 117
 };
99 118
 

+ 17
- 0
app/models/webplayer.js View File

@@ -0,0 +1,17 @@
1
+import Immutable from "immutable";
2
+
3
+export const entitiesRecord = new Immutable.Record({
4
+    artists: new Immutable.Map(),
5
+    albums: new Immutable.Map(),
6
+    tracks: new Immutable.Map()
7
+});
8
+
9
+export const stateRecord = new Immutable.Record({
10
+    isPlaying: false,
11
+    isRandom: false,
12
+    isRepeat: false,
13
+    isMute: false,
14
+    currentIndex: 0,
15
+    playlist: new Immutable.List(),
16
+    entities: new entitiesRecord()
17
+});

+ 3
- 1
app/reducers/index.js View File

@@ -3,6 +3,7 @@ import { combineReducers } from "redux";
3 3
 
4 4
 import auth from "./auth";
5 5
 import paginate from "./paginate";
6
+import webplayer from "./webplayer";
6 7
 
7 8
 import * as ActionTypes from "../actions";
8 9
 
@@ -16,7 +17,8 @@ const api = paginate([
16 17
 const rootReducer = combineReducers({
17 18
     routing,
18 19
     auth,
19
-    api
20
+    api,
21
+    webplayer
20 22
 });
21 23
 
22 24
 export default rootReducer;

+ 51
- 0
app/reducers/webplayer.js View File

@@ -0,0 +1,51 @@
1
+import Immutable from "immutable";
2
+
3
+import {
4
+    PUSH_PLAYLIST,
5
+    CHANGE_TRACK,
6
+    PLAY_PAUSE,
7
+    TOGGLE_RANDOM,
8
+    TOGGLE_REPEAT,
9
+    TOGGLE_MUTE } from "../actions";
10
+import { createReducer } from "../utils";
11
+import { stateRecord } from "../models/webplayer";
12
+
13
+/**
14
+ * Initial state
15
+ */
16
+
17
+var initialState = new stateRecord();
18
+
19
+
20
+/**
21
+ * Reducers
22
+ */
23
+
24
+export default createReducer(initialState, {
25
+    [PLAY_PAUSE]: (state, payload) => {
26
+        return state.set("isPlaying", payload.isPlaying);
27
+    },
28
+    [CHANGE_TRACK]: (state, payload) => {
29
+        return state.set("currentIndex", payload.index);
30
+    },
31
+    [PUSH_PLAYLIST]: (state, payload) => {
32
+        return (
33
+            state
34
+            .set("playlist", new Immutable.List(payload.playlist))
35
+            .setIn(["entities", "artists"], new Immutable.Map(payload.artists))
36
+            .setIn(["entities", "albums"], new Immutable.Map(payload.albums))
37
+            .setIn(["entities", "tracks"], new Immutable.Map(payload.tracks))
38
+            .set("currentIndex", 0)
39
+            .set("isPlaying", true)
40
+        );
41
+    },
42
+    [TOGGLE_RANDOM]: (state) => {
43
+        return state.set("isRandom", !state.get("isRandom"));
44
+    },
45
+    [TOGGLE_REPEAT]: (state) => {
46
+        return state.set("isRepeat", !state.get("isRepeat"));
47
+    },
48
+    [TOGGLE_MUTE]: (state) => {
49
+        return state.set("isMute", !state.get("isMute"));
50
+    },
51
+});

+ 5
- 2
app/styles/elements/WebPlayer.scss View File

@@ -25,11 +25,14 @@ $controlsMarginTop: 10px;
25 25
 .btn {
26 26
     background: transparent;
27 27
     border: none;
28
-    opacity: 0.8;
28
+    opacity: 0.4;
29 29
 }
30 30
 
31
-.btn:hover {
31
+.btn:hover,
32
+.btn:active,
33
+.btn:focus {
32 34
     opacity: 1;
35
+    outline: none;
33 36
 }
34 37
 
35 38
 .prevBtn,

+ 2
- 2
app/views/ArtistPage.jsx View File

@@ -27,14 +27,14 @@ class ArtistPageIntl extends Component {
27 27
         const {formatMessage} = this.props.intl;
28 28
         const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
29 29
         return (
30
-            <Artist isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
30
+            <Artist playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
31 31
         );
32 32
     }
33 33
 }
34 34
 
35 35
 const mapStateToProps = (state, ownProps) => {
36 36
     const artists = state.api.entities.get("artist");
37
-    let artist = undefined;
37
+    let artist = new Immutable.Map();
38 38
     let albums = new Immutable.Map();
39 39
     let songs = new Immutable.Map();
40 40
     if (artists) {

+ 1
- 1
app/views/SongsPage.jsx View File

@@ -35,7 +35,7 @@ class SongsPageIntl extends Component {
35 35
         const {formatMessage} = this.props.intl;
36 36
         const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
37 37
         return (
38
-            <Songs isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
38
+            <Songs playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
39 39
         );
40 40
     }
41 41
 }

+ 98
- 9
app/views/WebPlayer.jsx View File

@@ -1,23 +1,112 @@
1 1
 import React, { Component } from "react";
2
+import { bindActionCreators } from "redux";
3
+import { connect } from "react-redux";
4
+import { Howl } from "howler";
2 5
 import Immutable from "immutable";
3 6
 
7
+import * as actionCreators from "../actions";
8
+
4 9
 import WebPlayerComponent from "../components/elements/WebPlayer";
5 10
 
11
+class WebPlayer extends Component {
12
+    constructor (props) {
13
+        super(props);
14
+
15
+        this.play = this.play.bind(this);
16
+
17
+        this.howl = null;
18
+    }
19
+
20
+    componentDidMount () {
21
+        this.play(this.props.isPlaying);
22
+    }
23
+
24
+    componentWillUpdate (nextProps) {
25
+        // Toggle play / pause
26
+        if (nextProps.isPlaying != this.props.isPlaying) {
27
+            // This check ensure we do not start multiple times the same music.
28
+            this.play(nextProps);
29
+        }
30
+
31
+        // Toggle mute / unmute
32
+        if (this.howl) {
33
+            this.howl.mute(nextProps.isMute);
34
+        }
35
+    }
36
+
37
+    getCurrentTrackPath (props) {
38
+        return [
39
+            "tracks",
40
+            props.playlist.get(props.currentIndex)
41
+        ];
42
+    }
43
+
44
+    play (props) {
45
+        if (props.isPlaying) {
46
+            if (!this.howl) {
47
+                const url = props.entities.getIn(
48
+                    Array.concat([], this.getCurrentTrackPath(props), ["url"])
49
+                );
50
+                if (!url) {
51
+                    // TODO: Error handling
52
+                    return;
53
+                }
54
+                this.howl = new Howl({
55
+                    src: [url],
56
+                    html5: true,
57
+                    loop: false,
58
+                    mute: props.isMute,
59
+                    autoplay: false,
60
+                });
61
+            }
62
+            this.howl.play();
63
+        }
64
+        else {
65
+            if (this.howl) {
66
+                this.howl.pause();
67
+            }
68
+        }
69
+    }
6 70
 
7
-export default class WebPlayer extends Component {
8 71
     render () {
72
+        const currentTrack = this.props.entities.getIn(this.getCurrentTrackPath(this.props));
73
+        let currentArtist = new Immutable.Map();
74
+        if (currentTrack) {
75
+            currentArtist = this.props.entities.getIn(["artists", currentTrack.get("artist")]);
76
+        }
77
+
9 78
         const webplayerProps = {
10
-            song: new Immutable.Map({
11
-                art: "http://albumartcollection.com/wp-content/uploads/2011/07/summer-album-art.jpg",
12
-                title: "Tel-ho",
13
-                artist: "Lapso Laps",
14
-            }),
15
-            isPlaying: false,
16
-            isRandom: false,
17
-            isRepeat: true
79
+            isPlaying: this.props.isPlaying,
80
+            isRandom: this.props.isRandom,
81
+            isRepeat: this.props.isRepeat,
82
+            isMute: this.props.isMute,
83
+            currentTrack: currentTrack,
84
+            currentArtist: currentArtist,
85
+            onPlayPause: (() => this.props.actions.togglePlaying()),
86
+            onPrev: this.props.actions.playPrevious,
87
+            onSkip: this.props.actions.playNext,
88
+            onRandom: this.props.actions.toggleRandom,
89
+            onRepeat: this.props.actions.toggleRepeat,
90
+            onMute: this.props.actions.toggleMute
18 91
         };
19 92
         return (
20 93
             <WebPlayerComponent {...webplayerProps} />
21 94
         );
22 95
     }
23 96
 }
97
+
98
+const mapStateToProps = (state) => ({
99
+    isPlaying: state.webplayer.isPlaying,
100
+    isRandom: state.webplayer.isRandom,
101
+    isRepeat: state.webplayer.isRepeat,
102
+    isMute: state.webplayer.isMute,
103
+    currentIndex: state.webplayer.currentIndex,
104
+    playlist: state.webplayer.playlist,
105
+    entities: state.webplayer.entities
106
+});
107
+
108
+const mapDispatchToProps = (dispatch) => ({
109
+    actions: bindActionCreators(actionCreators, dispatch)
110
+});
111
+
112
+export default connect(mapStateToProps, mapDispatchToProps)(WebPlayer);

+ 1
- 0
package.json View File

@@ -26,6 +26,7 @@
26 26
     "eslint": "^3.2.2",
27 27
     "font-awesome": "^4.6.3",
28 28
     "fuse.js": "^2.4.1",
29
+    "howler": "^2.0.0",
29 30
     "html5shiv": "^3.7.3",
30 31
     "humps": "^1.1.0",
31 32
     "imagesloaded": "^4.1.0",

+ 3
- 3
public/1.1.js
File diff suppressed because it is too large
View File


+ 1
- 1
public/1.1.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
public/fix.ie9.js View File

@@ -1,2 +1,2 @@
1
-!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(626);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},626:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
1
+!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(630);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},630:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
2 2
 //# sourceMappingURL=fix.ie9.js.map

+ 1
- 1
public/fix.ie9.js.map
File diff suppressed because it is too large
View File


+ 27
- 26
public/index.js
File diff suppressed because it is too large
View File


+ 1
- 1
public/index.js.map
File diff suppressed because it is too large
View File


+ 1
- 1
public/style.css
File diff suppressed because it is too large
View File