Albums and artists

This commit is contained in:
nelsonlaquet 2013-07-31 21:01:41 -05:00
parent 21009713f3
commit 70dccafa9c
32 changed files with 860 additions and 28 deletions

View file

@ -1,7 +1,28 @@
<?php <?php
use Entities\Album;
class AlbumsController extends Controller { class AlbumsController extends Controller {
public function getIndex() { public function getIndex() {
return View::make('albums.index'); return View::make('albums.index');
} }
public function getShow($id, $slug) {
$album = Album::find($id);
if (!$album)
App::abort(404);
if ($album->slug != $slug)
return Redirect::action('AlbumsController@getAlbum', [$id, $album->slug]);
return View::make('albums.show');
}
public function getShortlink($id) {
$album = Album::find($id);
if (!$album)
App::abort(404);
return Redirect::action('AlbumsController@getTrack', [$id, $album->slug]);
}
} }

View file

@ -28,6 +28,93 @@
return $this->execute(new DeleteAlbumCommand($id)); return $this->execute(new DeleteAlbumCommand($id));
} }
public function getShow($id) {
$album = Album::with('tracks', 'user')->find($id);
if (!$album)
App::abort(404);
$tracks = [];
foreach ($album->tracks as $track) {
$tracks[] = Track::mapPublicTrackSummary($track);
}
$formats = [];
foreach (Track::$Formats as $name => $format) {
$formats[] = [
'name' => $name,
'extension' => $format['extension'],
'url' => $album->getDownloadUrl($name)
];
}
return Response::json([
'album' => [
'id' => $album->id,
'formats' => $formats,
'track_count' => $album->tracks->count(),
'title' => $album->title,
'description' => $album->description,
'slug' => $album->slug,
'created_at' => $album->created_at,
'covers' => [
'small' => $album->getCoverUrl(Image::SMALL),
'normal' => $album->getCoverUrl(Image::NORMAL)
],
'url' => $album->url,
'user' => [
'id' => $album->user->id,
'name' => $album->user->display_name,
'url' => $album->user->url,
],
'tracks' => $tracks,
'stats' => [
'views' => 0,
'downloads' => 0
],
'comments' => ['count' => 0, 'list' => []]
]
], 200);
}
public function getIndex() {
$page = 1;
if (Input::has('page'))
$page = Input::get('page');
$query = Album::summary()
->with('tracks', 'user')
->orderBy('created_at', 'desc')
->whereRaw('(SELECT COUNT(id) FROM tracks WHERE tracks.album_id = albums.id) > 0');
$count = $query->count();
$perPage = 15;
$query->skip(($page - 1) * $perPage)->take($perPage);
$albums = [];
foreach ($query->get() as $album) {
$albums[] = [
'id' => $album->id,
'track_count' => $album->tracks->count(),
'title' => $album->title,
'slug' => $album->slug,
'created_at' => $album->created_at,
'covers' => [
'small' => $album->getCoverUrl(Image::SMALL),
'normal' => $album->getCoverUrl(Image::NORMAL)
],
'url' => $album->url,
'user' => [
'id' => $album->user->id,
'name' => $album->user->display_name,
'url' => $album->user->url,
]
];
}
return Response::json(["albums" => $albums, "current_page" => $page, "total_pages" => ceil($count / $perPage)], 200);
}
public function getOwned() { public function getOwned() {
$query = Album::summary()->where('user_id', \Auth::user()->id)->orderBy('created_at', 'desc')->get(); $query = Album::summary()->where('user_id', \Auth::user()->id)->orderBy('created_at', 'desc')->get();
$albums = []; $albums = [];

View file

@ -0,0 +1,69 @@
<?php
namespace Api\Web;
use Commands\CreateAlbumCommand;
use Commands\DeleteAlbumCommand;
use Commands\DeleteTrackCommand;
use Commands\EditAlbumCommand;
use Commands\EditTrackCommand;
use Cover;
use Entities\Album;
use Entities\Image;
use Entities\Track;
use Entities\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response;
class ArtistsController extends \ApiControllerBase {
public function getShow($slug) {
$user = User::whereSlug($slug)->first();
if (!$user)
App::abort(404);
return Response::json([
'artist' => [
'id' => $user->id,
'name' => $user->display_name,
'slug' => $user->slug,
'avatars' => [
'small' => $user->getAvatarUrl(Image::SMALL),
'normal' => $user->getAvatarUrl(Image::NORMAL)
],
'created_at' => $user->created_at
]
], 200);
}
public function getIndex() {
$page = 1;
if (Input::has('page'))
$page = Input::get('page');
$query = User::orderBy('created_at', 'desc')
->whereRaw('(SELECT COUNT(id) FROM tracks WHERE tracks.user_id = users.id) > 0');
$count = $query->count();
$perPage = 15;
$query->skip(($page - 1) * $perPage)->take($perPage);
$users = [];
foreach ($query->get() as $user) {
$users[] = [
'id' => $user->id,
'name' => $user->display_name,
'slug' => $user->slug,
'url' => $user->url,
'avatars' => [
'small' => $user->getAvatarUrl(Image::SMALL),
'normal' => $user->getAvatarUrl(Image::NORMAL)
],
'created_at' => $user->created_at
];
}
return Response::json(["artists" => $users, "current_page" => $page, "total_pages" => ceil($count / $perPage)], 200);
}
}

View file

@ -1,7 +1,25 @@
<?php <?php
use Entities\User;
class ArtistsController extends Controller { class ArtistsController extends Controller {
public function getIndex() { public function getIndex() {
return View::make('artists.index'); return View::make('artists.index');
} }
public function getProfile($slug) {
$user = User::whereSlug($slug)->first();
if (!$user)
App::abort('404');
return View::make('artists.profile');
}
public function getShortlink($id) {
$user = User::find($id);
if (!$user)
App::abort('404');
return Redirect::action('ArtistsController@getProfile', [$id]);
}
} }

View file

@ -8,4 +8,19 @@
public static function angular($expression) { public static function angular($expression) {
return '{{' . $expression . '}}'; return '{{' . $expression . '}}';
} }
public static function formatBytes($bytes, $precision = 2) {
if ($bytes == 0)
return '0 MB';
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
} }

View file

@ -3,6 +3,7 @@
namespace Entities; namespace Entities;
use Cover; use Cover;
use Illuminate\Support\Facades\URL;
use Whoops\Example\Exception; use Whoops\Example\Exception;
use Traits\SlugTrait; use Traits\SlugTrait;
@ -33,6 +34,14 @@
return $this->cover_id != null; return $this->cover_id != null;
} }
public function getUrlAttribute() {
return URL::to('albums/' . $this->id . '-' . $this->slug);
}
public function getDownloadUrl($format) {
return URL::to('a' . $this->id . '/dl.' . Track::$Formats[$format]['extension']);
}
public function getCoverUrl($type = Image::NORMAL) { public function getCoverUrl($type = Image::NORMAL) {
if (!$this->hasCover()) if (!$this->hasCover())
return $this->user->getAvatarUrl($type); return $this->user->getAvatarUrl($type);

View file

@ -5,6 +5,7 @@
use Cover; use Cover;
use External; use External;
use getid3_writetags; use getid3_writetags;
use Helpers;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\URL; use Illuminate\Support\Facades\URL;
use Illuminate\Support\Str; use Illuminate\Support\Str;
@ -32,6 +33,35 @@
$returnValue = self::mapPublicTrackSummary($track); $returnValue = self::mapPublicTrackSummary($track);
$returnValue['description'] = $track->description; $returnValue['description'] = $track->description;
$returnValue['lyrics'] = $track->lyrics; $returnValue['lyrics'] = $track->lyrics;
$returnValue['stats'] = [
'views' => 0,
'plays' => 0,
'downloads' => 0
];
$returnValue['comments'] = ['count' => 0, 'list' => []];
if ($track->album_id != null) {
$returnValue['album'] = [
'title' => $track->album->title,
'url' => $track->album->url,
];
}
$formats = [];
foreach (self::$Formats as $name => $format) {
$file = $track->getFileFor($name);
$url = $track->getUrlFor($name);
$size = 0;
if (is_file($file))
$size = filesize($file);
$formats[] = ['name' => $name, 'extension' => $format['extension'], 'url' => $url, 'size' => Helpers::formatBytes($size)];
}
$returnValue['formats'] = $formats;
return $returnValue; return $returnValue;
} }
@ -204,6 +234,14 @@
return "{$this->getDirectory()}/{$this->id}.{$format['extension']}"; return "{$this->getDirectory()}/{$this->id}.{$format['extension']}";
} }
public function getUrlFor($format) {
if (!isset(self::$Formats[$format]))
throw new Exception("$format is not a valid format!");
$format = self::$Formats[$format];
return URL::to('/t' . $this->id . '/dl.' . $format['extension']);
}
public function updateTags() { public function updateTags() {
foreach (self::$Formats as $format => $data) { foreach (self::$Formats as $format => $data) {
$this->{$data['tag_method']}($format); $this->{$data['tag_method']}($format);

View file

@ -18,10 +18,14 @@
Route::get('tracks/{id}-{slug}', 'TracksController@getTrack'); Route::get('tracks/{id}-{slug}', 'TracksController@getTrack');
Route::get('t{id}', 'TracksController@getShortlink' ); Route::get('t{id}', 'TracksController@getShortlink' );
Route::get('t{id}/dl.{extension}', 'TracksController@getDownload' );
Route::get('/albums', 'AlbumsController@getIndex'); Route::get('albums', 'AlbumsController@getIndex');
Route::get('/artists', 'ArtistsController@getIndex'); Route::get('albums/{id}-{slug}', 'AlbumsController@getShow');
Route::get('/playlists', 'PlaylistsController@getIndex'); Route::get('a{id}', 'AlbumsController@getShortlink')->where('id', '\d+');
Route::get('artists', 'ArtistsController@getIndex');
Route::get('playlists', 'PlaylistsController@getIndex');
Route::get('/login', function() { return View::make('auth.login'); }); Route::get('/login', function() { return View::make('auth.login'); });
Route::get('/register', function() { return View::make('auth.register'); }); Route::get('/register', function() { return View::make('auth.register'); });
@ -29,12 +33,12 @@
Route::get('/about', function() { return View::make('pages.about'); }); Route::get('/about', function() { return View::make('pages.about'); });
Route::get('/faq', function() { return View::make('pages.faq'); }); Route::get('/faq', function() { return View::make('pages.faq'); });
Route::get('i{id}/{type}.png', 'ImagesController@getImage'); Route::get('i{id}/{type}.png', 'ImagesController@getImage')->where('id', '\d+');
Route::get('u{id}/avatar_{type}.png', 'UsersController@getAvatar'); Route::get('u{id}/avatar_{type}.png', 'UsersController@getAvatar')->where('id', '\d+');
Route::get('playlist/{id}-{slug}', 'PlaylistsController@getPlaylist'); Route::get('playlist/{id}-{slug}', 'PlaylistsController@getPlaylist');
Route::get('p{id}', 'PlaylistsController@getShortlink'); Route::get('p{id}', 'PlaylistsController@getShortlink')->where('id', '\d+');
Route::group(['prefix' => 'api/web'], function() { Route::group(['prefix' => 'api/web'], function() {
Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll'); Route::get('/taxonomies/all', 'Api\Web\TaxonomiesController@getAll');
@ -45,6 +49,12 @@
Route::get('/tracks', 'Api\Web\TracksController@getIndex'); Route::get('/tracks', 'Api\Web\TracksController@getIndex');
Route::get('/tracks/{id}', 'Api\Web\TracksController@getShow')->where('id', '\d+'); Route::get('/tracks/{id}', 'Api\Web\TracksController@getShow')->where('id', '\d+');
Route::get('/albums', 'Api\Web\AlbumsController@getIndex');
Route::get('/albums/{id}', 'Api\Web\AlbumsController@getShow')->where('id', '\d+');
Route::get('/artists', 'Api\Web\ArtistsController@getIndex');
Route::get('/artists/{slug}', 'Api\Web\ArtistsController@getShow')->where('id', '[-\w]');
Route::get('/dashboard', 'Api\Web\DashboardController@getIndex'); Route::get('/dashboard', 'Api\Web\DashboardController@getIndex');
Route::group(['before' => 'auth|csrf'], function() { Route::group(['before' => 'auth|csrf'], function() {
@ -101,4 +111,8 @@
}); });
}); });
Route::get('u{id}', 'ArtistsController@getShortlink')->where('id', '\d+');
Route::get('users/{id}-{slug}', 'ArtistsController@getShortlink')->where('id', '\d+');
Route::get('{slug}', 'ArtistsController@getProfile')->where('id', '[-\w]');
Route::get('/', 'HomeController@getIndex'); Route::get('/', 'HomeController@getIndex');

View file

@ -0,0 +1,6 @@
@extends('shared._app_layout')
@section('app_content')
<h1>Album!</h1>
<p>This page should be what search engines see</p>
@endsection

View file

@ -0,0 +1,6 @@
@extends('shared._app_layout')
@section('app_content')
<h1>Artist Profile!</h1>
<p>This page should be what search engines see</p>
@endsection

View file

@ -36,9 +36,8 @@
@endif @endif
<li><h3>Discover</h3></li> <li><h3>Discover</h3></li>
<li ng-class="{selected: $state.includes('tracks') || $state.includes('track')}"><a href="/tracks">Music <i class="icon-music"></i></a></li> <li ng-class="{selected: $state.includes('tracks') || $state.includes('track')}"><a href="/tracks">Music <i class="icon-music"></i></a></li>
<li ng-class="{selected: $state.includes('albums')}"><a href="/albums">Albums <i class="icon-music"></i></a></li> <li ng-class="{selected: $state.includes('albums') || $state.includes('album')}"><a href="/albums">Albums <i class="icon-th-list"></i></a></li>
<li ng-class="{selected: $state.includes('playlists')}"><a href="/playlists">Playlists <i class="icon-music"></i></a></li> <li ng-class="{selected: $state.includes('artists') || $state.includes('artist')}"><a href="/artists">Artists <i class="icon-user"></i></a></li>
<li ng-class="{selected: $state.includes('artists')}"><a href="/artists">Artists <i class="icon-user"></i></a></li>
@if (Auth::check()) @if (Auth::check())
<li> <li>
@ -102,7 +101,17 @@
@section('scripts') @section('scripts')
<div id="fb-root"></div>
<script> <script>
(function(d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s); js.id = id;
js.src = "//connect.facebook.net/en_US/all.js#xfbml=1&appId=186765381447538";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
window.pfm = { window.pfm = {
token: "{{Session::token()}}", token: "{{Session::token()}}",
auth: { auth: {

View file

@ -101,6 +101,18 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date', 'ui.sortable'],
state.state 'albums', state.state 'albums',
url: '/albums' url: '/albums'
templateUrl: '/templates/albums/index.html' templateUrl: '/templates/albums/index.html'
controller: 'albums'
abstract: true
state.state 'albums.list',
url: '?page'
templateUrl: '/templates/albums/list.html'
controller: 'albums-list'
state.state 'album',
url: '/albums/{id:[^\-]+}-{slug}'
templateUrl: '/templates/albums/show.html'
controller: 'album'
# Playlists # Playlists
@ -118,6 +130,13 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date', 'ui.sortable'],
state.state 'artists', state.state 'artists',
url: '/artists' url: '/artists'
templateUrl: '/templates/artists/index.html' templateUrl: '/templates/artists/index.html'
controller: 'artists'
abstract: true
state.state 'artists.list',
url: '?page'
templateUrl: '/templates/artists/list.html'
controller: 'artists-list'
# Pages # Pages
@ -152,6 +171,12 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date', 'ui.sortable'],
url: '/' url: '/'
templateUrl: '/templates/home/index.html' templateUrl: '/templates/home/index.html'
# Final catch-all for aritsts
state.state 'artist',
url: '^/:slug'
templateUrl: '/templates/artists/show.html'
controller: 'artist'
route.otherwise '/' route.otherwise '/'
location.html5Mode(true); location.html5Mode(true);

View file

@ -0,0 +1,18 @@
window.pfm.preloaders['album'] = [
'albums', '$state', 'playlists'
(albums, $state, playlists) ->
$.when.all [albums.fetch $state.params.id, playlists.refreshOwned(true)]
]
angular.module('ponyfm').controller "album", [
'$scope', 'albums', '$state', 'playlists', 'auth'
($scope, albums, $state, playlists, auth) ->
albums.fetch($state.params.id).done (albumResponse) ->
$scope.album = albumResponse.album
$scope.playlists = []
if auth.data.isLogged
playlists.refreshOwned().done (lists) ->
$scope.playlists.push list for list in lists
]

View file

@ -0,0 +1,12 @@
window.pfm.preloaders['albums-list'] = [
'albums', '$state'
(albums, $state) ->
albums.fetchList($state.params.page, true)
]
angular.module('ponyfm').controller "albums-list", [
'$scope', 'albums', '$state'
($scope, albums, $state) ->
albums.fetchList($state.params.page).done (list) ->
$scope.albums = list.albums
]

View file

@ -0,0 +1,21 @@
angular.module('ponyfm').controller "albums", [
'$scope', 'albums', '$state'
($scope, albums, $state) ->
refreshPages = (list) ->
$scope.albums = list.albums
$scope.currentPage = parseInt(list.current_page)
$scope.totalPages = parseInt(list.total_pages)
delete $scope.nextPage
delete $scope.prevPage
$scope.nextPage = $scope.currentPage + 1 if $scope.currentPage < $scope.totalPages
$scope.prevPage = $scope.currentPage - 1 if $scope.currentPage > 1
$scope.pages = [1..$scope.totalPages]
albums.fetchList($state.params.page).done refreshPages
$scope.$on 'albums-feteched', (e, list) -> refreshPages(list)
$scope.gotoPage = (page) ->
$state.transitionTo 'albums.list', {page: page}
]

View file

@ -0,0 +1,12 @@
window.pfm.preloaders['artist'] = [
'artists', '$state'
(artists, $state) ->
artists.fetch $state.params.slug
]
angular.module('ponyfm').controller "artist", [
'$scope', 'artists', '$state'
($scope, artists, $state) ->
artists.fetch($state.params.slug).done (artistResponse) ->
$scope.artist = artistResponse.artist
]

View file

@ -0,0 +1,12 @@
window.pfm.preloaders['artists-list'] = [
'artists', '$state'
(artists, $state) ->
artists.fetchList($state.params.page, true)
]
angular.module('ponyfm').controller "artists-list", [
'$scope', 'artists', '$state'
($scope, artists, $state) ->
artists.fetchList($state.params.page).done (list) ->
$scope.artists = list.artists
]

View file

@ -0,0 +1,21 @@
angular.module('ponyfm').controller "artists", [
'$scope', 'artists', '$state'
($scope, artists, $state) ->
refreshPages = (list) ->
$scope.artists = list.artists
$scope.currentPage = parseInt(list.current_page)
$scope.totalPages = parseInt(list.total_pages)
delete $scope.nextPage
delete $scope.prevPage
$scope.nextPage = $scope.currentPage + 1 if $scope.currentPage < $scope.totalPages
$scope.prevPage = $scope.currentPage - 1 if $scope.currentPage > 1
$scope.pages = [1..$scope.totalPages]
artists.fetchList($state.params.page).done refreshPages
$scope.$on 'artists-feteched', (e, list) -> refreshPages(list)
$scope.gotoPage = (page) ->
$state.transitionTo 'artists.list', {page: page}
]

View file

@ -1,12 +1,18 @@
window.pfm.preloaders['track'] = [ window.pfm.preloaders['track'] = [
'tracks', '$state' 'tracks', '$state', 'playlists'
(tracks, $state) -> (tracks, $state, playlists) ->
tracks.fetch $state.params.id $.when.all [tracks.fetch $state.params.id, playlists.refreshOwned(true)]
] ]
angular.module('ponyfm').controller "track", [ angular.module('ponyfm').controller "track", [
'$scope', 'tracks', '$state' '$scope', 'tracks', '$state', 'playlists', 'auth'
($scope, tracks, $state) -> ($scope, tracks, $state, playlists, auth) ->
tracks.fetch($state.params.id).done (trackResponse) -> tracks.fetch($state.params.id).done (trackResponse) ->
$scope.track = trackResponse.track $scope.track = trackResponse.track
$scope.playlists = []
if auth.data.isLogged
playlists.refreshOwned().done (lists) ->
$scope.playlists.push list for list in lists
] ]

View file

@ -0,0 +1,32 @@
angular.module('ponyfm').factory('albums', [
'$rootScope', '$http'
($rootScope, $http) ->
albumPages = []
albums = {}
self =
filters: {}
fetchList: (page, force) ->
force = force || false
page = 1 if !page
return albumPages[page] if !force && albumPages[page]
albumsDef = new $.Deferred()
$http.get('/api/web/albums?page=' + page).success (albums) ->
albumsDef.resolve albums
$rootScope.$broadcast 'albums-feteched', albums
albumPages[page] = albumsDef.promise()
fetch: (id, force) ->
force = force || false
id = 1 if !id
return albums[id] if !force && albums[id]
albumsDef = new $.Deferred()
$http.get('/api/web/albums/' + id).success (albums) ->
albumsDef.resolve albums
albums[id] = albumsDef.promise()
self
])

View file

@ -0,0 +1,32 @@
angular.module('ponyfm').factory('artists', [
'$rootScope', '$http'
($rootScope, $http) ->
artistPage = []
artists = {}
self =
filters: {}
fetchList: (page, force) ->
force = force || false
page = 1 if !page
return artistPage[page] if !force && artistPage[page]
artistsDef = new $.Deferred()
$http.get('/api/web/artists?page=' + page).success (albums) ->
artistsDef.resolve albums
$rootScope.$broadcast 'artists-feteched', albums
artistPage[page] = artistsDef.promise()
fetch: (slug, force) ->
force = force || false
slug = 1 if !slug
return artists[slug] if !force && artists[slug]
artistsDef = new $.Deferred()
$http.get('/api/web/artists/' + slug).success (albums) ->
artistsDef.resolve albums
artists[slug] = artistsDef.promise()
self
])

View file

@ -11,10 +11,16 @@ window.handleResize = () ->
$('.revealable').each () -> $('.revealable').each () ->
$this = $ this $this = $ this
return if $this.data 'real-height'
$this.data 'real-height', $this.height() $this.data 'real-height', $this.height()
$this.css $this.css
height: '15em' height: '15em'
# if $this.height() > $this.data 'real-height'
# $this.css {height: 'auto'}
# $this.find('.reveal').css {display: 'none'}
# return
$this.find('.reveal').click (e) -> $this.find('.reveal').click (e) ->
e.preventDefault() e.preventDefault()
$this.css {height: 'auto'} $this.css {height: 'auto'}

97
public/styles/albums.less Normal file
View file

@ -0,0 +1,97 @@
@import-once 'base/bootstrap/bootstrap';
@import-once 'mixins';
.albums-listing, .artist-listing {
overflow-y: auto;
margin: 0px;
padding: 0px;
list-style: none;
li {
.box-sizing(border-box);
width: 20%;
float: left;
padding: 0px;
margin: 0px;
line-height: normal;
&.empty {
.alert();
float: none !important;
width: auto !important;
display: block;
padding: 5px;
font-size: 9pt;
}
&.is-not-published a {
background: fadeout(@yellow, 90%);
}
&.selected, &.selected:hover {
a {
#gradient>.vertical(#149bdf, #0480be);
cursor: default;
color: #fff;
.published {
color: #eee;
}
}
}
a {
.transition(350px ease-out all);
display: block;
margin: 0px;
font-size: 9pt;
font-weight: normal;
overflow: hidden;
padding: 15px;
.image {
}
.title {
margin: 0px;
font-size: 12pt;
padding: 0px;
margin-top: 5px;
}
.published {
margin: 0px;
padding: 0px;
}
.image {
.img-polaroid();
float: left;
display: block;
position: relative;
left: -5px;
width: 100%;
height: auto;
float: none;
}
.title {
.ellipsis();
display: block;
}
.published {
display: block;
color: #777;
font-size: 8pt;
}
&:hover {
background: #ddd;
text-decoration: none;
}
}
}
}

View file

@ -7,4 +7,5 @@
@import-once 'components'; @import-once 'components';
@import-once 'forms'; @import-once 'forms';
@import-once 'tracks'; @import-once 'tracks';
@import-once 'albums';
@import-once 'animations'; @import-once 'animations';

View file

@ -127,14 +127,60 @@
} }
} }
.track-details { .track-details, .album-details {
h1 {
.box-shadow(0px 2px 3px rgba(0, 0, 0, .3));
background: #eee;
padding: 5px;
}
.comments {
.alert {
.border-radius(0px);
margin: 5px 0px;
}
button {
.border-radius(0px);
}
}
.stretch-to-bottom {
padding-top: 10px;
}
.cover-image { .cover-image {
img { img {
.img-polaroid(); .img-polaroid();
.box-sizing(border-box);
width: 100%;
display: block;
margin-bottom: 5px;
}
.btn {
.border-radius(0px);
display: block;
width: 'auto';
margin: 5px 0px;
}
.stats {
margin: 0px;
padding: 0px;
list-style: none;
li {
margin: 0px;
padding: 5px 0px;
border-bottom: 1px solid #ddd;
}
} }
} }
.lyrics { .lyrics {
font-size: 10pt;
color: #222;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
padding: 0px; padding: 0px;
@ -178,11 +224,27 @@
html { html {
.track-toolbar { .track-toolbar {
background: #eee;
padding: 5px; padding: 5px;
> .btn:first-child, > .btn:last-child { .btn:first-child, > .btn:last-child {
.border-radius(0px); .border-radius(0px);
} }
> .btn {
border-right: none;
}
.dropdown:first-child {
border-right: 1px solid #cccccc;
}
.dropdown {
float: right;
border-right: none;
.btn-primary {
color: #fff;
}
}
} }
} }

View file

@ -1,3 +1,15 @@
<div> <div>
<h1>Albums</h1> <h1>Albums</h1>
<div class="pagination" ng-show="totalPages > 1">
<ul>
<li ng-class="{disabled: !prevPage}"><a href="#" ng-click="gotoPage(prevPage);" pfm-eat-click>Prev</a></li>
<li ng-repeat="page in pages" ng-class="{active: page == currentPage}">
<a href="#" ng-click="gotoPage(page);" pfm-eat-click>{{page}}</a>
</li>
<li ng-class="{disabled: !nextPage}"><a href="#" ng-click="gotoPage(nextPage);" pfm-eat-click>Next</a></li>
</ul>
</div>
<ui-view></ui-view>
</div> </div>

View file

@ -0,0 +1,14 @@
<ul class="albums-listing stretch-to-bottom">
<li ng-repeat="album in albums">
<a href="{{album.url}}">
<img class="image" ng-src="{{album.covers.normal}}" />
<span class="title">{{album.title}}</span>
<span class="published">
by {{album.user.name}}
</span>
</a>
</li>
<li ng-show="!albums.length" class="empty">
No albums found...
</li>
</ul>

View file

@ -0,0 +1,88 @@
<div class="album-details">
<ul class="breadcrumb">
<li><a href="/albums">Albums</a> <span class="divider">/</span></li>
<li class="active">{{album.title}}</li>
</ul>
<div class="track-toolbar btn-group pull-right">
<a href="#" class="btn btn-small">
Favourite This!
<i class="icon-star-empty"></i>
</a>
<div class="dropdown">
<a href="#" class="btn btn-small btn-info dropdown-toggle">
Downloads <i class="caret"></i>
</a>
<ul class="dropdown-menu">
<li ng-repeat="format in album.formats"><a href="{{format.url}}">{{format.name}}</a></li>
</ul>
</div>
</div>
<h1>
{{album.title}}
<span class="subtitle">
by: <a href="{{album.user.url}}">{{album.user.name}}</a>
</span>
</h1>
<div class="stretch-to-bottom">
<div class="row-fluid">
<div class="span8">
<div class="description">
<p ng-bind-html-unsafe="album.description | noHTML | newlines"></p>
</div>
<h2>Tracks</h2>
<ul class="tracks-listing">
<li ng-repeat="track in album.tracks">
<div class="image">
<a href="#" class="play-button"><i class="icon-play"></i></a>
<img ng-src="{{track.covers.thumbnail}}" />
</div>
<div class="icons">
<span><i ng-class="{'icon-microphone-off': !track.is_vocal, 'icon-microphone': track.is_vocal}"></i></span>
<a href="#"><i class="icon-star-empty"></i></a>
</div>
<a class="info" href="{{track.url}}">
<span class="title">{{track.title}}</span>
<span class="metadata">
by: <span class="artist">{{track.user.name}}</span> /
<span class="genre">{{track.genre.name}}</span> /
{{track.published_at.date | momentFromNow}}
</span>
</a>
</li>
</ul>
<h2>Comments</h2>
<div class="comments">
<div class="alert alert-info" ng-show="album.comments.count == 0">
There are no comments yet!
</div>
<form class="pfm-form">
<div class="form-row">
<textarea></textarea>
</div>
<button type="submit" class="btn disabled">Post Comment</button>
</form>
</div>
</div>
<div class="span4 cover-image">
<img ng-src="{{album.covers.normal}}" />
<div class="fb-like" data-href="{{album.url}}" data-send="false" data-layout="button_count" data-width="20" data-show-faces="false"></div>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="{{album.url}}" data-text="{{album.title + ' by ' + album.user.name + ' on Pony.fm'}}" data-via="ponyfm">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<a href="#" class="btn btn-info">Share or Embed</a>
<h2>Stats</h2>
<ul class="stats">
<li>Views: <strong>{{album.stats.views}}</strong></li>
<li>Downloads: <strong>{{album.stats.downloads}}</strong></li>
</ul>
</div>
</div>
</div>
</div>

View file

@ -1,3 +1,15 @@
<div> <div>
<h1>Artists</h1> <h1>Artist Directory</h1>
<div class="pagination" ng-show="totalPages > 1">
<ul>
<li ng-class="{disabled: !prevPage}"><a href="#" ng-click="gotoPage(prevPage);" pfm-eat-click>Prev</a></li>
<li ng-repeat="page in pages" ng-class="{active: page == currentPage}">
<a href="#" ng-click="gotoPage(page);" pfm-eat-click>{{page}}</a>
</li>
<li ng-class="{disabled: !nextPage}"><a href="#" ng-click="gotoPage(nextPage);" pfm-eat-click>Next</a></li>
</ul>
</div>
<ui-view></ui-view>
</div> </div>

View file

@ -0,0 +1,14 @@
<ul class="artist-listing stretch-to-bottom">
<li ng-repeat="artist in artists">
<a href="{{artist.url}}">
<img class="image" ng-src="{{artist.avatars.normal}}" />
<span class="title">{{artist.name}}</span>
<span class="published">
joined {{artist.created_at | momentFromNow}}
</span>
</a>
</li>
<li ng-show="!artists.length" class="empty">
No artists found...
</li>
</ul>

View file

@ -0,0 +1,8 @@
<div>
<ul class="breadcrumb">
<li><a href="/artists">Artist Directory</a> <span class="divider">/</span></li>
<li class="active">{{artist.name}}</li>
</ul>
<h1>{{artist.name}}</h1>
</div>

View file

@ -1,4 +1,4 @@
<div> <div class="track-details">
<ul class="breadcrumb"> <ul class="breadcrumb">
<li><a href="/tracks">Tracks</a> <span class="divider">/</span></li> <li><a href="/tracks">Tracks</a> <span class="divider">/</span></li>
<li><a href="/tracks?filter=genres-{{track.genre.id}}">{{track.genre.name}}</a> <span class="divider">/</span></li> <li><a href="/tracks?filter=genres-{{track.genre.id}}">{{track.genre.name}}</a> <span class="divider">/</span></li>
@ -10,15 +10,21 @@
Favourite This! Favourite This!
<i class="icon-star-empty"></i> <i class="icon-star-empty"></i>
</a> </a>
<a href="#" class="btn btn-small">
Add to Playlist <i class="caret"></i>
</a>
<div class="dropdown"> <div class="dropdown">
<a href="#" class="btn btn-small btn-info dropdown-toggle"> <a href="#" class="btn btn-small btn-info dropdown-toggle">
Downloads <i class="caret"></i> Downloads <i class="caret"></i>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="#"></a></li> <li ng-repeat="format in track.formats"><a href="{{format.url}}">{{format.name}} <small>({{format.size}})</small></a></li>
</ul>
</div>
<div class="dropdown">
<a href="#" class="btn btn-small dropdown-toggle">
Add to Playlist <i class="caret"></i>
</a>
<ul class="dropdown-menu">
<li ng-repeat="playlist in playlists"><a href="{{playlist.url}}">{{playlist.title}}</a></li>
<li><a href="#" class="btn-primary">Add to New Playlist</a></li>
</ul> </ul>
</div> </div>
</div> </div>
@ -26,13 +32,17 @@
<h1> <h1>
{{track.title}} {{track.title}}
<span class="subtitle"> <span class="subtitle">
<span ng-show="track.album">
from: <a href="{{track.album.url}}">{{track.album.title}}</a>
</span>
by: <a href="{{track.user.url}}">{{track.user.name}}</a> by: <a href="{{track.user.url}}">{{track.user.name}}</a>
</span> </span>
</h1> </h1>
<div class="track-details"> <div class="stretch-to-bottom">
<div class="row-fluid"> <div class="row-fluid">
<div class="span7"> <div class="span8">
<div class="description"> <div class="description">
<p ng-bind-html-unsafe="track.description | noHTML | newlines"></p> <p ng-bind-html-unsafe="track.description | noHTML | newlines"></p>
</div> </div>
@ -46,12 +56,37 @@
<p class="content" ng-bind-html-unsafe="track.lyrics | noHTML | newlines"></p> <p class="content" ng-bind-html-unsafe="track.lyrics | noHTML | newlines"></p>
</div> </div>
</div> </div>
<h2>Comments</h2>
<div class="comments">
<div class="alert alert-info" ng-show="track.comments.count == 0">
There are no comments yet!
</div>
<form class="pfm-form">
<div class="form-row">
<textarea></textarea>
</div>
<button type="submit" class="btn disabled">Post Comment</button>
</form>
</div>
</div> </div>
<div class="span5 cover-image"> <div class="span4 cover-image">
<img ng-src="{{track.covers.normal}}" /> <img ng-src="{{track.covers.normal}}" />
<div class="fb-like" data-href="{{track.url}}" data-send="false" data-layout="button_count" data-width="20" data-show-faces="false"></div>
<a href="https://twitter.com/share" class="twitter-share-button" data-url="{{track.url}}" data-text="{{track.title + ' by ' + track.user.name + ' on Pony.fm'}}" data-via="ponyfm">Tweet</a>
<script>!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");</script>
<a href="#" class="btn btn-info">Share or Embed</a>
<h2>Stats</h2>
<ul class="stats">
<li>Published: <strong>{{track.published_at | pfmdate:"short"}}</strong></li>
<li>Views: <strong>{{track.stats.views}}</strong></li>
<li>Plays: <strong>{{track.stats.plays}}</strong></li>
<li>Downloads: <strong>{{track.stats.downloads}}</strong></li>
</ul>
</div> </div>
</div> </div>
<h2>Comments</h2>
</div> </div>
</div> </div>