Playback history
This commit is contained in:
@ -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
|
||||
|
||||
@ -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 = (
|
||||
<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 (
|
||||
<div
|
||||
className={className}
|
||||
@ -67,19 +103,7 @@ export default class Track extends React.Component{
|
||||
onContextMenu={ e => this.handleContextMenu(e) }>
|
||||
{ this.props.track.selected ? <FontAwesome name="check" className="select-state" fixedWidth /> : null }
|
||||
{ this.props.track.playing ? <FontAwesome name="play" className="play-state" fixedWidth /> : null }
|
||||
<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>
|
||||
{ track_columns }
|
||||
{ this.props.show_source_icon ? <FontAwesome className="source" name={helpers.sourceIcon(track.uri)} /> : null }
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -257,15 +257,28 @@ class TrackList extends React.Component{
|
||||
|
||||
renderHeader(){
|
||||
if( this.props.noheader ) return null
|
||||
|
||||
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>
|
||||
)
|
||||
|
||||
switch (this.props.context){
|
||||
|
||||
case 'history':
|
||||
return (
|
||||
<div className="list-item header track">
|
||||
<span className="col name">Name</span>
|
||||
<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(){
|
||||
@ -273,7 +286,7 @@ class TrackList extends React.Component{
|
||||
|
||||
let self = this;
|
||||
return (
|
||||
<div className="track-list">
|
||||
<div className={this.props.context+" track-list"}>
|
||||
{ this.renderHeader() }
|
||||
{
|
||||
this.state.tracks.map(
|
||||
|
||||
@ -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(
|
||||
|
||||
<IndexRoute component={Queue} />
|
||||
<Route path="queue" component={Queue} />
|
||||
<Route path="queue-history" component={QueueHistory} />
|
||||
<Route path="settings" component={Settings} />
|
||||
<Route path="debug" component={Debug} />
|
||||
|
||||
|
||||
@ -225,4 +225,11 @@ export function getSearchResults(query, uris = null, fields = ['any']){
|
||||
fields: fields
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueueHistory(){
|
||||
return {
|
||||
type: 'MOPIDY_INSTRUCT',
|
||||
call: 'history.getHistory'
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = (
|
||||
<span>
|
||||
<button onClick={() => this.props.uiActions.openModal('edit_radio')}>
|
||||
<button onClick={e => this.props.uiActions.openModal('edit_radio')}>
|
||||
<FontAwesome name="spotify" />
|
||||
Radio
|
||||
</button>
|
||||
<button onClick={() => this.props.mopidyActions.clearTracklist()}>
|
||||
<button onClick={e => this.props.mopidyActions.clearTracklist()}>
|
||||
<FontAwesome name="trash" />
|
||||
Clear
|
||||
</button>
|
||||
<button onClick={e => hashHistory.push(global.baseURL+'queue-history')}>
|
||||
<FontAwesome name="history" />
|
||||
History
|
||||
</button>
|
||||
</span>
|
||||
)
|
||||
|
||||
|
||||
88
src/js/views/QueueHistory.js
Executable file
88
src/js/views/QueueHistory.js
Executable 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" />
|
||||
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)
|
||||
@ -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%; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user