Explorar el Código

[front] Not-virtualized AlbumList with slick styles

Innocenty Enikeew hace 7 años
padre
commit
7ea89648d4
Se han modificado 7 ficheros con 107 adiciones y 227 borrados
  1. 8 0
      front/package-lock.json
  2. 1 0
      front/package.json
  3. 18 17
      front/src/AlbumList.css
  4. 48 183
      front/src/AlbumList.js
  5. 2 1
      front/src/MediaSession.js
  6. 28 24
      front/src/Player.js
  7. 2 2
      front/src/utils.js

+ 8 - 0
front/package-lock.json

@@ -9143,6 +9143,14 @@
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.0.tgz",
       "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-4.0.0.tgz",
       "integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw=="
       "integrity": "sha512-FlsPxavEyMuR6TjVbSSywovXSEyOg6ZDj5+Z8nbsRl9EkOzAhEIcS+GLoQDC5fz/t9suhUXWmUrOBrgeUvrMxw=="
     },
     },
+    "react-infinite-scroller": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/react-infinite-scroller/-/react-infinite-scroller-1.1.3.tgz",
+      "integrity": "sha512-fBnsmZ05y/5Jp0CiLCoUI9/WChK1CHET6w2hf/bPFlfP49wgBZBwhqMNy8Y+eC2yp4Gh8uGTh/m955ohuT4dxA==",
+      "requires": {
+        "prop-types": "15.6.1"
+      }
+    },
     "react-input-autosize": {
     "react-input-autosize": {
       "version": "2.2.1",
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",
       "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz",

+ 1 - 0
front/package.json

@@ -5,6 +5,7 @@
   "dependencies": {
   "dependencies": {
     "react": "^16.2.0",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-dom": "^16.2.0",
+    "react-infinite-scroller": "^1.1.3",
     "react-scripts": "1.1.1",
     "react-scripts": "1.1.1",
     "react-select": "git://github.com/enikesha/react-select.git#feature/async-pagination",
     "react-select": "git://github.com/enikesha/react-select.git#feature/async-pagination",
     "react-sound": "^1.1.0",
     "react-sound": "^1.1.0",

+ 18 - 17
front/src/AlbumList.css

@@ -1,23 +1,24 @@
-.AlbumList {
-  clear: both;
-  padding-top: 15px;
+.AlbumList { padding-top: 15px; }
+.AlbumList h3 { margin: 10px 0; }
+.AlbumList h4 { margin: 8px 0; }
+.AlbumList .album { padding-bottom: 8px; }
+.AlbumList .album > div {
+  vertical-align: middle;
+  display: inline-block;
+  padding-right: 8px;
 }
 }
-.AlbumList h3 { margin: 10px 0}
-.AlbumList h4 { margin: 8px 0}
-.AlbumList .album { height: 80px; }
+.AlbumList .album > div:nth-child(1) { height: 64px; }
+.AlbumList .album > div:nth-child(2) { width: 72px; }
+.AlbumList .album > div:nth-child(2) > span:nth-child(2) { font-size: small; }
+.AlbumList .album > div:nth-child(2) > span:nth-child(3) { font-size: small; }
+
+.AlbumList .album > div:nth-child(3) > span:nth-child(2) { font-size: small; }
+.AlbumList .album > div:nth-child(3) > span:nth-child(3) { font-size: small; }
 
 
+.AlbumList .album span {
+  display: block;
+}
 .AlbumList img {
 .AlbumList img {
   width: 64px;
   width: 64px;
   height: 64px;
   height: 64px;
-  vertical-align: middle;
-}
-.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; }

+ 48 - 183
front/src/AlbumList.js

@@ -1,205 +1,70 @@
 import React from 'react';
 import React from 'react';
-import AutoSizer from 'react-virtualized/dist/commonjs/AutoSizer'
-import List from 'react-virtualized/dist/commonjs/List'
-import InfiniteLoader from 'react-virtualized/dist/commonjs/InfiniteLoader';
+import InfiniteScroll from 'react-infinite-scroller';
 
 
 import {formatDuration} from './utils.js';
 import {formatDuration} from './utils.js';
 import logo from './logo.svg';
 import logo from './logo.svg';
-import styles from './AlbumList.css';
+import './AlbumList.css';
 
 
-function mapAlbums(items) {
-  var artists = [], curArtist = {}, curType = {};
-  for (const album of items) {
-    if (album.artist !== curArtist.name) {
-      curArtist = {name: album.artist, types: []}
-      curType = {}
-      artists.push(curArtist)
-    }
-    if (album.type !== curType.type) {
-      curType = {type: album.type, albums: []}
-      curArtist.types.push(curType)
-    }
-    curType.albums.push(album);
+function _albumProps(albums, index) {
+  if (index === 0) {
+    return {showArtist: true, showType: true}
   }
   }
-  return artists;
-}
 
 
-const STATUS_LOADING = 1;
-
-export default class AlbumList extends React.PureComponent {
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      loadedRowsMap: {},
-    };
-    this._isRowLoaded = this._isRowLoaded.bind(this);
-    this._loadMoreRows = this._loadMoreRows.bind(this);
-    this._rowRenderer = this._rowRenderer.bind(this);
-    this._rowHeight = this._rowHeight.bind(this);
+  if (albums[index-1].artist !== albums[index].artist) {
+    return {showArtist: true, showType: true}
   }
   }
 
 
-  render() {
-    const {rowCount} = this.props;
-
-    return (
-        <div className="AlbumList">
-      <h2>{this.props.title}</h2>
-
-        <InfiniteLoader
-          isRowLoaded={this._isRowLoaded}
-      loadMoreRows={this._loadMoreRows}
-      minimumBatchSize={15}
-          rowCount={rowCount}>
-        {({onRowsRendered, registerChild}) => (
-           <AutoSizer disableHeight>
-            {({width}) => (
-              <List
-                  ref={registerChild}
-              height={1000}
-            width={width}
-                  onRowsRendered={onRowsRendered}
-                  rowCount={rowCount}
-                  rowHeight={this._rowHeight}
-                  rowRenderer={this._rowRenderer}
-                />
-            )}
-          </AutoSizer>
-        )}
-      </InfiniteLoader>
-        </div>
-    )
-  }
-
-  _isRowLoaded({index}) {
-    const {loadedRowsMap} = this.state;
-    return !!loadedRowsMap[index]; // STATUS_LOADING or STATUS_LOADED
-  }
-
-  _loadMoreRows({startIndex, stopIndex}) {
-    console.log('_loadMoreRows', startIndex, stopIndex);
-    const {loadedRowsMap} = this.state;
-
-    for (var i = startIndex; i <= stopIndex; i++) {
-      loadedRowsMap[i] = STATUS_LOADING;
-    }
-
-    return this.props.loadMoreRows({offset: startIndex, limit: (stopIndex-startIndex+1)})
-      .then(rows => {
-        for (var i = startIndex; i <= stopIndex; i++) {
-          loadedRowsMap[i] = rows[i-startIndex];
-        }
-      })
-  }
-
-  _albumHeadings(index) {
-    let rows = this.state.loadedRowsMap;
-    if (index === 0 || rows[index-1] === STATUS_LOADING) {
-      return {showArtist: true, showType: true}
-    }
-
-    if (rows[index-1].artist !== rows[index].artist) {
-      return {showArtist: true, showType: true}
-    }
-
-    let showType = (rows[index-1].type !== rows[index].type);
-    return {showType};
-  }
+  let showType = (albums[index-1].type !== albums[index].type);
+  return {showType};
+}
 
 
-  _rowHeight({index}) {
-    const {loadedRowsMap} = this.state;
-    if (!loadedRowsMap[index]) {
-      return 0;
-    }
-    if (loadedRowsMap[index] === STATUS_LOADING) {
-      return 80;
+function Album({album, showArtist, showType, onPlayAlbum}) {
+  function onClick(album) {
+    return (e) => {
+      e.preventDefault()
+      onPlayAlbum(album)
     }
     }
-    let {showArtist, showType} = this._albumHeadings(index);
-    return 80 + (showArtist ? 42 : 0) + (showType ? 34 : 0);
   }
   }
-
-  _rowRenderer({index, key, style}) {
-    const {loadedRowsMap} = this.state;
-    let album = loadedRowsMap[index];
-    if (!album) {
-      return null;
-    }
-
-    let content;
-    if (album === STATUS_LOADING) {
-      content = (
-        <div className={styles.placeholder} />
-      );
-    } else {
-      let {showArtist, showType} = this._albumHeadings(index)
-      content = (
-          <div>
-          {showArtist && (<h3>{album.artist}</h3>)}
-        {showType && (<h4>{album.type}</h4>)}
-        <div className="album">
-          <img src={album.cover || logo} alt={album.album} />
+  return (
+    <div>
+      {showArtist && (<h3>{album.artist}</h3>)}
+      {showType && (<h4>{album.type}</h4>)}
+      <div className="album">
+        <div><img src={album.cover || logo} alt={album.album} /></div>
+        <div>
           <span>{album.year}</span>
           <span>{album.year}</span>
-          <span>{album.album}</span>
-          <span>{album.publisher}</span>
           <span>{album.country}</span>
           <span>{album.country}</span>
+          <span>{album.track_count}&nbsp;@&nbsp;{formatDuration(album.total_duration)}</span>
+        </div>
+        <div>
+          <span><a href={`/album/${album.id}`} onClick={onClick(album)}>{album.album}</a></span>
+          <span>{album.publisher}</span>
           <span>{album.genre}</span>
           <span>{album.genre}</span>
-          <span>{album.track_count}</span>
-          <span>{formatDuration(album.total_duration)}</span>
-          <span><button onClick={() => this.props.onPlayAlbum(album)}>play</button></span>
-          </div>
-          </div>
-      )
-    }
-
-    return (
-      <div className={styles.row} key={key} style={style}>
-        {content}
+        </div>
       </div>
       </div>
-    );
-  }
+    </div>
+  )
 }
 }
 
 
-function AlbumListOld(props) {
-  const { error, isLoaded, items } = props.state;
-
-  var content;
-  if (error) {
-    content = <div>Error: {error.message}</div>
-  } else if (!isLoaded) {
-    content = <div>Loading</div>
-  } else {
-    const artists = mapAlbums(items);
-
-    content = artists.map(artist => (
-      <div key={artist.name}>
-        <h3>{artist.name}</h3>
-        {artist.types.map(type => (
-          <div key={type.type}>
-            <h4>{type.type}</h4>
-            <table><tbody>
-            {type.albums.map(album => (
-              <tr key={album.id}>
-                <td><img src={album.cover || logo} alt={album.album} /></td>
-                <td>{album.year}</td>
-                <td>{album.album}</td>
-                <td>{album.publisher}</td>
-                <td>{album.country}</td>
-                <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>
-          </div>
-        ))}
-      </div>
-    ))
-  }
+export default function AlbumList({loadMore, hasMore, albums, title, error, ...props}) {
   return (
   return (
     <div className="AlbumList">
     <div className="AlbumList">
-      <h2>{props.title}</h2>
-      {content}
+      <h2>{title}</h2>
+      { error ? (
+          <div className="error">{error}></div>
+        ) : (
+          <InfiniteScroll
+            loadMore={loadMore}
+            hasMore={hasMore}
+            loader={<div className="loader" key={0}>Loading ...</div>}
+            initialLoad={false}
+          >
+            {albums.map((album, idx) => (
+              <Album key={idx} album={album} {..._albumProps(albums, idx)} {...props} />
+            ))}
+         </InfiniteScroll>
+        )
+      }
     </div>
     </div>
-  );
+  )
 }
 }

+ 2 - 1
front/src/MediaSession.js

@@ -6,7 +6,8 @@ export default class MediaSession extends React.Component {
       return;
       return;
     }
     }
     const {title, artist, album, cover} = this.props;
     const {title, artist, album, cover} = this.props;
-    navigator.mediaSession.metadata = new window.MediaMetadata({title, artist, album, artwork: [{src:cover}]});
+    const artwork = cover ? [{src: cover}] : undefined;
+    navigator.mediaSession.metadata = new window.MediaMetadata({title, artist, album, artwork});
   }
   }
   updateEvents() {
   updateEvents() {
     if (!('mediaSession' in navigator)) {
     if (!('mediaSession' in navigator)) {

+ 28 - 24
front/src/Player.js

@@ -20,8 +20,7 @@ export default class Player extends Component {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
     this.state = {
     this.state = {
-      albums:[],
-      albums_count: 0,
+      albumList: {key:'', albums: [], error: false, hasMore: false},
       filters: {},
       filters: {},
       filter: '',
       filter: '',
       sound: {url: '', playStatus: Sound.status.STOPPED},
       sound: {url: '', playStatus: Sound.status.STOPPED},
@@ -32,36 +31,43 @@ export default class Player extends Component {
     };
     };
     this.filterTimeout = null;
     this.filterTimeout = null;
   }
   }
-  loadAlbums = ({size, offset, limit}) => {
+  genAlbumListKey() {
     const {filter, filters} = this.state
     const {filter, filters} = this.state
-    const params = {filter};
+    return `${filter}-${this.selects.map(({type}) => filters[type] ? filters[type].item : '').join('-')}`
+  }
+  loadAlbums = (page) => {
+    const {filter, filters} = this.state
+    const limit  = 15;
+    const offset = page * limit;
+    const params = {filter, offset, limit:(limit+1)};
+    let key = (page === 0 ? this.genAlbumListKey() : this.state.albumList.key)
     for (const {type} of this.selects) {
     for (const {type} of this.selects) {
       if (filters[type]) {
       if (filters[type]) {
         params[type] = filters[type].item;
         params[type] = filters[type].item;
       }
       }
     }
     }
-    if (offset) {
-      params.offset = offset;
-    }
-    if (limit) {
-      params.limit = limit;
-    }
-    return fetch(`/cat/album${size ? '/size' : ''}?${getQueryString(params)}`, fetchOpts)
+    return fetch(`/cat/album?${getQueryString(params)}`, fetchOpts)
       .then(res => (res.ok ? res.json() : Promise.reject({message:res.statusText})))
       .then(res => (res.ok ? res.json() : Promise.reject({message:res.statusText})))
       .then(data => {
       .then(data => {
-        this.setState({[size ? 'albums_count' : 'albums']: data})
-        return data;
+        const hasMore = (data.length > limit)
+        const albums = page === 0 ? data.slice(0, limit) : this.state.albumList.albums.concat(data.slice(0, limit))
+        const albumList = {key, albums, hasMore}
+        this.setState({albumList})
+        return albums;
+      })
+      .catch(error => {
+        console.log(error)
+        const albumList = {key, error}
+        this.setState({albumList})
       })
       })
-      .catch(error => console.log(error))
   }
   }
   componentDidMount() {
   componentDidMount() {
-    this.loadAlbums({size:true});
+    this.loadAlbums(0)
   }
   }
   componentDidUpdate(prevProps, prevState) {
   componentDidUpdate(prevProps, prevState) {
     const {filters} = this.state;
     const {filters} = this.state;
     if (filters !== prevState.filters) {
     if (filters !== prevState.filters) {
-      // TODO: reset
-      this.loadAlbums({size:true});
+      this.loadAlbums(0);
     }
     }
     if (this.state.activeAlbum !== prevState.activeAlbum) {
     if (this.state.activeAlbum !== prevState.activeAlbum) {
       this.fetchTracks(this.state.activeAlbum);
       this.fetchTracks(this.state.activeAlbum);
@@ -102,8 +108,7 @@ export default class Player extends Component {
     }
     }
     const filter = e.target.value;
     const filter = e.target.value;
     this.filterTimeout = setTimeout(() => {
     this.filterTimeout = setTimeout(() => {
-      // TODO: reset
-      this.loadAlbums({size:true})
+      this.loadAlbums(0);
     }, 500);
     }, 500);
     this.setState({filter})
     this.setState({filter})
   }
   }
@@ -256,11 +261,10 @@ export default class Player extends Component {
           activeTrack={this.state.activeTrack}
           activeTrack={this.state.activeTrack}
           onActivateTrack={this.handleActivateTrack} />
           onActivateTrack={this.handleActivateTrack} />
         <AlbumList
         <AlbumList
-      title="Albums"
-      rowCount={this.state.albums_count}
-      items={this.state.albums}
-      loadMoreRows={this.loadAlbums}
-      onPlayAlbum={this.handlePlayAlbum} />
+          title="Albums"
+          {...this.state.albumList}
+          loadMore={this.loadAlbums}
+          onPlayAlbum={this.handlePlayAlbum} />
         <Sound {...this.state.sound}
         <Sound {...this.state.sound}
           onError={this.handleSoundError}
           onError={this.handleSoundError}
           onLoading={this.handleSoundLoading}
           onLoading={this.handleSoundLoading}

+ 2 - 2
front/src/utils.js

@@ -1,11 +1,11 @@
-export function formatDuration(duration) {
+export function formatDuration(duration, text = false) {
   duration = Math.floor(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) {
     seconds = '0' + seconds;
     seconds = '0' + seconds;
   }
   }
-  return minutes + ':' + seconds;
+  return `${minutes}${text?'m':''}:${seconds}${text?'s':''}`;
 }
 }
 
 
 export function getQueryString(params) {
 export function getQueryString(params) {