Playback history

This commit is contained in:
James Barnsley
2017-02-10 09:06:41 +13:00
parent 8c99d219eb
commit 5ac503bb3a
10 changed files with 353 additions and 185 deletions

View File

@ -8,7 +8,7 @@ from frontend import IrisFrontend
from http import RequestHandler from http import RequestHandler
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
__version__ = '2.11.3' __version__ = '2.11.4'
## ##
# Core extension class # Core extension class

View File

@ -45,6 +45,7 @@ export default class Track extends React.Component{
var track = this.props.track; var track = this.props.track;
var className = 'list-item track'; var className = 'list-item track';
if( typeof(track.selected) !== 'undefined' && track.selected ) className += ' selected'; if( typeof(track.selected) !== 'undefined' && track.selected ) className += ' selected';
if( typeof(track.type) !== 'undefined' ) className += ' '+track.type;
if( track.playing ) className += ' playing'; if( track.playing ) className += ' playing';
var album = '-' var album = '-'
@ -56,6 +57,41 @@ export default class Track extends React.Component{
} }
} }
switch (track.type){
case 'history':
var track_columns = (
<span>
<span className="col name">
{ track.name ? track.name : <span className="grey-text">{track.uri}</span> }
</span>
<span className="col played_at">
{ track.played_at ? <span><Dater type="ago" data={track.played_at} /> ago</span> : null }
</span>
</span>
)
break;
default:
var track_columns = (
<span>
<span className="col name">
{ track.name ? track.name : <span className="grey-text">{track.uri}</span> }
</span>
<span className="col artists">
{ track.artists ? <ArtistSentence artists={track.artists} /> : '-' }
</span>
<span className="col album">
{album}
</span>
<span className="col duration">
{ track.duration_ms ? <Dater type="length" data={track.duration_ms} /> : null }
{ track.length ? <Dater type="length" data={track.length} /> : null }
</span>
</span>
)
}
return ( return (
<div <div
className={className} className={className}
@ -67,19 +103,7 @@ export default class Track extends React.Component{
onContextMenu={ e => this.handleContextMenu(e) }> onContextMenu={ e => this.handleContextMenu(e) }>
{ this.props.track.selected ? <FontAwesome name="check" className="select-state" fixedWidth /> : null } { this.props.track.selected ? <FontAwesome name="check" className="select-state" fixedWidth /> : null }
{ this.props.track.playing ? <FontAwesome name="play" className="play-state" fixedWidth /> : null } { this.props.track.playing ? <FontAwesome name="play" className="play-state" fixedWidth /> : null }
<span className="col name"> { track_columns }
{ track.name ? track.name : <span className="grey-text">{track.uri}</span> }
</span>
<span className="col artists">
{ track.artists ? <ArtistSentence artists={track.artists} /> : '-' }
</span>
<span className="col album">
{album}
</span>
<span className="col duration">
{ track.duration_ms ? <Dater type="length" data={track.duration_ms} /> : null }
{ track.length ? <Dater type="length" data={track.length} /> : null }
</span>
{ this.props.show_source_icon ? <FontAwesome className="source" name={helpers.sourceIcon(track.uri)} /> : null } { this.props.show_source_icon ? <FontAwesome className="source" name={helpers.sourceIcon(track.uri)} /> : null }
</div> </div>
); );

View File

@ -257,15 +257,28 @@ class TrackList extends React.Component{
renderHeader(){ renderHeader(){
if( this.props.noheader ) return null if( this.props.noheader ) return null
return ( switch (this.props.context){
<div className="list-item header track">
<span className="col name">Name</span> case 'history':
<span className="col artists">Artists</span> return (
<span className="col album">Album</span> <div className="list-item header track">
<span className="col duration">Length</span> <span className="col name">Name</span>
</div> <span className="col played_at">Started playing</span>
) </div>
)
break
default:
return (
<div className="list-item header track">
<span className="col name">Name</span>
<span className="col artists">Artists</span>
<span className="col album">Album</span>
<span className="col duration">Length</span>
</div>
)
}
} }
render(){ render(){
@ -273,7 +286,7 @@ class TrackList extends React.Component{
let self = this; let self = this;
return ( return (
<div className="track-list"> <div className={this.props.context+" track-list"}>
{ this.renderHeader() } { this.renderHeader() }
{ {
this.state.tracks.map( this.state.tracks.map(

View File

@ -18,6 +18,7 @@ import Artist from './views/Artist'
import Playlist from './views/Playlist' import Playlist from './views/Playlist'
import User from './views/User' import User from './views/User'
import Queue from './views/Queue' import Queue from './views/Queue'
import QueueHistory from './views/QueueHistory'
import Settings from './views/Settings' import Settings from './views/Settings'
import Debug from './views/Debug' import Debug from './views/Debug'
import Search from './views/Search' import Search from './views/Search'
@ -54,6 +55,7 @@ ReactDOM.render(
<IndexRoute component={Queue} /> <IndexRoute component={Queue} />
<Route path="queue" component={Queue} /> <Route path="queue" component={Queue} />
<Route path="queue-history" component={QueueHistory} />
<Route path="settings" component={Settings} /> <Route path="settings" component={Settings} />
<Route path="debug" component={Debug} /> <Route path="debug" component={Debug} />

View File

@ -225,4 +225,11 @@ export function getSearchResults(query, uris = null, fields = ['any']){
fields: fields fields: fields
} }
} }
export function getQueueHistory(){
return {
type: 'MOPIDY_INSTRUCT',
call: 'history.getHistory'
}
}

View File

@ -163,7 +163,6 @@ const MopidyMiddleware = (function(){
instruct( socket, store, action.call, action.value ) instruct( socket, store, action.call, action.value )
break; break;
// send an instruction to the websocket
case 'MOPIDY_DEBUG': case 'MOPIDY_DEBUG':
instruct( socket, store, action.call, action.value ) instruct( socket, store, action.call, action.value )
.then( response => { .then( response => {
@ -171,7 +170,29 @@ const MopidyMiddleware = (function(){
}) })
break; 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': case 'MOPIDY_NEXT':
var icon = false var icon = false
if( store.getState().ui.current_track ){ if( store.getState().ui.current_track ){
@ -212,24 +233,6 @@ const MopidyMiddleware = (function(){
}) })
break; 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': case 'MOPIDY_ENQUEUE_URIS_NEXT':
var current_track = store.getState().ui.current_track var current_track = store.getState().ui.current_track

View File

@ -62,6 +62,22 @@ export default function reducer(mopidy = {}, action){
time_position: action.data 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 * Asset-oriented actions

View File

@ -1,6 +1,7 @@
import React, { PropTypes } from 'react' import React, { PropTypes } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { hashHistory } from 'react-router'
import { bindActionCreators } from 'redux' import { bindActionCreators } from 'redux'
import FontAwesome from 'react-fontawesome' import FontAwesome from 'react-fontawesome'
@ -58,14 +59,18 @@ class Queue extends React.Component{
render(){ render(){
var actions = ( var actions = (
<span> <span>
<button onClick={() => this.props.uiActions.openModal('edit_radio')}> <button onClick={e => this.props.uiActions.openModal('edit_radio')}>
<FontAwesome name="spotify" />&nbsp; <FontAwesome name="spotify" />&nbsp;
Radio Radio
</button> </button>
<button onClick={() => this.props.mopidyActions.clearTracklist()}> <button onClick={e => this.props.mopidyActions.clearTracklist()}>
<FontAwesome name="trash" />&nbsp; <FontAwesome name="trash" />&nbsp;
Clear Clear
</button> </button>
<button onClick={e => hashHistory.push(global.baseURL+'queue-history')}>
<FontAwesome name="history" />&nbsp;
History
</button>
</span> </span>
) )

88
src/js/views/QueueHistory.js Executable file
View File

@ -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 = (
<span>
<button onClick={e => hashHistory.push(global.baseURL+'queue')}>
<FontAwesome name="reply" />&nbsp;
Back
</button>
</span>
)
return (
<div className="view playback-history-view">
<Header icon="play" title="Playback history" actions={actions} />
<section className="list-wrapper">
<TrackList
show_source_icon={true}
context="history"
tracks={this.props.queue_history} />
</section>
</div>
);
}
}
/**
* 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)

View File

@ -1,154 +1,164 @@
.list-item { .list-wrapper {
-webkit-touch-callout: none; .list-item {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
display: block; -webkit-touch-callout: none;
position: relative; -webkit-user-select: none;
padding: 9px 10px 9px 20px; -khtml-user-select: none;
border-top: 1px solid $faint_grey; -moz-user-select: none;
border-bottom: 1px solid $faint_grey; -ms-user-select: none;
margin-bottom: -1px; user-select: none;
@include clearfix;
.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; display: block;
border: 0; position: relative;
margin-bottom: 5px; padding: 9px 10px 9px 20px;
border-top: 1px solid $faint_grey;
border-bottom: 1px solid $faint_grey;
margin-bottom: -1px;
@include clearfix; @include clearfix;
.thumbnail { .source {
@include animate(); color: $mid_grey;
width: 50px; position: absolute;
margin-right: 15px; top: 10px;
float: left; right: 15px;
border: 3px solid transparent;
} }
.name { &.selected {
display: block; background: $yellow !important;
padding-top: 20px; }
&: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 { .artist-list {
.thumbnail { .artist {
border-color: $turquoise; display: block;
opacity: 0.8; 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%; }
}
} }
} }
} }