From 02fcb2465303e7a1b5ab9e0d42d1f3602f8d5280 Mon Sep 17 00:00:00 2001 From: Anh Trinh Date: Sat, 4 Jul 2015 17:46:35 +1200 Subject: [PATCH] display track length in search result --- app/jsx/trackList.js | 292 +++++++++++++----------- app/scripts/filters/prettyTimeFilter.js | 4 +- app/scripts/services/SearchService.js | 5 +- app/scripts/services/TrackAdapter.js | 32 ++- app/scripts/trackList.js | 292 +++++++++++++----------- app/styles/main.css | 18 +- 6 files changed, 355 insertions(+), 288 deletions(-) diff --git a/app/jsx/trackList.js b/app/jsx/trackList.js index 2f53765..0374f25 100644 --- a/app/jsx/trackList.js +++ b/app/jsx/trackList.js @@ -1,147 +1,165 @@ /** @jsx React.DOM */ window.TrackItem = React.createClass({ - onClick: function() { - if (this.props.onTrackClick) - this.props.onTrackClick(this.props.track, this.props.trackNumber - 1); - }, - onPlayNext: function() { - if (this.props.onPlayNext) - this.props.onPlayNext(this.props.track); - }, - onAddTrackToPlaylist: function() { - if (this.props.onAddTrackToPlaylist) - this.props.onAddTrackToPlaylist(this.props.track); - }, - onRemoveTrack: function() { - if (this.props.onRemoveTrack) - this.props.onRemoveTrack(this.props.track); - }, - onStarTrack: function() { - if (this.props.onStarTrack) - this.props.onStarTrack(this.props.track); - }, - render: function() { - var track = this.props.track; - var context = this.props.context || 'nowplaying'; - - if (!track) return; - - var fontIcon = { - sc: 'brand-icon icon-soundcloud', + onClick: function() { + if (this.props.onTrackClick) + this.props.onTrackClick(this.props.track, this.props.trackNumber - 1); + }, + onPlayNext: function() { + if (this.props.onPlayNext) + this.props.onPlayNext(this.props.track); + }, + onAddTrackToPlaylist: function() { + if (this.props.onAddTrackToPlaylist) + this.props.onAddTrackToPlaylist(this.props.track); + }, + onRemoveTrack: function() { + if (this.props.onRemoveTrack) + this.props.onRemoveTrack(this.props.track); + }, + onStarTrack: function() { + if (this.props.onStarTrack) + this.props.onStarTrack(this.props.track); + }, + seconds2time: function (value) { + if (!value) return 'N/A'; + var hours = Math.floor(value / 3600), + mins = '0' + Math.floor((value % 3600) / 60), + secs = '0' + Math.floor((value % 60)); + mins = mins.substr(mins.length - 2); + secs = secs.substr(secs.length - 2); + if(!isNaN(secs)){ + if (hours){ + return hours+':'+mins+':'+secs; + } else { + return mins+':'+secs; + }; + } else { + return '00:00'; + }; + }, + render: function() { + var track = this.props.track; + var context = this.props.context || 'nowplaying'; + + if (!track) return; + + var fontIcon = { + sc: 'brand-icon icon-soundcloud', yt: 'brand-icon icon ion-social-youtube-outline' - }; + }; - var removeButton, playNextButton, unstarButton, starButton; - - if (context === 'nowplaying' || context === 'playlist') { - removeButton = ( + var removeButton, playNextButton, unstarButton, starButton; + + if (context === 'nowplaying' || context === 'playlist') { + removeButton = ( - ); - } - - if (context !== 'nowplaying') { - playNextButton = ( - - ); - } - - if (track.starred) { - unstarButton = ( -
- -
- ) - } else { - starButton = ( - - ) - } - - var className = 'track-item'; - - if (track.error) { - className += ' error'; - } - - return ( -
  • -
    -
    - {track.title} -
    -
    -
    - {this.props.trackNumber} - - - -
    - {unstarButton} -
    -

    {track.title}

    -

    - - - - {parseInt(track.viewCount).toLocaleString()} - {parseInt(track.likeCount).toLocaleString()} -

    -
    -
    - {starButton} - {playNextButton} - - {removeButton} -
    -
  • - ); - } + ); + } + + if (context !== 'nowplaying') { + playNextButton = ( + + ); + } + + if (track.starred) { + unstarButton = ( +
    + +
    + ) + } else { + starButton = ( + + ) + } + + var className = 'track-item'; + + if (track.error) { + className += ' error'; + } + + return ( +
  • +
    +
    + {track.title} +
    +
    +
    + {this.props.trackNumber} + + + +
    + {unstarButton} +
    +

    {track.title}

    +

    + + + + {parseInt(track.viewCount).toLocaleString()} + {parseInt(track.likeCount).toLocaleString()} + {this.seconds2time(track.duration)} +

    +
    +
    + {starButton} + {playNextButton} + + {removeButton} +
    +
  • + ); + } }); window.TrackList = React.createClass({ - propTypes: { - tracks: React.PropTypes.array, - trackClick: React.PropTypes.string, - onTrackClick: React.PropTypes.func, - onPlayNext: React.PropTypes.func, - onAddTrackToPlaylist: React.PropTypes.func, - onStarTrack: React.PropTypes.func, - onRemoveTrack: React.PropTypes.func, - componentDidUpdate: React.PropTypes.func, - listContext: React.PropTypes.string - }, - render: function() { - var tracks = this.props.tracks; - var onTrackClick = this.props.onTrackClick; - var onAddTrackToPlaylist = this.props.onAddTrackToPlaylist; - var onRemoveTrack = this.props.onRemoveTrack; - var onStarTrack = this.props.onStarTrack; - var onPlayNext = this.props.onPlayNext; - var listContext = this.props.listContext; - - var trackNumber = 0; - var rows = _.map(tracks, function(track) { - - trackNumber ++; - - if (track) { - return ( - - ); - } - }); - - return ( - - ) - - }, - componentDidUpdate: function() { - if (this.props.componentDidUpdate) { - this.props.componentDidUpdate(); - } - } + propTypes: { + tracks: React.PropTypes.array, + trackClick: React.PropTypes.string, + onTrackClick: React.PropTypes.func, + onPlayNext: React.PropTypes.func, + onAddTrackToPlaylist: React.PropTypes.func, + onStarTrack: React.PropTypes.func, + onRemoveTrack: React.PropTypes.func, + componentDidUpdate: React.PropTypes.func, + listContext: React.PropTypes.string + }, + render: function() { + var tracks = this.props.tracks; + var onTrackClick = this.props.onTrackClick; + var onAddTrackToPlaylist = this.props.onAddTrackToPlaylist; + var onRemoveTrack = this.props.onRemoveTrack; + var onStarTrack = this.props.onStarTrack; + var onPlayNext = this.props.onPlayNext; + var listContext = this.props.listContext; + + var trackNumber = 0; + var rows = _.map(tracks, function(track) { + + trackNumber ++; + + if (track) { + return ( + + ); + } + }); + + return ( +
      + {rows} +
    + ) + + }, + componentDidUpdate: function() { + if (this.props.componentDidUpdate) { + this.props.componentDidUpdate(); + } + } }); \ No newline at end of file diff --git a/app/scripts/filters/prettyTimeFilter.js b/app/scripts/filters/prettyTimeFilter.js index f14401f..2b13c4c 100644 --- a/app/scripts/filters/prettyTimeFilter.js +++ b/app/scripts/filters/prettyTimeFilter.js @@ -12,9 +12,9 @@ secs = secs.substr(secs.length - 2); if(!isNaN(secs)){ if (hours){ - return hours+':'+mins+':'+secs; + return hours+':'+mins+':'+secs; } else { - return mins+':'+secs; + return mins+':'+secs; }; } else { return '00:00'; diff --git a/app/scripts/services/SearchService.js b/app/scripts/services/SearchService.js index 8afa81e..ad8122c 100644 --- a/app/scripts/services/SearchService.js +++ b/app/scripts/services/SearchService.js @@ -8,7 +8,7 @@ function SearchService($http, CLIENT_ID, YOUTUBE_KEY, TrackAdapter, $q){ - + return { search: search, searchYoutube: searchYoutube @@ -65,13 +65,14 @@ return item.id.videoId; }); - var parts = ['id', 'snippet', 'statistics', 'status']; + var parts = ['id', 'snippet', 'statistics', 'contentDetails', 'status']; var fields = [ 'items/id', 'items/snippet/title', 'items/snippet/thumbnails', 'items/statistics/viewCount', 'items/statistics/likeCount', + 'items/contentDetails/duration', 'items/status/embeddable' ]; diff --git a/app/scripts/services/TrackAdapter.js b/app/scripts/services/TrackAdapter.js index 9d36764..cc21ef7 100644 --- a/app/scripts/services/TrackAdapter.js +++ b/app/scripts/services/TrackAdapter.js @@ -5,7 +5,7 @@ .service("TrackAdapter", TrackAdapter); function TrackAdapter (StarService) { - + var DEFAULT_THUMBNAIL = 'images/artwork-default.jpg'; var ORIGIN_YOUTUBE = 'yt'; @@ -24,8 +24,8 @@ * Also this brings one more benefit: it will reduce the unneccessary information that we dont need, * therefore, we don't waste storage space. * - * Each track should have attached origin added by $http transform when retrieved by the application - * + * Each track should have attached origin added by $http transform when retrieved by the application + * */ function adapt(track, origin) { @@ -46,6 +46,7 @@ normalizedTrack.viewCount = track.statistics ? track.statistics.viewCount : 0; normalizedTrack.origin = ORIGIN_YOUTUBE; normalizedTrack.originalUrl = 'https://www.youtube.com/watch?v=' + track.id + '&source=soundcloudify'; + normalizedTrack.duration = parseDuration(track.contentDetails.duration); } else { normalizedTrack.id = track.id; @@ -58,6 +59,7 @@ normalizedTrack.viewCount = track.playback_count; normalizedTrack.origin = ORIGIN_SOUNDCLOUD; normalizedTrack.originalUrl = track.permalink_url + '?source=soundcloudify'; + normalizedTrack.duration = parseDuration(track.duration/1000); } normalizedTrack.uuid = window.ServiceHelpers.ID(); @@ -85,6 +87,30 @@ return track !== null && typeof track !== 'undefined'; }); } + + function parseDuration(duration) { + + var seconds = Number(duration); + + if (!isNaN(seconds)) { + return duration; + } + + if (duration.indexOf('PT') > -1) { + var chunks = duration.replace('PT','').replace('H', ':').replace('M', ':').replace('S', ''); + chunks = chunks.split(':'); + + if (chunks.length === 3) { + return (Number(chunks[0]) * 60 * 60) + (Number(chunks[1]) * 60) + Number(chunks[2]); + } else if (chunks.length === 2) { + return (Number(chunks[0]) * 60) + Number(chunks[1]); + } else if (chunks.length === 1) { + return Number(chunks[0]); + } + + return 0; + } + } }; }()); diff --git a/app/scripts/trackList.js b/app/scripts/trackList.js index d310539..342380e 100644 --- a/app/scripts/trackList.js +++ b/app/scripts/trackList.js @@ -1,147 +1,165 @@ /** @jsx React.DOM */ window.TrackItem = React.createClass({displayName: "TrackItem", - onClick: function() { - if (this.props.onTrackClick) - this.props.onTrackClick(this.props.track, this.props.trackNumber - 1); - }, - onPlayNext: function() { - if (this.props.onPlayNext) - this.props.onPlayNext(this.props.track); - }, - onAddTrackToPlaylist: function() { - if (this.props.onAddTrackToPlaylist) - this.props.onAddTrackToPlaylist(this.props.track); - }, - onRemoveTrack: function() { - if (this.props.onRemoveTrack) - this.props.onRemoveTrack(this.props.track); - }, - onStarTrack: function() { - if (this.props.onStarTrack) - this.props.onStarTrack(this.props.track); - }, - render: function() { - var track = this.props.track; - var context = this.props.context || 'nowplaying'; - - if (!track) return; - - var fontIcon = { - sc: 'brand-icon icon-soundcloud', + onClick: function() { + if (this.props.onTrackClick) + this.props.onTrackClick(this.props.track, this.props.trackNumber - 1); + }, + onPlayNext: function() { + if (this.props.onPlayNext) + this.props.onPlayNext(this.props.track); + }, + onAddTrackToPlaylist: function() { + if (this.props.onAddTrackToPlaylist) + this.props.onAddTrackToPlaylist(this.props.track); + }, + onRemoveTrack: function() { + if (this.props.onRemoveTrack) + this.props.onRemoveTrack(this.props.track); + }, + onStarTrack: function() { + if (this.props.onStarTrack) + this.props.onStarTrack(this.props.track); + }, + seconds2time: function (value) { + if (!value) return 'N/A'; + var hours = Math.floor(value / 3600), + mins = '0' + Math.floor((value % 3600) / 60), + secs = '0' + Math.floor((value % 60)); + mins = mins.substr(mins.length - 2); + secs = secs.substr(secs.length - 2); + if(!isNaN(secs)){ + if (hours){ + return hours+':'+mins+':'+secs; + } else { + return mins+':'+secs; + }; + } else { + return '00:00'; + }; + }, + render: function() { + var track = this.props.track; + var context = this.props.context || 'nowplaying'; + + if (!track) return; + + var fontIcon = { + sc: 'brand-icon icon-soundcloud', yt: 'brand-icon icon ion-social-youtube-outline' - }; + }; - var removeButton, playNextButton, unstarButton, starButton; - - if (context === 'nowplaying' || context === 'playlist') { - removeButton = ( + var removeButton, playNextButton, unstarButton, starButton; + + if (context === 'nowplaying' || context === 'playlist') { + removeButton = ( React.createElement("i", {className: "remove-btn icon ion-close", onClick: this.onRemoveTrack, title: "Remove"}) - ); - } - - if (context !== 'nowplaying') { - playNextButton = ( - React.createElement("i", {className: "add-to-playlist-btn icon ion-log-in", title: "Play Next", onClick: this.onPlayNext}) - ); - } - - if (track.starred) { - unstarButton = ( - React.createElement("div", {className: "starred"}, - React.createElement("i", {className: "remove-btn icon ion-star", title: "Remove Star", onClick: this.onStarTrack}) - ) - ) - } else { - starButton = ( - React.createElement("i", {className: "like-btn icon ion-star", title: "Star", onClick: this.onStarTrack}) - ) - } - - var className = 'track-item'; - - if (track.error) { - className += ' error'; - } - - return ( - React.createElement("li", {id: 'track-item-' + track.id, className: className}, - React.createElement("div", {className: "md-tile-left", onClick: this.onClick}, - React.createElement("div", {className: "face"}, - React.createElement("img", {src: track.artworkUrl, alt: track.title}) - ) - ), - React.createElement("div", {className: "play-actions", onClick: this.onClick}, - React.createElement("span", {className: "track-number"}, this.props.trackNumber), - React.createElement("i", {className: "dynamic-icon icon icon-playing"}), - React.createElement("i", {className: "dynamic-icon icon ion-ios-play"}), - React.createElement("i", {className: "dynamic-icon icon ion-ios-pause"}) - ), - unstarButton, - React.createElement("div", {className: "md-tile-content"}, - React.createElement("h3", {onClick: this.onClick}, track.title), - React.createElement("p", {className: "statistic"}, - React.createElement("a", {className: "original-link", href: track.originalUrl, title: "View Original"}, - React.createElement("i", {className: fontIcon[track.origin]}) - ), - React.createElement("i", {className: "icon ion-headphone"}), React.createElement("span", null, parseInt(track.viewCount).toLocaleString()), - React.createElement("i", {className: "icon ion-heart"}), React.createElement("span", null, parseInt(track.likeCount).toLocaleString()) - ) - ), - React.createElement("div", {className: "md-tile-hover"}, - starButton, - playNextButton, - React.createElement("i", {className: "add-to-playlist-btn icon ion-plus", title: "Add to playlist", onClick: this.onAddTrackToPlaylist}), - removeButton - ) - ) - ); - } + ); + } + + if (context !== 'nowplaying') { + playNextButton = ( + React.createElement("i", {className: "add-to-playlist-btn icon ion-log-in", title: "Play Next", onClick: this.onPlayNext}) + ); + } + + if (track.starred) { + unstarButton = ( + React.createElement("div", {className: "starred"}, + React.createElement("i", {className: "remove-btn icon ion-star", title: "Remove Star", onClick: this.onStarTrack}) + ) + ) + } else { + starButton = ( + React.createElement("i", {className: "like-btn icon ion-star", title: "Star", onClick: this.onStarTrack}) + ) + } + + var className = 'track-item'; + + if (track.error) { + className += ' error'; + } + + return ( + React.createElement("li", {id: 'track-item-' + track.id, className: className}, + React.createElement("div", {className: "md-tile-left", onClick: this.onClick}, + React.createElement("div", {className: "face"}, + React.createElement("img", {src: track.artworkUrl, alt: track.title}) + ) + ), + React.createElement("div", {className: "play-actions", onClick: this.onClick}, + React.createElement("span", {className: "track-number"}, this.props.trackNumber), + React.createElement("i", {className: "dynamic-icon icon icon-playing"}), + React.createElement("i", {className: "dynamic-icon icon ion-ios-play"}), + React.createElement("i", {className: "dynamic-icon icon ion-ios-pause"}) + ), + unstarButton, + React.createElement("div", {className: "md-tile-content"}, + React.createElement("h3", {onClick: this.onClick}, track.title), + React.createElement("p", {className: "statistic"}, + React.createElement("a", {className: "original-link", href: track.originalUrl, title: "View Original"}, + React.createElement("i", {className: fontIcon[track.origin]}) + ), + React.createElement("i", {className: "icon ion-headphone"}), React.createElement("span", null, parseInt(track.viewCount).toLocaleString()), + React.createElement("i", {className: "icon ion-heart"}), React.createElement("span", null, parseInt(track.likeCount).toLocaleString()), + React.createElement("i", {className: "icon ion-clock"}), React.createElement("span", null, this.seconds2time(track.duration)) + ) + ), + React.createElement("div", {className: "md-tile-hover"}, + starButton, + playNextButton, + React.createElement("i", {className: "add-to-playlist-btn icon ion-plus", title: "Add to playlist", onClick: this.onAddTrackToPlaylist}), + removeButton + ) + ) + ); + } }); window.TrackList = React.createClass({displayName: "TrackList", - propTypes: { - tracks: React.PropTypes.array, - trackClick: React.PropTypes.string, - onTrackClick: React.PropTypes.func, - onPlayNext: React.PropTypes.func, - onAddTrackToPlaylist: React.PropTypes.func, - onStarTrack: React.PropTypes.func, - onRemoveTrack: React.PropTypes.func, - componentDidUpdate: React.PropTypes.func, - listContext: React.PropTypes.string - }, - render: function() { - var tracks = this.props.tracks; - var onTrackClick = this.props.onTrackClick; - var onAddTrackToPlaylist = this.props.onAddTrackToPlaylist; - var onRemoveTrack = this.props.onRemoveTrack; - var onStarTrack = this.props.onStarTrack; - var onPlayNext = this.props.onPlayNext; - var listContext = this.props.listContext; - - var trackNumber = 0; - var rows = _.map(tracks, function(track) { - - trackNumber ++; - - if (track) { - return ( - React.createElement(TrackItem, {context: listContext, key: track.uuid, track: track, trackNumber: trackNumber, player: player, onTrackClick: onTrackClick, onAddTrackToPlaylist: onAddTrackToPlaylist, onRemoveTrack: onRemoveTrack, onStarTrack: onStarTrack, onPlayNext: onPlayNext}) - ); - } - }); - - return ( - React.createElement("ul", {className: "media-list"}, - rows - ) - ) - - }, - componentDidUpdate: function() { - if (this.props.componentDidUpdate) { - this.props.componentDidUpdate(); - } - } + propTypes: { + tracks: React.PropTypes.array, + trackClick: React.PropTypes.string, + onTrackClick: React.PropTypes.func, + onPlayNext: React.PropTypes.func, + onAddTrackToPlaylist: React.PropTypes.func, + onStarTrack: React.PropTypes.func, + onRemoveTrack: React.PropTypes.func, + componentDidUpdate: React.PropTypes.func, + listContext: React.PropTypes.string + }, + render: function() { + var tracks = this.props.tracks; + var onTrackClick = this.props.onTrackClick; + var onAddTrackToPlaylist = this.props.onAddTrackToPlaylist; + var onRemoveTrack = this.props.onRemoveTrack; + var onStarTrack = this.props.onStarTrack; + var onPlayNext = this.props.onPlayNext; + var listContext = this.props.listContext; + + var trackNumber = 0; + var rows = _.map(tracks, function(track) { + + trackNumber ++; + + if (track) { + return ( + React.createElement(TrackItem, {context: listContext, key: track.uuid, track: track, trackNumber: trackNumber, player: player, onTrackClick: onTrackClick, onAddTrackToPlaylist: onAddTrackToPlaylist, onRemoveTrack: onRemoveTrack, onStarTrack: onStarTrack, onPlayNext: onPlayNext}) + ); + } + }); + + return ( + React.createElement("ul", {className: "media-list"}, + rows + ) + ) + + }, + componentDidUpdate: function() { + if (this.props.componentDidUpdate) { + this.props.componentDidUpdate(); + } + } }); \ No newline at end of file diff --git a/app/styles/main.css b/app/styles/main.css index a65d725..e13ef2a 100644 --- a/app/styles/main.css +++ b/app/styles/main.css @@ -2,7 +2,7 @@ * Spotify: #84bd00; * * light-green: #8bc34a; - * + * */ html { max-height: 448px; @@ -25,7 +25,11 @@ html { md-icon { font-size: 24px; line-height: 24px;} -.md-tile-content .statistic {font-size: 12px;} +.md-tile-content .statistic { + font-size: 12px; + white-space: nowrap; + overflow: hidden; +} .md-tile-content .statistic > md-icon {font-size: 12px; line-height: 12px; color: #555; width: 12px; height: 12px;} .md-tile-content .statistic > span {margin-right: 10px; vertical-align: middle;} @@ -103,7 +107,7 @@ md-autocomplete ul li:hover, md-autocomplete ul li.selected { .chosen-category { text-align: right; } -.chosen-category h3{ +.chosen-category h3{ display: inline-block; margin: 0px; padding: 5px 0; @@ -370,11 +374,11 @@ md-slider:not([md-discrete]):not([disabled]):focus .md-focus-ring, md-slider:not .track-item .statistic i { margin-right: 5px; } - + .media-list a.original-link {width: 20px; margin-top: 25px; margin-right: 5px;} .media-list .statistic { display: none; } .media-list .brand-icon {font-size: 18px; line-height: 18px; width: 24px; height: 18px;} - + .media-list .face { position: relative; width: 40px; @@ -439,7 +443,7 @@ md-slider:not([md-discrete]):not([disabled]):focus .md-focus-ring, md-slider:not .media-list .playing h3, .media-list .pause h3 { color: #8bc34a; } - + .media-list .playing .icon-playing{ display: inline-block; } @@ -502,7 +506,7 @@ track-list > div { z-index: 100; display: none; animation: fadeOut .25s; - box-shadow: 0px 2px 5px 0 rgba(0, 0, 0, 0.26); + box-shadow: 0px 2px 5px 0 rgba(0, 0, 0, 0.26); } #singleton-playlist-menu.open {