From 2d43bf64af59c059ff66576c2f7e88c454da0674 Mon Sep 17 00:00:00 2001 From: nelsonlaquet <nelsonlaquet@gmail.com> Date: Sat, 31 Aug 2013 21:20:48 -0500 Subject: [PATCH] finished embed code --- app/controllers/TracksController.php | 34 ++- app/library/Assets.php | 22 ++ app/library/Helpers.php | 16 ++ app/views/tracks/embed.blade.php | 49 ++--- .../scripts/app/controllers/uploader.coffee | 3 - public/scripts/base/jquery.timeago.js | 193 ++++++++++++++++++ public/scripts/embed/favourite.coffee | 12 ++ public/scripts/embed/player.coffee | 81 ++++++++ public/styles/embed.less | 136 ++++++++++++ 9 files changed, 511 insertions(+), 35 deletions(-) create mode 100644 public/scripts/base/jquery.timeago.js create mode 100644 public/scripts/embed/favourite.coffee create mode 100644 public/scripts/embed/player.coffee create mode 100644 public/styles/embed.less diff --git a/app/controllers/TracksController.php b/app/controllers/TracksController.php index acd365dc..771fc7cb 100644 --- a/app/controllers/TracksController.php +++ b/app/controllers/TracksController.php @@ -10,11 +10,41 @@ } public function getEmbed($id) { - $track = Track::find($id); + $track = Track + ::whereId($id) + ->published() + ->userDetails() + ->with( + 'user', + 'user.avatar', + 'genre' + )->first(); + if (!$track || !$track->canView(Auth::user())) App::abort(404); - return View::make('tracks.embed', ['track' => $track]); + $userData = [ + 'stats' => [ + 'views' => 0, + 'plays' => 0, + 'downloads' => 0 + ], + 'is_favourited' => false + ]; + + if ($track->users->count()) { + $userRow = $track->users[0]; + $userData = [ + 'stats' => [ + 'views' => $userRow->view_count, + 'plays' => $userRow->play_count, + 'downloads' => $userRow->download_count, + ], + 'is_favourited' => $userRow->is_favourited + ]; + } + + return View::make('tracks.embed', ['track' => $track, 'user' => $userData]); } public function getTrack($id, $slug) { diff --git a/app/library/Assets.php b/app/library/Assets.php index 2a2b6b24..ee6a3065 100644 --- a/app/library/Assets.php +++ b/app/library/Assets.php @@ -82,6 +82,22 @@ ])); } + return $collection; + } else if ($area == 'embed') { + $collection = new AssetCollection([ + new FileAsset('scripts/base/jquery-2.0.2.js'), + new FileAsset('scripts/base/jquery.viewport.js'), + new FileAsset('scripts/base/underscore.js'), + new FileAsset('scripts/base/moment.js'), + new FileAsset('scripts/base/jquery.timeago.js'), + new FileAsset('scripts/base/soundmanager2-nodebug.js'), + new AssetCollection([ + new GlobAsset('scripts/embed/*.coffee'), + ], [ + new CoffeeScriptFilter(Config::get('app.coffee')) + ]) + ]); + return $collection; } @@ -104,6 +120,12 @@ $css->add(new FileAsset('styles/prettify.css')); } + return $css; + } else if ($area == 'embed') { + $css = new AssetCollection([ + new FileAsset('styles/embed.less'), + ], [new \Assetic\Filter\LessFilter('node')]); + return $css; } diff --git a/app/library/Helpers.php b/app/library/Helpers.php index ec6caaac..771cb787 100644 --- a/app/library/Helpers.php +++ b/app/library/Helpers.php @@ -23,4 +23,20 @@ return round($bytes, $precision) . ' ' . $units[$pow]; } + + /** + * timeago-style timestamp generator macro. + * + * @param string $timestamp A timestamp in SQL DATETIME syntax + * @return string + */ + public static function timestamp( $timestamp ) { + if(gettype($timestamp) !== 'string' && get_class($timestamp) === 'DateTime'){ + $timestamp = $timestamp->format('c'); + } + + $title = date('c', strtotime($timestamp)); + $content = date('F d, o \@ g:i:s a', strtotime($timestamp)); + return '<abbr class="timeago" title="'.$title.'">'.$content.'</abbr>'; + } } \ No newline at end of file diff --git a/app/views/tracks/embed.blade.php b/app/views/tracks/embed.blade.php index 6ef69c7b..e27cd640 100644 --- a/app/views/tracks/embed.blade.php +++ b/app/views/tracks/embed.blade.php @@ -2,10 +2,8 @@ <html lang="en-CA"> <head> <meta charset="UTF-8"> - <title>@section('title')Pony.fm - @yield_section</title> + <title>{{$track->title}} by {{$track->user->display_name}} on Pony.fm</title> <meta itemprop="name" content="Pony.fm"> - {{-- <meta itemprop="image" content="https://pony.fm/favicon.ico"> --}} <meta property="og:title" content="Pony.fm - The Pony Music Hosting Site" /> <meta property="og:type" content="website" /> <meta property="og:url" content="https://pony.fm/" /> @@ -13,55 +11,46 @@ <meta property="og:site_name" content="Pony.fm" /> <meta property="fb:admins" content="1165335382" /> - {{ HTML::style( 'css/app-embed.css?' . filemtime(path('public').'/css/app.css') ) }} - {{ Asset::styles() }} - - <?php Asset::add('jquery', 'https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js'); ?> - <?php Asset::add('jquery-ui', 'https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.0/jquery-ui.min.js', 'jquery'); ?> - <?php Asset::add('scripts', 'js/app.js?' . filemtime(path('public').'/js/app.js'), 'jquery'); ?> + {{ Assets::styleIncludes('embed') }} </head> -<body class="embed"> - <div class="fixed-image-width"> - @if($track->explicit && !(Auth::check() && Auth::user()->can_see_explicit_content)) - <div class="explicit alert-box alert"> - <em>Enable explicit content in {{ HTML::link(URL::to_action('account@edit'), 'your account', ['target' => '_blank']) }} to play this track.</em> - +<body> + @if($track->explicit && !(Auth::check() && Auth::user()->can_see_explicit_content)) + <div class="explicit alert alert-danger"> + <em>Enable explicit content in {{ HTML::link(URL::to('/account/settings'), 'your account', ['target' => '_blank']) }} to play this track.</em> <div class="stats"> <span>Hosted by <a href="{{URL::to('/')}}" target="_blank">Pony.fm</a></span> </div> </div> - @else - <div class="player-small {{Auth::check() ? 'can-favourite' : ''}} {{Track_Plays::hasPlayed($track->id) ? 'played' : 'unplayed'}} {{Track_Plays::hasFavourited($track->id) ? 'favourited' : ''}}" data-track-id="{{ $track->id }}" data-duration="{{ $track->duration * 1000 }}"> + @else + <div class="player loading {{Auth::check() ? 'can-favourite' : ''}} {{$user['is_favourited'] ? 'favourited' : ''}}" data-track-id="{{ $track->id }}" data-duration="{{ $track->duration * 1000 }}"> <div class="play" disabled="disabled"> - <div><i class="icon-play icon-1x"></i></div> - {{ HTML::image($track->get_cover_url('normal')) }} + <div class="button"><i class="icon-play"></i></div> + {{ HTML::image($track->getCoverUrl(\Entities\Image::SMALL)) }} </div> <div class="meta"> @if (Auth::check()) - <a href="#" class="favourite"><i title="Favourite this track!" class="favourite-icon icon-star-empty"></i></a> + <a href="#" class="favourite"><i title="Favourite this track!" class="favourite-icon icon-star-empty"></i></a> @endif <div class="progressbar"> - <div class="progress-container"> - <div class="loader"></div> - <div class="seeker"></div> - </div> + <div class="loader"></div> + <div class="seeker"></div> </div> <span class="title">{{ HTML::link( $track->url, $track->title, ['target' => '_blank'] ) }}</span> - <span>by: <strong>{{ HTML::link($track->user->url, $track->artist, ['target' => '_blank']) }}</strong> / {{ HTML::link($track->genre->url, $track->genre->title, ['target' => '_blank']) }} / {{ HTML::timestamp($track->published_at) }}</span> - <div class="clear"></div> + <span>by: <strong>{{ HTML::link($track->user->url, $track->user->display_name, ['target' => '_blank']) }}</strong> / {{$track->genre->name}} / {{Helpers::timestamp($track->published_at)}}</span> </div> <div class="stats"> - Views: <strong>{{ $track->views }}</strong> / Plays: <strong>{{ $track->plays }}</strong> / Downloads: <strong>{{ $track->downloads }}</strong> / + Views: <strong>{{ $track->view_count }}</strong> / Plays: <strong>{{ $track->play_count }}</strong> / Downloads: <strong>{{ $track->download_count }}</strong> / <span>Hosted by <a href="{{URL::to('/')}}" target="_blank">Pony.fm</a></span> </div> </div> - @endif - </div> + @endif <script> var pfm = {token: '{{ Session::token() }}'} </script> - {{ Asset::scripts() }} + + {{ Assets::scriptIncludes('embed') }} + <script type="text/javascript"> var _gaq = _gaq || []; _gaq.push(['_setAccount', 'UA-29463256-1']); diff --git a/public/scripts/app/controllers/uploader.coffee b/public/scripts/app/controllers/uploader.coffee index 121a4afa..caa0ba99 100644 --- a/public/scripts/app/controllers/uploader.coffee +++ b/public/scripts/app/controllers/uploader.coffee @@ -1,8 +1,5 @@ angular.module('ponyfm').controller "uploader", [ '$scope', 'auth', 'upload', '$state' ($scope, auth, upload, $state) -> - $scope.data = upload - - $scope.$on 'upload-finished', (e, upload) -> ] \ No newline at end of file diff --git a/public/scripts/base/jquery.timeago.js b/public/scripts/base/jquery.timeago.js new file mode 100644 index 00000000..4d58026e --- /dev/null +++ b/public/scripts/base/jquery.timeago.js @@ -0,0 +1,193 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.3.0 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowFuture: false, + localeTitle: false, + cutoff: 0, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + inWords: function(distanceMillis) { + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function(){ + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(time){ + $(this).data('timeago', { datetime: $t.parse(time) }); + refresh.apply(this); + }, + updateFromDOM: function(){ + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); + refresh.apply(this); + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if(!fn){ + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function(){ + fn.call(this, options); + }); + return this; + }; + + function refresh() { + var data = prepareData(this); + var $s = $t.settings; + + if (!isNaN(data.datetime)) { + if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) { + $(this).text(inWords(data.datetime)); + } + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if ($t.settings.localeTitle) { + element.attr("title", element.data('timeago').datetime.toLocaleString()); + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/public/scripts/embed/favourite.coffee b/public/scripts/embed/favourite.coffee new file mode 100644 index 00000000..48372ea3 --- /dev/null +++ b/public/scripts/embed/favourite.coffee @@ -0,0 +1,12 @@ +$player = $ '.player' +$favourite = $player.find '.favourite' +trackId = $player.data 'track-id' + +$favourite.click (e) -> + e.preventDefault() + + $.post('/api/web/favourites/toggle', {type: 'track', id: trackId, _token: pfm.token}).done (res) -> + if res.is_favourited + $player.addClass 'favourited' + else + $player.removeClass 'favourited' \ No newline at end of file diff --git a/public/scripts/embed/player.coffee b/public/scripts/embed/player.coffee new file mode 100644 index 00000000..43ba788d --- /dev/null +++ b/public/scripts/embed/player.coffee @@ -0,0 +1,81 @@ +$('.timeago').timeago() + +loaderDef = new $.Deferred() + +soundManager.setup + url: '/flash/soundmanager/' + flashVersion: 9 + onready: () -> + loaderDef.resolve() + +loaderDef.done -> + $player = $('.player') + $play = $('.player .play') + $progressBar = $player.find '.progressbar' + $loadingBar = $progressBar.find '.loader' + $seekBar = $progressBar.find '.seeker' + currentSound = null + isPlaying = false + trackId = $player.data('track-id') + duration = $player.data('duration') + + $player.removeClass 'loading' + + setPlaying = (playing) -> + isPlaying = playing + if playing + $player.addClass 'playing' + $player.removeClass 'paused' + else + $player.addClass 'paused' + $player.removeClass 'playing' + + $progressBar.click (e) -> + return if !currentSound + percent = ((e.pageX - $progressBar.offset().left) / $progressBar.width()) + duration = parseFloat(duration) + progress = percent * duration + currentSound.setPosition(progress) + + $play.click -> + if currentSound + if isPlaying + currentSound.pause() + else + currentSound.play() + else + currentSound = soundManager.createSound + url: '/t' + trackId + '/stream', + volume: 50 + + whileloading: -> + loadingProgress = (currentSound.bytesLoaded / currentSound.bytesTotal) * 100 + $loadingBar.css + width: loadingProgress + '%' + + whileplaying: -> + progress = (currentSound.position / (duration)) * 100 + $seekBar.css + width: progress + '%' + + onfinish: -> + setPlaying false + currentSound = null + $loadingBar.css {width: '0'} + $seekBar.css {width: '0'} + $player.removeClass 'playing' + $player.removeClass 'paused' + + onstop: -> + setPlaying false + + onplay: -> + + onresume: -> + setPlaying true + + onpause: -> + setPlaying false + + setPlaying true + currentSound.play() \ No newline at end of file diff --git a/public/styles/embed.less b/public/styles/embed.less new file mode 100644 index 00000000..a1b5b8c4 --- /dev/null +++ b/public/styles/embed.less @@ -0,0 +1,136 @@ +@import 'base/bootstrap/bootstrap'; +@import 'base/bootstrap/responsive'; +@import 'base/font-awesome/font-awesome'; +@import 'variables'; +@import 'mixins'; + +body { + padding: 10px; + + a { + color: #C2889C; + + &:hover { + text-decoration: none; + } + } +} + +.player { + + &.playing .play .button i:before { + content: "\f04c"; + } + + &.favourited .meta .favourite { + color: @orange; + text-decoration: none; + + i { + color: #FFD76E; + text-shadow: 0px 0px 2px rgba(0,0,0,0.8); + + &:before { + content: "\f005" + } + } + } + + .play { + .img-polaroid(); + + float: left; + width: 100px; + padding: 3px; + position: relative; + cursor: pointer; + + img { + display: block; + width: 100px; + height: 100px; + } + + &:hover { + .button { + background: rgba(0, 0, 0, 1); + border: 3px solid rgba(255, 255, 255, .9); + } + } + + .button { + .border-radius(60px); + .transition(all 250ms ease-out); + + border: 3px solid rgba(255, 255, 255, .6); + width: 32px; + height: 32px; + position: absolute; + top: 35px; + left: 35px; + text-align: center; + line-height: 32px; + color: #fff; + background: rgba(0, 0, 0, .4); + } + } + + .meta { + margin-left: 115px; + font-size: 11px; + + .favourite { + display: block; + float: right; + font-size: 18px; + margin-top: -3px; + color: #2795b6; + + &:hover { + text-decoration: none; + } + } + + span { + display: block; + } + + .title { + .ellipsis(); + margin-top: 5px; + font-size: 16px; + } + + .progressbar { + cursor: pointer; + height: 11px; + background: #fff; + border: 1px solid #888; + margin-right: 27px; + position: relative; + + .loader, .seeker { + height: 100%; + position: absolute; + top: 0px; + left: 0px; + } + + .loader { + background: #eee; + } + + .seeker { + background: @pfm-purple; + } + } + } + + .stats { + position: absolute; + bottom: 5px; + right: 5px; + font-size: 8pt; + color: #555; + } +} \ No newline at end of file