1
0
Просмотр исходного кода

[front] Player with controls and playlist

Innocenty Enikeew 7 лет назад
Родитель
Сommit
b2648c82d6
5 измененных файлов с 215 добавлено и 8 удалено
  1. 1 0
      back/server.lisp
  2. 14 0
      front/package-lock.json
  3. 1 0
      front/package.json
  4. 45 3
      front/src/App.css
  5. 154 5
      front/src/App.js

+ 1 - 0
back/server.lisp

@@ -45,6 +45,7 @@
   (with-object
     (maybe-key-value "artist" (track-artist track))
     (maybe-key-value "album" (album-album (track-album track)))
+    (maybe-key-value "year" (album-year (track-album track)))
     (maybe-key-value "no" (clear-track-no (track-no track)))
     (maybe-key-value "title" (track-title track))
     (maybe-key-value "bit_rate" (track-bit-rate track))

+ 14 - 0
front/package-lock.json

@@ -9198,6 +9198,15 @@
         }
       }
     },
+    "react-sound": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/react-sound/-/react-sound-1.1.0.tgz",
+      "integrity": "sha512-ZySUXb4SxLrqnF5O3DUa/MOcfXUhCQXRDe+z+sjEE7LzeezMDog1mxuXbq49POOz2Lbjie4lCr14fVsPyTrUmQ==",
+      "requires": {
+        "prop-types": "15.6.1",
+        "soundmanager2": "2.97.20170602"
+      }
+    },
     "react-virtualized": {
       "version": "9.18.5",
       "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.18.5.tgz",
@@ -10046,6 +10055,11 @@
         "is-plain-obj": "1.1.0"
       }
     },
+    "soundmanager2": {
+      "version": "2.97.20170602",
+      "resolved": "https://registry.npmjs.org/soundmanager2/-/soundmanager2-2.97.20170602.tgz",
+      "integrity": "sha512-2ee7ES9SJ++WkD7PGHMeT4QUuJr7uC3wacD6RoCDlKjdSp9lpEOaKm3lKWKld119DLILjS2l9U6xpXJN6U0KPQ=="
+    },
     "source-list-map": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.0.tgz",

+ 1 - 0
front/package.json

@@ -6,6 +6,7 @@
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-scripts": "1.1.1",
+    "react-sound": "^1.1.0",
     "react-virtualized": "^9.18.5"
   },
   "proxy": "http://localhost:5000/",

+ 45 - 3
front/src/App.css

@@ -45,9 +45,51 @@
   padding-top: 15px;
 }
 .AlbumList img {
-  width: 128px;
-  height: 128px;
+  width: 64px;
+  height: 64px;
 }
 .AlbumList table td {
   width: 200px;
-}
+}
+.AlbumList table td:nth-child(1) { width: 75px; }
+.AlbumList table td:nth-child(2) { width: 64px; }
+.AlbumList table td:nth-child(3) { width: 256px; }
+.AlbumList table td:nth-child(5) { width: 64px; }
+.AlbumList table td:nth-child(7) { width: 64px; }
+.AlbumList table td:nth-child(8) { width: 64px; }
+.AlbumList table td:nth-child(9) { width: 64px; }
+
+.Controls {
+  margin: 10px 0;
+}
+.Controls-progress {
+  width: 512px;
+  border: 1px black solid;
+  height: 16px;
+  position: relative;
+  margin-bottom: 8px;
+}
+.Controls-progress span {
+  font-size: 14px;
+}
+.Controls-bar {
+  background: cadetblue;
+  height: 16px;
+}
+.Controls-elapsed {
+  position: absolute;
+  left: 2px;
+  top: 0;
+}
+.Controls-duration {
+  position: absolute;
+  right: 2px;
+  top: 0;
+}
+.Controls button {
+  margin-right: 8px;
+}
+
+.Playlist-active {
+  background: darkseagreen;
+}

+ 154 - 5
front/src/App.js

@@ -1,4 +1,5 @@
 import React, { Component } from 'react';
+import Sound from 'react-sound';
 import logo from './logo.svg';
 import './App.css';
 
@@ -28,6 +29,7 @@ function CategoryFlatList(props) {
 }
 
 function formatDuration(duration) {
+  duration = Math.floor(duration);
   var seconds = duration % 60;
   const minutes = Math.floor(duration / 60);
   if (seconds < 10) {
@@ -73,7 +75,7 @@ function AlbumList(props) {
             <table><tbody>
             {type.albums.map(album => (
               <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.album}</td>
                 <td>{album.publisher}</td>
@@ -81,6 +83,7 @@ function AlbumList(props) {
                 <td>{album.genre}</td>
                 <td>{album.track_count}</td>
                 <td>{formatDuration(album.total_duration)}</td>
+                <td><button onClick={() => props.onPlayAlbum(album)}>play</button></td>
               </tr>
             ))}
             </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 {
   stateLoading = {
     error: null,
@@ -112,11 +163,16 @@ class Player extends Component {
                        {type: 'country', title: 'Countries'},
                        {type: 'type', title: 'Album types'},
                        {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) {
       this.state[type] = Object.assign({}, this.stateLoading);
     }
-    this.state.album = Object.assign({}, this.stateLoading);
     this.filterTimeout = null;
   }
   fetchCategory(cat) {
@@ -160,13 +216,36 @@ class Player extends Component {
     this.setState({restrictions: {}});
   }
   componentDidUpdate(prevProps, prevState) {
-    console.log('stateChange');
     if (this.state.restrictions !== prevState.restrictions) {
       // Reload category filters on restriction changes
       this.fetchAll();
     } else if (this.state.filter !== prevState.filter) {
       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) {
     console.log(type, value);
@@ -185,6 +264,62 @@ class Player extends Component {
     const value = e.target.value;
     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() {
     return (
       <div className="Player">
@@ -194,7 +329,21 @@ class Player extends Component {
         ))}
         </div><div style={{clear:'both', paddingBottom:'20px'}}/>
         <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>
     );
   }