From 5ac503bb3a60ad77a1e239b46c66047facbe741b Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Fri, 10 Feb 2017 09:06:41 +1300 Subject: [PATCH] Playback history --- mopidy_iris/__init__.py | 2 +- src/js/components/Track.js | 50 +++-- src/js/components/TrackList.js | 33 ++- src/js/index.js | 2 + src/js/services/mopidy/actions.js | 7 + src/js/services/mopidy/middleware.js | 43 ++-- src/js/services/mopidy/reducer.js | 16 ++ src/js/views/Queue.js | 9 +- src/js/views/QueueHistory.js | 88 ++++++++ src/scss/components/_lists.scss | 288 ++++++++++++++------------- 10 files changed, 353 insertions(+), 185 deletions(-) create mode 100755 src/js/views/QueueHistory.js diff --git a/mopidy_iris/__init__.py b/mopidy_iris/__init__.py index e54741b9..ef56ab74 100755 --- a/mopidy_iris/__init__.py +++ b/mopidy_iris/__init__.py @@ -8,7 +8,7 @@ from frontend import IrisFrontend from http import RequestHandler logger = logging.getLogger(__name__) -__version__ = '2.11.3' +__version__ = '2.11.4' ## # Core extension class diff --git a/src/js/components/Track.js b/src/js/components/Track.js index 4020739d..f0dc358f 100755 --- a/src/js/components/Track.js +++ b/src/js/components/Track.js @@ -45,6 +45,7 @@ export default class Track extends React.Component{ var track = this.props.track; var className = 'list-item track'; if( typeof(track.selected) !== 'undefined' && track.selected ) className += ' selected'; + if( typeof(track.type) !== 'undefined' ) className += ' '+track.type; if( track.playing ) className += ' playing'; var album = '-' @@ -56,6 +57,41 @@ export default class Track extends React.Component{ } } + switch (track.type){ + + case 'history': + var track_columns = ( + + + { track.name ? track.name : {track.uri} } + + + { track.played_at ? ago : null } + + + ) + break; + + default: + var track_columns = ( + + + { track.name ? track.name : {track.uri} } + + + { track.artists ? : '-' } + + + {album} + + + { track.duration_ms ? : null } + { track.length ? : null } + + + ) + } + return (
this.handleContextMenu(e) }> { this.props.track.selected ? : null } { this.props.track.playing ? : null } - - { track.name ? track.name : {track.uri} } - - - { track.artists ? : '-' } - - - {album} - - - { track.duration_ms ? : null } - { track.length ? : null } - + { track_columns } { this.props.show_source_icon ? : null }
); diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js index c5cdbad8..f1427e87 100755 --- a/src/js/components/TrackList.js +++ b/src/js/components/TrackList.js @@ -257,15 +257,28 @@ class TrackList extends React.Component{ renderHeader(){ if( this.props.noheader ) return null - - return ( -
- Name - Artists - Album - Length -
- ) + + switch (this.props.context){ + + case 'history': + return ( +
+ Name + Started playing +
+ ) + break + + default: + return ( +
+ Name + Artists + Album + Length +
+ ) + } } render(){ @@ -273,7 +286,7 @@ class TrackList extends React.Component{ let self = this; return ( -
+
{ this.renderHeader() } { this.state.tracks.map( diff --git a/src/js/index.js b/src/js/index.js index d1911ad4..422c41a5 100755 --- a/src/js/index.js +++ b/src/js/index.js @@ -18,6 +18,7 @@ import Artist from './views/Artist' import Playlist from './views/Playlist' import User from './views/User' import Queue from './views/Queue' +import QueueHistory from './views/QueueHistory' import Settings from './views/Settings' import Debug from './views/Debug' import Search from './views/Search' @@ -54,6 +55,7 @@ ReactDOM.render( + diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index 0fe7d7f0..a9a9df13 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -225,4 +225,11 @@ export function getSearchResults(query, uris = null, fields = ['any']){ fields: fields } } + +export function getQueueHistory(){ + return { + type: 'MOPIDY_INSTRUCT', + call: 'history.getHistory' + } +} \ No newline at end of file diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index d3c737f0..9ea7b4a8 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -163,7 +163,6 @@ const MopidyMiddleware = (function(){ instruct( socket, store, action.call, action.value ) break; - // send an instruction to the websocket case 'MOPIDY_DEBUG': instruct( socket, store, action.call, action.value ) .then( response => { @@ -171,7 +170,29 @@ const MopidyMiddleware = (function(){ }) break; - // send an instruction to the websocket + case 'MOPIDY_URISCHEMES': + var uri_schemes = action.data + var remove = ['http','https','mms','rtmp','rtmps','rtsp','sc','spotify'] + + // remove all our ignored types + for( var i = 0; i < remove.length; i++ ){ + var index = uri_schemes.indexOf(remove[i]) + if( index > -1 ) uri_schemes.splice(index, 1); + } + + // append with ':' to make them a mopidy URI + for( var i = 0; i < uri_schemes.length; i++ ){ + uri_schemes[i] = uri_schemes[i] +':' + } + + store.dispatch({ type: 'MOPIDY_URISCHEMES_FILTERED', data: uri_schemes }); + break; + + + /** + * General playback + **/ + case 'MOPIDY_NEXT': var icon = false if( store.getState().ui.current_track ){ @@ -212,24 +233,6 @@ const MopidyMiddleware = (function(){ }) break; - case 'MOPIDY_URISCHEMES': - var uri_schemes = action.data - var remove = ['http','https','mms','rtmp','rtmps','rtsp','sc','spotify'] - - // remove all our ignored types - for( var i = 0; i < remove.length; i++ ){ - var index = uri_schemes.indexOf(remove[i]) - if( index > -1 ) uri_schemes.splice(index, 1); - } - - // append with ':' to make them a mopidy URI - for( var i = 0; i < uri_schemes.length; i++ ){ - uri_schemes[i] = uri_schemes[i] +':' - } - - store.dispatch({ type: 'MOPIDY_URISCHEMES_FILTERED', data: uri_schemes }); - break; - case 'MOPIDY_ENQUEUE_URIS_NEXT': var current_track = store.getState().ui.current_track diff --git a/src/js/services/mopidy/reducer.js b/src/js/services/mopidy/reducer.js index af55520f..339a3b6f 100755 --- a/src/js/services/mopidy/reducer.js +++ b/src/js/services/mopidy/reducer.js @@ -62,6 +62,22 @@ export default function reducer(mopidy = {}, action){ time_position: action.data }); + case 'MOPIDY_HISTORY': + var history = [] + for (var i = 0; i < action.data.length; i++){ + history.push(Object.assign( + {}, + action.data[i][1], + { + played_at: action.data[i][0], + type: 'history' + } + )) + } + return Object.assign({}, mopidy, { + queue_history: history + }); + /** * Asset-oriented actions diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js index f9e46f87..e5b56ccc 100755 --- a/src/js/views/Queue.js +++ b/src/js/views/Queue.js @@ -1,6 +1,7 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' +import { hashHistory } from 'react-router' import { bindActionCreators } from 'redux' import FontAwesome from 'react-fontawesome' @@ -58,14 +59,18 @@ class Queue extends React.Component{ render(){ var actions = ( - - + ) diff --git a/src/js/views/QueueHistory.js b/src/js/views/QueueHistory.js new file mode 100755 index 00000000..d7ef14f7 --- /dev/null +++ b/src/js/views/QueueHistory.js @@ -0,0 +1,88 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { hashHistory } from 'react-router' +import { bindActionCreators } from 'redux' +import FontAwesome from 'react-fontawesome' + +import TrackList from '../components/TrackList' +import SidebarToggleButton from '../components/SidebarToggleButton' +import Header from '../components/Header' + +import * as uiActions from '../services/ui/actions' +import * as pusherActions from '../services/pusher/actions' +import * as spotifyActions from '../services/spotify/actions' +import * as mopidyActions from '../services/mopidy/actions' + +class QueueHistory extends React.Component{ + + constructor(props) { + super(props); + } + + componentDidMount(){ + this.loadHistory() + } + + componentWillReceiveProps(nextProps){ + if( !this.props.mopidy_connected && nextProps.mopidy_connected ){ + this.loadHistory(nextProps) + } + } + + loadHistory(props = this.props){ + if (props.mopidy_connected){ + this.props.mopidyActions.getQueueHistory() + } + } + + render(){ + var actions = ( + + + + ) + + return ( +
+
+ +
+ +
+ +
+ ); + } +} + + +/** + * Export our component + * + * We also integrate our global store, using connect() + **/ + +const mapStateToProps = (state, ownProps) => { + return { + mopidy_connected: state.mopidy.connected, + queue_history: (state.mopidy.queue_history ? state.mopidy.queue_history : []) + } +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch), + pusherActions: bindActionCreators(pusherActions, dispatch), + spotifyActions: bindActionCreators(spotifyActions, dispatch), + mopidyActions: bindActionCreators(mopidyActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(QueueHistory) \ No newline at end of file diff --git a/src/scss/components/_lists.scss b/src/scss/components/_lists.scss index 6b513b07..85d0fc7d 100755 --- a/src/scss/components/_lists.scss +++ b/src/scss/components/_lists.scss @@ -1,154 +1,164 @@ -.list-item { +.list-wrapper { - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; + .list-item { - display: block; - position: relative; - padding: 9px 10px 9px 20px; - border-top: 1px solid $faint_grey; - border-bottom: 1px solid $faint_grey; - margin-bottom: -1px; - @include clearfix; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; - .source { - color: $mid_grey; - position: absolute; - top: 10px; - right: 15px; - } - - &.selected { - background: $yellow !important; - } - - &:not(.header):hover { - background: $faint_grey; - cursor: pointer; - border-color: $faint_grey; - } - - &.header { - font-size: 10px; - color: #CCCCCC; - text-transform: uppercase; - border-top: 0; - padding-bottom: 2px; - } - - - &.track { - padding-left: 40px; - - .col { - &.name { width: 40%; } - &.artists, - &.album { width: 26%; } - } - - .select-state { - position: absolute; - top: 12px; - left: 15px; - font-size: 10px; - } - - .play-state { - display: none; - position: absolute; - top: 13px; - left: 15px; - font-size: 10px; - } - - &.playing { - font-weight: 600; - .play-state { - display: block; - opacity: 1; - } - } - - &.playing.selected { - .play-state { - display: none; - } - } - - :root .dragging &:hover { - border-top: 3px solid $blue; - margin-top: -3px; - background: transparent; - } - - - @include responsive( $bp_medium ){ - padding: 7px 10px 7px 20px; - - .col { - &.name { width: 50%; } - &.artists { width: 50%; } - &.album { width: 50%; padding-left: 50%; color: $mid_grey; } - &.duration { display: none; } - } - - &.header { - display: none; - } - - .select-state, - .play-state { - top: 11px; - left: 5px; - } - } - - } - - - &.playlist { - - .col { - &.name { - width: 30%; - } - &.owner { - width: 20%; - } - } - } -} - -.artist-list { - .artist { display: block; - border: 0; - margin-bottom: 5px; + position: relative; + padding: 9px 10px 9px 20px; + border-top: 1px solid $faint_grey; + border-bottom: 1px solid $faint_grey; + margin-bottom: -1px; @include clearfix; - .thumbnail { - @include animate(); - width: 50px; - margin-right: 15px; - float: left; - border: 3px solid transparent; + .source { + color: $mid_grey; + position: absolute; + top: 10px; + right: 15px; } - .name { - display: block; - padding-top: 20px; + &.selected { + background: $yellow !important; + } + + &:not(.header):hover { + background: $faint_grey; + cursor: pointer; + border-color: $faint_grey; + } + + &.header { + font-size: 10px; + color: #CCCCCC; + text-transform: uppercase; + border-top: 0; + padding-bottom: 2px; + } + + &.track { + padding-left: 40px; + + .col { + &.name { width: 40%; } + &.artists, + &.album { width: 26%; } + } + + .select-state { + position: absolute; + top: 12px; + left: 15px; + font-size: 10px; + } + + .play-state { + display: none; + position: absolute; + top: 13px; + left: 15px; + font-size: 10px; + } + + &.playing { + font-weight: 600; + .play-state { + display: block; + opacity: 1; + } + } + + &.playing.selected { + .play-state { + display: none; + } + } + + :root .dragging &:hover { + border-top: 3px solid $blue; + margin-top: -3px; + background: transparent; + } + + @include responsive( $bp_medium ){ + padding: 7px 10px 7px 20px; + + .col { + &.name { width: 50%; } + &.artists { width: 50%; } + &.album { width: 50%; padding-left: 50%; color: $mid_grey; } + &.duration { display: none; } + } + + &.header { + display: none; + } + + .select-state, + .play-state { + top: 11px; + left: 5px; + } + } + + } + + + &.playlist { + + .col { + &.name { + width: 30%; + } + &.owner { + width: 20%; + } + } } } - a.artist:hover { - .thumbnail { - border-color: $turquoise; - opacity: 0.8; + .artist-list { + .artist { + display: block; + border: 0; + margin-bottom: 5px; + @include clearfix; + + .thumbnail { + @include animate(); + width: 50px; + margin-right: 15px; + float: left; + border: 3px solid transparent; + } + + .name { + display: block; + padding-top: 20px; + } + } + + a.artist:hover { + .thumbnail { + border-color: $turquoise; + opacity: 0.8; + } + } + } + + .history.track-list { + .list-item { + .col{ + &.name { width: 60%; } + &.played_at { width: 20%; } + } } } } \ No newline at end of file