Basic playlist viewing page

Add a playlist page to browse the current playlist at `/playlist`. This
is a very basic page that will need some rework.
This commit is contained in:
Lucas Verney 2016-08-12 00:57:12 +02:00
parent 4f8f201e67
commit e24e5cd6a9
15 changed files with 178 additions and 57 deletions

View File

@ -69,21 +69,28 @@ export const SET_PLAYLIST = "SET_PLAYLIST";
* @return Dispatch a SET_PLAYLIST action. * @return Dispatch a SET_PLAYLIST action.
*/ */
export function setPlaylist(playlist) { export function setPlaylist(playlist) {
// Attention, order of actions *do* matter in this method. We should first
// set the playlist and then handle the reference counting.
// We should first increment and then increment to avoid garbage collecting
// items that would stay in the playlist.
return (dispatch, getState) => { return (dispatch, getState) => {
// Handle reference counting const oldPlaylist = getState().webplayer.get("playlist").toArray();
dispatch(decrementRefCount({
song: getState().webplayer.get("playlist").toArray(), // Set new playlist
}));
dispatch(incrementRefCount({
song: playlist,
}));
// Set playlist
dispatch ({ dispatch ({
type: SET_PLAYLIST, type: SET_PLAYLIST,
payload: { payload: {
playlist: playlist, playlist: playlist,
}, },
}); });
// Handle reference counting
dispatch(incrementRefCount({
song: playlist,
}));
dispatch(decrementRefCount({
song: oldPlaylist,
}));
}; };
} }
@ -98,14 +105,14 @@ export function setPlaylist(playlist) {
* @return Dispatch a SET_PLAYLIST action to play this song and start playing. * @return Dispatch a SET_PLAYLIST action to play this song and start playing.
*/ */
export function playSong(songID) { export function playSong(songID) {
// Attention, order of actions *do* matter in this method. We should first
// set the playlist and then handle the reference counting.
// We should first increment and then increment to avoid garbage collecting
// items that would stay in the playlist.
return (dispatch, getState) => { return (dispatch, getState) => {
// Handle reference counting // Get old and new playlists
dispatch(decrementRefCount({ const oldPlaylist = getState().webplayer.get("playlist").toArray();
song: getState().webplayer.get("playlist").toArray(),
}));
dispatch(incrementRefCount({
song: [songID],
}));
// Set new playlist // Set new playlist
dispatch({ dispatch({
type: SET_PLAYLIST, type: SET_PLAYLIST,
@ -113,6 +120,15 @@ export function playSong(songID) {
playlist: [songID], playlist: [songID],
}, },
}); });
// Handle reference counting
dispatch(incrementRefCount({
song: [songID],
}));
dispatch(decrementRefCount({
song: oldPlaylist,
}));
// Force playing // Force playing
dispatch(togglePlaying(true)); dispatch(togglePlaying(true));
}; };

View File

@ -0,0 +1,38 @@
// NPM import
import React, { Component, PropTypes } from "react";
import Immutable from "immutable";
// Other components
import { SongsTable } from "./Songs";
/**
* An entire album row containing art and tracks table.
*/
export default class Playlist extends Component {
render() {
const currentSongSongsTableProps = {
playAction: this.props.playAction,
playNextAction: this.props.playNextAction,
songs: this.props.songs.slice(this.props.currentIndex, this.props.currentIndex + 1),
};
const fullPlaylistSongsTableProps = {
playAction: this.props.playAction,
playNextAction: this.props.playNextAction,
songs: this.props.songs,
};
return (
<div className="row">
<h2>Current song playing</h2>
<SongsTable {...currentSongSongsTableProps}/>
<h2>Full playlist</h2>
<SongsTable {...fullPlaylistSongsTableProps}/>
</div>
);
}
}
Playlist.propTypes = {
playAction: PropTypes.func.isRequired,
playNextAction: PropTypes.func.isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired,
currentIndex: PropTypes.number.isRequired,
};

View File

@ -2,6 +2,7 @@
import React, { Component, PropTypes } from "react"; import React, { Component, PropTypes } from "react";
import CSSModules from "react-css-modules"; import CSSModules from "react-css-modules";
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl"; import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
import { Link } from "react-router";
import Immutable from "immutable"; import Immutable from "immutable";
import FontAwesome from "react-fontawesome"; import FontAwesome from "react-fontawesome";
@ -63,12 +64,16 @@ class WebPlayerCSSIntl extends Component {
// Get classes for random and repeat buttons // Get classes for random and repeat buttons
const randomBtnStyles = ["randomBtn"]; const randomBtnStyles = ["randomBtn"];
const repeatBtnStyles = ["repeatBtn"]; const repeatBtnStyles = ["repeatBtn"];
const playlistBtnStyles = ["playlistBtn"];
if (this.props.isRandom) { if (this.props.isRandom) {
randomBtnStyles.push("active"); randomBtnStyles.push("active");
} }
if (this.props.isRepeat) { if (this.props.isRepeat) {
repeatBtnStyles.push("active"); repeatBtnStyles.push("active");
} }
if (this.props.isPlaylistViewActive) {
playlistBtnStyles.push("active");
}
// Check if a song is currently playing // Check if a song is currently playing
let art = null; let art = null;
@ -155,9 +160,9 @@ class WebPlayerCSSIntl extends Component {
<button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom} onClick={onRandom} ref="randomBtn"> <button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom} onClick={onRandom} ref="randomBtn">
<FontAwesome name="random" /> <FontAwesome name="random" />
</button> </button>
<button styleName="playlistBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.playlist"])} title={formatMessage(webplayerMessages["app.webplayer.playlist"])}> <Link to="/playlist" styleName={playlistBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.playlist"])} title={formatMessage(webplayerMessages["app.webplayer.playlist"])}>
<FontAwesome name="list" /> <FontAwesome name="list" />
</button> </Link>
</div> </div>
</div> </div>
</div> </div>
@ -182,6 +187,7 @@ WebPlayerCSSIntl.propTypes = {
onRandom: PropTypes.func.isRequired, onRandom: PropTypes.func.isRequired,
onRepeat: PropTypes.func.isRequired, onRepeat: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
isPlaylistViewActive: PropTypes.bool,
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };

View File

@ -146,7 +146,7 @@ class SidebarLayoutIntl extends Component {
</li> </li>
</ul> </ul>
</nav> </nav>
<WebPlayer /> <WebPlayer location={this.props.location} />
</div> </div>
</div> </div>

View File

@ -8,16 +8,17 @@ import RequireAuthentication from "./containers/RequireAuthentication";
import App from "./containers/App"; import App from "./containers/App";
import SimpleLayout from "./components/layouts/Simple"; import SimpleLayout from "./components/layouts/Simple";
import SidebarLayout from "./components/layouts/Sidebar"; import SidebarLayout from "./components/layouts/Sidebar";
import ArtistPage from "./views/ArtistPage";
import ArtistsPage from "./views/ArtistsPage";
import AlbumPage from "./views/AlbumPage";
import AlbumsPage from "./views/AlbumsPage";
import BrowsePage from "./views/BrowsePage"; import BrowsePage from "./views/BrowsePage";
import DiscoverPage from "./views/DiscoverPage"; import DiscoverPage from "./views/DiscoverPage";
import HomePage from "./views/HomePage"; import HomePage from "./views/HomePage";
import LoginPage from "./views/LoginPage"; import LoginPage from "./views/LoginPage";
import LogoutPage from "./views/LogoutPage"; import LogoutPage from "./views/LogoutPage";
import ArtistsPage from "./views/ArtistsPage"; import PlaylistPage from "./views/PlaylistPage";
import AlbumsPage from "./views/AlbumsPage";
import SongsPage from "./views/SongsPage"; import SongsPage from "./views/SongsPage";
import ArtistPage from "./views/ArtistPage";
import AlbumPage from "./views/AlbumPage";
export default ( export default (
<Route path="/" component={App}> // Main container is App <Route path="/" component={App}> // Main container is App
@ -34,6 +35,7 @@ export default (
<Route path="albums" component={AlbumsPage} /> <Route path="albums" component={AlbumsPage} />
<Route path="album/:id" component={AlbumPage} /> <Route path="album/:id" component={AlbumPage} />
<Route path="songs" component={SongsPage} /> <Route path="songs" component={SongsPage} />
<Route path="playlist" component={PlaylistPage} />
<IndexRoute component={HomePage} /> <IndexRoute component={HomePage} />
</Route> </Route>
</Route> </Route>

View File

@ -54,6 +54,10 @@ $controlsMarginTop: 10px;
font-size: $font-size-h2; font-size: $font-size-h2;
} }
.playlistBtn {
color: white;
}
.active { .active {
color: $blue; color: $blue;
} }

View File

@ -0,0 +1,47 @@
// NPM imports
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Immutable from "immutable";
// Actions
import * as actionCreators from "../actions";
// Components
import Playlist from "../components/Playlist";
/**
* Table of songs in the current playlist.
*/
class PlaylistPage extends Component {
render() {
return (
<Playlist playAction={this.props.actions.playSong} playNextAction={this.props.actions.pushSong} songs={this.props.songsList} currentIndex={this.props.currentIndex} />
);
}
}
const mapStateToProps = (state) => {
let songsList = new Immutable.List();
if (state.webplayer.playlist.size > 0) {
songsList = state.webplayer.playlist.map(function (id) {
let song = state.entities.getIn(["entities", "song", id]);
// Add artist and album infos to song
const artist = state.entities.getIn(["entities", "artist", song.get("artist")]);
const album = state.entities.getIn(["entities", "album", song.get("album")]);
return (
song
.set("artist", new Immutable.Map({id: artist.get("id"), name: artist.get("name")}))
.set("album", new Immutable.Map({id: album.get("id"), name: album.get("name")}))
);
});
}
return {
songsList: songsList,
currentIndex: state.webplayer.currentIndex,
};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(PlaylistPage);

View File

@ -1,5 +1,5 @@
// NPM imports // NPM imports
import React, { Component } from "react"; import React, { Component, PropTypes } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Howl } from "howler"; import { Howl } from "howler";
@ -138,6 +138,11 @@ class WebPlayer extends Component {
onRandom: this.props.actions.toggleRandom, onRandom: this.props.actions.toggleRandom,
onRepeat: this.props.actions.toggleRepeat, onRepeat: this.props.actions.toggleRepeat,
onMute: this.props.actions.toggleMute, onMute: this.props.actions.toggleMute,
isPlaylistViewActive: (
(this.props.location && this.props.location.pathname == "/playlist")
? true
: false
),
}; };
return ( return (
(this.props.playlist.size > 0) (this.props.playlist.size > 0)
@ -146,6 +151,9 @@ class WebPlayer extends Component {
); );
} }
} }
WebPlayer.propTypes = {
location: PropTypes.object,
};
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const currentIndex = state.webplayer.currentIndex; const currentIndex = state.webplayer.currentIndex;
const playlist = state.webplayer.playlist; const playlist = state.webplayer.playlist;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
!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(634);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},634: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)}}); !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(636);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},636: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)}});
//# sourceMappingURL=fix.ie9.js.map //# sourceMappingURL=fix.ie9.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long