|
@@ -1,4 +1,5 @@
|
|
|
import React, { Component } from 'react';
|
|
import React, { Component } from 'react';
|
|
|
|
|
+import Sound from 'react-sound';
|
|
|
import logo from './logo.svg';
|
|
import logo from './logo.svg';
|
|
|
import './App.css';
|
|
import './App.css';
|
|
|
|
|
|
|
@@ -28,6 +29,7 @@ function CategoryFlatList(props) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function formatDuration(duration) {
|
|
function formatDuration(duration) {
|
|
|
|
|
+ duration = Math.floor(duration);
|
|
|
var seconds = duration % 60;
|
|
var seconds = duration % 60;
|
|
|
const minutes = Math.floor(duration / 60);
|
|
const minutes = Math.floor(duration / 60);
|
|
|
if (seconds < 10) {
|
|
if (seconds < 10) {
|
|
@@ -73,7 +75,7 @@ function AlbumList(props) {
|
|
|
<table><tbody>
|
|
<table><tbody>
|
|
|
{type.albums.map(album => (
|
|
{type.albums.map(album => (
|
|
|
<tr key={album.id}>
|
|
<tr key={album.id}>
|
|
|
- <td><img src={album.cover} alt={album.album} /></td>
|
|
|
|
|
|
|
+ <td><img src={album.cover || logo} alt={album.album} /></td>
|
|
|
<td>{album.year}</td>
|
|
<td>{album.year}</td>
|
|
|
<td>{album.album}</td>
|
|
<td>{album.album}</td>
|
|
|
<td>{album.publisher}</td>
|
|
<td>{album.publisher}</td>
|
|
@@ -81,6 +83,7 @@ function AlbumList(props) {
|
|
|
<td>{album.genre}</td>
|
|
<td>{album.genre}</td>
|
|
|
<td>{album.track_count}</td>
|
|
<td>{album.track_count}</td>
|
|
|
<td>{formatDuration(album.total_duration)}</td>
|
|
<td>{formatDuration(album.total_duration)}</td>
|
|
|
|
|
+ <td><button onClick={() => props.onPlayAlbum(album)}>play</button></td>
|
|
|
</tr>
|
|
</tr>
|
|
|
))}
|
|
))}
|
|
|
</tbody></table>
|
|
</tbody></table>
|
|
@@ -97,6 +100,54 @@ function AlbumList(props) {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function Controls(props) {
|
|
|
|
|
+ const playPause = (props.playStatus === Sound.status.PLAYING ? 'pause' : 'play');
|
|
|
|
|
+ const percent = (props.duration ? (100 * props.position / props.duration) : 0);
|
|
|
|
|
+ const elapsed = (props.position ? formatDuration(props.position/1000) : '');
|
|
|
|
|
+ const duration = (props.duration ? formatDuration(props.duration/1000) : '');
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className="Controls">
|
|
|
|
|
+ <div className="Controls-progress">
|
|
|
|
|
+ <div className="Controls-bar" style={{width:percent+'%'}} />
|
|
|
|
|
+ <span className="Controls-elapsed">{elapsed}</span>
|
|
|
|
|
+ <span className="Controls-duration">{duration}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="Controls-title">{props.title}</div>
|
|
|
|
|
+ <div className="Controls-buttons">
|
|
|
|
|
+ <button onClick={props.onPrev}>prev</button>
|
|
|
|
|
+ <button onClick={props.onPlayPause}>{playPause}</button>
|
|
|
|
|
+ <button onClick={props.onStop}>stop</button>
|
|
|
|
|
+ <button onClick={props.onNext}>next</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function Playlist(props) {
|
|
|
|
|
+ const {tracks} = props
|
|
|
|
|
+ if (!tracks) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!Array.isArray(tracks)) {
|
|
|
|
|
+ return <div className="Playlist-error">{tracks}</div>;
|
|
|
|
|
+ }
|
|
|
|
|
+ return (
|
|
|
|
|
+ <table className="Playlist"><tbody>
|
|
|
|
|
+ {tracks.map((track, i) => (
|
|
|
|
|
+ <tr key={i} className={ i === props.activeTrack ? "Playlist-active" : "" }>
|
|
|
|
|
+ <td>{track.no}</td>
|
|
|
|
|
+ <td>{track.title}</td>
|
|
|
|
|
+ <td>{track.artist}</td>
|
|
|
|
|
+ <td>{track.year}</td>
|
|
|
|
|
+ <td>{track.album}</td>
|
|
|
|
|
+ <td>{track.bit_rate} kbps</td>
|
|
|
|
|
+ <td>{formatDuration(track.duration)}</td>
|
|
|
|
|
+ <td><button onClick={() => props.onActivateTrack(i)}>play</button></td>
|
|
|
|
|
+ </tr>))}
|
|
|
|
|
+ </tbody></table>
|
|
|
|
|
+ )
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
class Player extends Component {
|
|
class Player extends Component {
|
|
|
stateLoading = {
|
|
stateLoading = {
|
|
|
error: null,
|
|
error: null,
|
|
@@ -112,11 +163,16 @@ class Player extends Component {
|
|
|
{type: 'country', title: 'Countries'},
|
|
{type: 'country', title: 'Countries'},
|
|
|
{type: 'type', title: 'Album types'},
|
|
{type: 'type', title: 'Album types'},
|
|
|
{type: 'status', title: 'Album statuses'}];
|
|
{type: 'status', title: 'Album statuses'}];
|
|
|
- this.state = {};
|
|
|
|
|
|
|
+ this.state = {
|
|
|
|
|
+ album: Object.assign({}, this.stateLoading),
|
|
|
|
|
+ sound: {url: '', playStatus: Sound.status.STOPPED},
|
|
|
|
|
+ tracks: null,
|
|
|
|
|
+ activeTrack: -1,
|
|
|
|
|
+ activeAlbum: null
|
|
|
|
|
+ };
|
|
|
for (var {type} of this.categories) {
|
|
for (var {type} of this.categories) {
|
|
|
this.state[type] = Object.assign({}, this.stateLoading);
|
|
this.state[type] = Object.assign({}, this.stateLoading);
|
|
|
}
|
|
}
|
|
|
- this.state.album = Object.assign({}, this.stateLoading);
|
|
|
|
|
this.filterTimeout = null;
|
|
this.filterTimeout = null;
|
|
|
}
|
|
}
|
|
|
fetchCategory(cat) {
|
|
fetchCategory(cat) {
|
|
@@ -160,13 +216,36 @@ class Player extends Component {
|
|
|
this.setState({restrictions: {}});
|
|
this.setState({restrictions: {}});
|
|
|
}
|
|
}
|
|
|
componentDidUpdate(prevProps, prevState) {
|
|
componentDidUpdate(prevProps, prevState) {
|
|
|
- console.log('stateChange');
|
|
|
|
|
if (this.state.restrictions !== prevState.restrictions) {
|
|
if (this.state.restrictions !== prevState.restrictions) {
|
|
|
// Reload category filters on restriction changes
|
|
// Reload category filters on restriction changes
|
|
|
this.fetchAll();
|
|
this.fetchAll();
|
|
|
} else if (this.state.filter !== prevState.filter) {
|
|
} else if (this.state.filter !== prevState.filter) {
|
|
|
this.fetchCategory('album');
|
|
this.fetchCategory('album');
|
|
|
}
|
|
}
|
|
|
|
|
+ if (this.state.activeAlbum !== prevState.activeAlbum) {
|
|
|
|
|
+ this.fetchTracks(this.state.activeAlbum);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.state.activeTrack !== prevState.activeTrack) {
|
|
|
|
|
+ if (this.state.activeTrack !== -1 && Array.isArray(this.state.tracks)) {
|
|
|
|
|
+ const track = this.state.tracks[this.state.activeTrack];
|
|
|
|
|
+ if (!track) {
|
|
|
|
|
+ console.log('Bad activeTrack', this.state.tracks, this.state.activeTrack);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.setState({sound: {
|
|
|
|
|
+ url: track.url,
|
|
|
|
|
+ position: 0,
|
|
|
|
|
+ playStatus: Sound.status.PLAYING
|
|
|
|
|
+ }})
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ fetchTracks(album) {
|
|
|
|
|
+ fetch(`/album/${album.id}/tracks`)
|
|
|
|
|
+ .then(res => (res.ok ? res.json() : Promise.reject({message:res.statusText})))
|
|
|
|
|
+ .then(result => this.setState({tracks: result, activeTrack: 0}))
|
|
|
|
|
+ .catch(error => this.setState({tracks: error.message}));
|
|
|
|
|
+ this.setState({tracks: "Loading", activeTrack: -1})
|
|
|
}
|
|
}
|
|
|
handleCategoryChange(type, value) {
|
|
handleCategoryChange(type, value) {
|
|
|
console.log(type, value);
|
|
console.log(type, value);
|
|
@@ -185,6 +264,62 @@ class Player extends Component {
|
|
|
const value = e.target.value;
|
|
const value = e.target.value;
|
|
|
this.filterTimeout = setTimeout(() => this.setState({filter:value}), 500);
|
|
this.filterTimeout = setTimeout(() => this.setState({filter:value}), 500);
|
|
|
}
|
|
}
|
|
|
|
|
+ handleControlPrev = () => {
|
|
|
|
|
+ if (!Array.isArray(this.state.tracks)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var activeTrack = this.state.activeTrack - 1
|
|
|
|
|
+ if (activeTrack < 0) {
|
|
|
|
|
+ activeTrack = -1;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.setState({activeTrack: activeTrack})
|
|
|
|
|
+ }
|
|
|
|
|
+ handleControlPlayPause = () => {
|
|
|
|
|
+ const playStatus = (this.state.sound.playStatus === Sound.status.PLAYING ? Sound.status.PAUSED : Sound.status.PLAYING);
|
|
|
|
|
+ this.setState({sound: Object.assign({}, this.state.sound, {playStatus: playStatus})});
|
|
|
|
|
+ }
|
|
|
|
|
+ handleControlStop = () => {
|
|
|
|
|
+ this.setState({
|
|
|
|
|
+ sound: Object.assign({}, this.state.sound, {
|
|
|
|
|
+ playStatus: Sound.status.STOPPED,
|
|
|
|
|
+ position: 0}),
|
|
|
|
|
+ activeTrack: -1});
|
|
|
|
|
+ }
|
|
|
|
|
+ handleControlNext = () => {
|
|
|
|
|
+ if (!Array.isArray(this.state.tracks)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var activeTrack = this.state.activeTrack + 1
|
|
|
|
|
+ if (activeTrack >= this.state.tracks.length) {
|
|
|
|
|
+ activeTrack = -1;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.setState({activeTrack: activeTrack})
|
|
|
|
|
+ }
|
|
|
|
|
+ handleSoundError = (errorCode, description) => {
|
|
|
|
|
+ console.log('sound error', errorCode, description)
|
|
|
|
|
+ // try next track
|
|
|
|
|
+ this.handleControlNext();
|
|
|
|
|
+ }
|
|
|
|
|
+ handleSoundLoading = (sound) => {
|
|
|
|
|
+ // TODOconsole.log('sound loading', sound)
|
|
|
|
|
+ }
|
|
|
|
|
+ handleSoundPlaying = (sound) => {
|
|
|
|
|
+ this.setState({sound: Object.assign({}, this.state.sound, {
|
|
|
|
|
+ position: sound.position,
|
|
|
|
|
+ duration: sound.duration
|
|
|
|
|
+ })})
|
|
|
|
|
+ }
|
|
|
|
|
+ handleSoundFinished = () => {
|
|
|
|
|
+ console.log('sound finished');
|
|
|
|
|
+ this.setState({sound: Object.assign({}, this.state.sound, {playStatus: Sound.status.STOPPED})});
|
|
|
|
|
+ this.handleControlNext();
|
|
|
|
|
+ }
|
|
|
|
|
+ handleActivateTrack = (activeTrack) => {
|
|
|
|
|
+ this.setState({activeTrack: activeTrack})
|
|
|
|
|
+ }
|
|
|
|
|
+ handlePlayAlbum = (album) => {
|
|
|
|
|
+ this.setState({activeAlbum: album})
|
|
|
|
|
+ }
|
|
|
render() {
|
|
render() {
|
|
|
return (
|
|
return (
|
|
|
<div className="Player">
|
|
<div className="Player">
|
|
@@ -194,7 +329,21 @@ class Player extends Component {
|
|
|
))}
|
|
))}
|
|
|
</div><div style={{clear:'both', paddingBottom:'20px'}}/>
|
|
</div><div style={{clear:'both', paddingBottom:'20px'}}/>
|
|
|
<input type="text" placeholder="Search albums" onChange={this.handleFilterChange} size={150} />
|
|
<input type="text" placeholder="Search albums" onChange={this.handleFilterChange} size={150} />
|
|
|
- <AlbumList title="Albums" state={this.state.album} />
|
|
|
|
|
|
|
+ <Controls {...this.state.sound}
|
|
|
|
|
+ onPrev={this.handleControlPrev}
|
|
|
|
|
+ onPlayPause={this.handleControlPlayPause}
|
|
|
|
|
+ onStop={this.handleControlStop}
|
|
|
|
|
+ onNext={this.handleControlNext} />
|
|
|
|
|
+ <Playlist
|
|
|
|
|
+ tracks={this.state.tracks}
|
|
|
|
|
+ activeTrack={this.state.activeTrack}
|
|
|
|
|
+ onActivateTrack={this.handleActivateTrack} />
|
|
|
|
|
+ <AlbumList title="Albums" state={this.state.album} onPlayAlbum={this.handlePlayAlbum} />
|
|
|
|
|
+ <Sound {...this.state.sound}
|
|
|
|
|
+ onError={this.handleSoundError}
|
|
|
|
|
+ onLoading={this.handleSoundLoading}
|
|
|
|
|
+ onPlaying={this.handleSoundPlaying}
|
|
|
|
|
+ onFinishedPlaying={this.handleSoundFinished} />
|
|
|
</div>
|
|
</div>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|