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:
parent
4f8f201e67
commit
e24e5cd6a9
@ -69,21 +69,28 @@ export const SET_PLAYLIST = "SET_PLAYLIST";
|
||||
* @return Dispatch a SET_PLAYLIST action.
|
||||
*/
|
||||
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) => {
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: getState().webplayer.get("playlist").toArray(),
|
||||
}));
|
||||
dispatch(incrementRefCount({
|
||||
song: playlist,
|
||||
}));
|
||||
// Set playlist
|
||||
const oldPlaylist = getState().webplayer.get("playlist").toArray();
|
||||
|
||||
// Set new playlist
|
||||
dispatch ({
|
||||
type: SET_PLAYLIST,
|
||||
payload: {
|
||||
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.
|
||||
*/
|
||||
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) => {
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: getState().webplayer.get("playlist").toArray(),
|
||||
}));
|
||||
dispatch(incrementRefCount({
|
||||
song: [songID],
|
||||
}));
|
||||
// Get old and new playlists
|
||||
const oldPlaylist = getState().webplayer.get("playlist").toArray();
|
||||
|
||||
// Set new playlist
|
||||
dispatch({
|
||||
type: SET_PLAYLIST,
|
||||
@ -113,6 +120,15 @@ export function playSong(songID) {
|
||||
playlist: [songID],
|
||||
},
|
||||
});
|
||||
|
||||
// Handle reference counting
|
||||
dispatch(incrementRefCount({
|
||||
song: [songID],
|
||||
}));
|
||||
dispatch(decrementRefCount({
|
||||
song: oldPlaylist,
|
||||
}));
|
||||
|
||||
// Force playing
|
||||
dispatch(togglePlaying(true));
|
||||
};
|
||||
|
38
app/components/Playlist.jsx
Normal file
38
app/components/Playlist.jsx
Normal 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,
|
||||
};
|
@ -2,6 +2,7 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import CSSModules from "react-css-modules";
|
||||
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||
import { Link } from "react-router";
|
||||
import Immutable from "immutable";
|
||||
import FontAwesome from "react-fontawesome";
|
||||
|
||||
@ -63,12 +64,16 @@ class WebPlayerCSSIntl extends Component {
|
||||
// Get classes for random and repeat buttons
|
||||
const randomBtnStyles = ["randomBtn"];
|
||||
const repeatBtnStyles = ["repeatBtn"];
|
||||
const playlistBtnStyles = ["playlistBtn"];
|
||||
if (this.props.isRandom) {
|
||||
randomBtnStyles.push("active");
|
||||
}
|
||||
if (this.props.isRepeat) {
|
||||
repeatBtnStyles.push("active");
|
||||
}
|
||||
if (this.props.isPlaylistViewActive) {
|
||||
playlistBtnStyles.push("active");
|
||||
}
|
||||
|
||||
// Check if a song is currently playing
|
||||
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">
|
||||
<FontAwesome name="random" />
|
||||
</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" />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -182,6 +187,7 @@ WebPlayerCSSIntl.propTypes = {
|
||||
onRandom: PropTypes.func.isRequired,
|
||||
onRepeat: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
isPlaylistViewActive: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
|
@ -146,7 +146,7 @@ class SidebarLayoutIntl extends Component {
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<WebPlayer />
|
||||
<WebPlayer location={this.props.location} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -8,16 +8,17 @@ import RequireAuthentication from "./containers/RequireAuthentication";
|
||||
import App from "./containers/App";
|
||||
import SimpleLayout from "./components/layouts/Simple";
|
||||
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 DiscoverPage from "./views/DiscoverPage";
|
||||
import HomePage from "./views/HomePage";
|
||||
import LoginPage from "./views/LoginPage";
|
||||
import LogoutPage from "./views/LogoutPage";
|
||||
import ArtistsPage from "./views/ArtistsPage";
|
||||
import AlbumsPage from "./views/AlbumsPage";
|
||||
import PlaylistPage from "./views/PlaylistPage";
|
||||
import SongsPage from "./views/SongsPage";
|
||||
import ArtistPage from "./views/ArtistPage";
|
||||
import AlbumPage from "./views/AlbumPage";
|
||||
|
||||
export default (
|
||||
<Route path="/" component={App}> // Main container is App
|
||||
@ -34,6 +35,7 @@ export default (
|
||||
<Route path="albums" component={AlbumsPage} />
|
||||
<Route path="album/:id" component={AlbumPage} />
|
||||
<Route path="songs" component={SongsPage} />
|
||||
<Route path="playlist" component={PlaylistPage} />
|
||||
<IndexRoute component={HomePage} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
@ -54,6 +54,10 @@ $controlsMarginTop: 10px;
|
||||
font-size: $font-size-h2;
|
||||
}
|
||||
|
||||
.playlistBtn {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: $blue;
|
||||
}
|
||||
|
47
app/views/PlaylistPage.jsx
Normal file
47
app/views/PlaylistPage.jsx
Normal 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);
|
@ -1,5 +1,5 @@
|
||||
// NPM imports
|
||||
import React, { Component } from "react";
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { Howl } from "howler";
|
||||
@ -138,6 +138,11 @@ class WebPlayer extends Component {
|
||||
onRandom: this.props.actions.toggleRandom,
|
||||
onRepeat: this.props.actions.toggleRepeat,
|
||||
onMute: this.props.actions.toggleMute,
|
||||
isPlaylistViewActive: (
|
||||
(this.props.location && this.props.location.pathname == "/playlist")
|
||||
? true
|
||||
: false
|
||||
),
|
||||
};
|
||||
return (
|
||||
(this.props.playlist.size > 0)
|
||||
@ -146,6 +151,9 @@ class WebPlayer extends Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
WebPlayer.propTypes = {
|
||||
location: PropTypes.object,
|
||||
};
|
||||
const mapStateToProps = (state) => {
|
||||
const currentIndex = state.webplayer.currentIndex;
|
||||
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
@ -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
|
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
Loading…
Reference in New Issue
Block a user