Track searching stuff

This commit is contained in:
nelsonlaquet 2013-07-31 06:47:16 -05:00
parent d4789ebda3
commit 8d3d126550
37 changed files with 648 additions and 199 deletions

View file

@ -6,14 +6,15 @@
use Entities\License; use Entities\License;
use Entities\ShowSong; use Entities\ShowSong;
use Entities\TrackType; use Entities\TrackType;
use Illuminate\Support\Facades\DB;
class TaxonomiesController extends \ApiControllerBase { class TaxonomiesController extends \ApiControllerBase {
public function getAll() { public function getAll() {
return \Response::json([ return \Response::json([
'licenses' => License::all()->toArray(), 'licenses' => License::all()->toArray(),
'genres' => Genre::orderBy('name')->get()->toArray(), 'genres' => Genre::select('genres.*', DB::raw('(SELECT COUNT(id) FROM tracks WHERE tracks.genre_id = genres.id AND tracks.published_at IS NOT NULL) AS track_count'))->orderBy('name')->get()->toArray(),
'track_types' => TrackType::all()->toArray(), 'track_types' => TrackType::select('track_types.*', DB::raw('(SELECT COUNT(id) FROM tracks WHERE tracks.track_type_id = track_types.id AND tracks.published_at IS NOT NULL) AS track_count'))->get()->toArray(),
'show_songs' => ShowSong::select('title', 'id', 'slug')->get()->toArray() 'show_songs' => ShowSong::select('title', 'id', 'slug', DB::raw('(SELECT COUNT(tracks.id) FROM show_song_track INNER JOIN tracks ON tracks.id = show_song_track.track_id WHERE show_song_track.show_song_id = show_songs.id AND tracks.published_at IS NOT NULL) AS track_count'))->get()->toArray()
], 200); ], 200);
} }
} }

View file

@ -34,39 +34,32 @@
$tracks = []; $tracks = [];
foreach ($query->get() as $track) { foreach ($query->get() as $track) {
$tracks[] = [ $tracks[] = $this->mapPublicTrack($track);
'id' => $track->id,
'title' => $track->title,
'user' => [
'id' => $track->user->id,
'name' => $track->user->display_name,
'url' => $track->user->url
],
'url' => $track->url,
'slug' => $track->slug,
'is_vocal' => $track->is_vocal,
'is_explicit' => $track->is_explicit,
'is_downloadable' => $track->is_downloadable,
'is_published' => $track->isPublished(),
'published_at' => $track->published_at,
'duration' => $track->duration,
'genre' => [
'id' => $track->genre->id,
'slug' => $track->genre->slug,
'name' => $track->genre->name
],
'track_type_id' => $track->track_type_id,
'covers' => [
'thumbnail' => $track->getCoverUrl(Image::THUMBNAIL),
'small' => $track->getCoverUrl(Image::SMALL),
'normal' => $track->getCoverUrl(Image::NORMAL)
]
];
} }
return Response::json($tracks, 200); return Response::json($tracks, 200);
} }
public function getIndex() {
$page = 1;
if (Input::has('page'))
$page = Input::get('page');
$query = Track::summary()->whereNotNull('published_at');
$this->applyFilters($query);
$totalCount = $query->count();
$query->take(30)->skip(30 * ($page - 1));
$tracks = [];
foreach ($query->get() as $track) {
$tracks[] = $this->mapPublicTrack($track);
}
return Response::json(["tracks" => $tracks, "current_page" => $page, "total_pages" => ceil($totalCount / 30)], 200);
}
public function getOwned() { public function getOwned() {
$query = Track::summary()->where('user_id', \Auth::user()->id); $query = Track::summary()->where('user_id', \Auth::user()->id);
@ -78,24 +71,7 @@
$query->whereNull('published_at'); $query->whereNull('published_at');
} }
if (Input::has('order')) { $this->applyFilters($query);
$order = \Input::get('order');
$parts = explode(',', $order);
$query->orderBy($parts[0], $parts[1]);
}
if (Input::has('in_album')) {
if (Input::get('in_album') == 'true')
$query->whereNotNull('album_id');
else
$query->whereNull('album_id');
}
if (Input::has('genres'))
$query->whereIn('genre_id', Input::get('genres'));
if (Input::has('types'))
$query->whereIn('track_type_id', Input::get('types'));
$dbTracks = $query->get(); $dbTracks = $query->get();
$tracks = []; $tracks = [];
@ -159,4 +135,75 @@
'album_id' => $track->album_id 'album_id' => $track->album_id
], 200); ], 200);
} }
private function mapPublicTrack($track) {
return [
'id' => $track->id,
'title' => $track->title,
'user' => [
'id' => $track->user->id,
'name' => $track->user->display_name,
'url' => $track->user->url
],
'url' => $track->url,
'slug' => $track->slug,
'is_vocal' => $track->is_vocal,
'is_explicit' => $track->is_explicit,
'is_downloadable' => $track->is_downloadable,
'is_published' => $track->isPublished(),
'published_at' => $track->published_at,
'duration' => $track->duration,
'genre' => $track->genre != null
?
[
'id' => $track->genre->id,
'slug' => $track->genre->slug,
'name' => $track->genre->name
] : null,
'track_type_id' => $track->track_type_id,
'covers' => [
'thumbnail' => $track->getCoverUrl(Image::THUMBNAIL),
'small' => $track->getCoverUrl(Image::SMALL),
'normal' => $track->getCoverUrl(Image::NORMAL)
]
];
}
private function applyFilters($query) {
if (Input::has('order')) {
$order = \Input::get('order');
$parts = explode(',', $order);
$query->orderBy($parts[0], $parts[1]);
}
if (Input::has('is_vocal')) {
$isVocal = \Input::get('is_vocal');
if ($isVocal == 'true')
$query->whereIsVocal(true);
else
$query->whereIsVocal(false);
}
if (Input::has('in_album')) {
if (Input::get('in_album') == 'true')
$query->whereNotNull('album_id');
else
$query->whereNull('album_id');
}
if (Input::has('genres'))
$query->whereIn('genre_id', Input::get('genres'));
if (Input::has('types'))
$query->whereIn('track_type_id', Input::get('types'));
if (Input::has('songs')) {
$query->join('show_song_track', 'tracks.id', '=', 'show_song_track.track_id')
->whereIn('show_song_track.show_song_id', Input::get('songs'));
$query->select('tracks.*');
}
return $query;
}
} }

View file

@ -40,6 +40,7 @@
Route::get('/playlists/show/{id}', 'Api\Web\PlaylistsController@getShow'); Route::get('/playlists/show/{id}', 'Api\Web\PlaylistsController@getShow');
Route::get('/tracks/recent', 'Api\Web\TracksController@getRecent'); Route::get('/tracks/recent', 'Api\Web\TracksController@getRecent');
Route::get('/tracks', 'Api\Web\TracksController@getIndex');
Route::get('/dashboard', 'Api\Web\DashboardController@getIndex'); Route::get('/dashboard', 'Api\Web\DashboardController@getIndex');

View file

@ -34,7 +34,6 @@
@else @else
<li ng-class="{selected: $state.includes('home')}"><a href="/">Home</a></li> <li ng-class="{selected: $state.includes('home')}"><a href="/">Home</a></li>
@endif @endif
<li><a href="/tracks">Now Playing</a></li>
<li><h3>Discover</h3></li> <li><h3>Discover</h3></li>
<li ng-class="{selected: $state.includes('tracks')}"><a href="/tracks">Music <i class="icon-music"></i></a></li> <li ng-class="{selected: $state.includes('tracks')}"><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')}"><a href="/albums">Albums <i class="icon-music"></i></a></li>

BIN
public/images/pattern1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

BIN
public/images/pattern10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

BIN
public/images/pattern2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

BIN
public/images/pattern3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

BIN
public/images/pattern4.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

BIN
public/images/pattern5.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

BIN
public/images/pattern6.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

BIN
public/images/pattern7.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
public/images/pattern8.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

BIN
public/images/pattern9.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View file

@ -69,7 +69,26 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date', 'ui.sortable'],
state.state 'tracks', state.state 'tracks',
url: '/tracks' url: '/tracks'
templateUrl: '/templates/tracks/index.html' templateUrl: '/templates/tracks/_layout.html'
abstract: true
state.state 'tracks.search',
templateUrl: '/templates/tracks/search.html'
controller: 'tracks'
state.state 'tracks.search.list',
url: '?filter&page'
templateUrl: '/templates/tracks/search-list.html'
controller: 'tracks-list'
state.state 'tracks.popular',
url: '/popular'
templateUrl: '/templates/tracks/search.html'
controller: 'tracks'
state.state 'tracks.random',
url: '/random'
templateUrl: '/templates/tracks/search.html'
controller: 'tracks' controller: 'tracks'
# Albums # Albums
@ -121,7 +140,7 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date', 'ui.sortable'],
if window.pfm.auth.isLogged if window.pfm.auth.isLogged
state.state 'home', state.state 'home',
url: '/' url: '/'
templateUrl: '/templates/dashboard.html' templateUrl: '/templates/dashboard/index.html'
controller: 'dashboard' controller: 'dashboard'
else else
state.state 'home', state.state 'home',

View file

@ -137,10 +137,6 @@ angular.module('ponyfm').controller "account-albums-edit", [
title: '' title: ''
description: '' description: ''
window.onbeforeunload = ->
return if !$scope.isDirty
"Are you sure you want to leave this page without saving your changes?"
$scope.$on '$locationChangeStart', (e) -> $scope.$on '$locationChangeStart', (e) ->
return if !$scope.isDirty return if !$scope.isDirty
e.preventDefault() if !confirm('Are you sure you want to leave this page without saving your changes?') e.preventDefault() if !confirm('Are you sure you want to leave this page without saving your changes?')

View file

@ -138,10 +138,6 @@ angular.module('ponyfm').controller "account-tracks-edit", [
$scope.$emit 'track-deleted' $scope.$emit 'track-deleted'
$state.transitionTo 'account-content.tracks' $state.transitionTo 'account-content.tracks'
window.onbeforeunload = ->
return if !$scope.isDirty
"Are you sure you want to leave this page without saving your changes?"
$scope.$on '$locationChangeStart', (e) -> $scope.$on '$locationChangeStart', (e) ->
return if !$scope.isDirty return if !$scope.isDirty
e.preventDefault() if !confirm('Are you sure you want to leave this page without saving your changes?') e.preventDefault() if !confirm('Are you sure you want to leave this page without saving your changes?')

View file

@ -1,14 +1,15 @@
window.pfm.preloaders['dashboard'] = [
'dashboard'
(dashboard) -> dashboard.refresh(true)
]
angular.module('ponyfm').controller "dashboard", [ angular.module('ponyfm').controller "dashboard", [
'$scope' '$scope', 'dashboard'
($scope) -> ($scope, dashboard) ->
$scope.recentTracks = null $scope.recentTracks = null
$scope.popularTracks = null $scope.popularTracks = null
$scope.refresh = () -> dashboard.refresh().done (res) ->
$.getJSON('/api/web/dashboard') $scope.recentTracks = res.recent_tracks
.done (res) -> $scope.$apply -> $scope.popularTracks = res.popular_tracks
$scope.recentTracks = res.recent_tracks
$scope.popularTracks = res.popular_tracks
$scope.refresh()
] ]

View file

@ -0,0 +1,19 @@
window.pfm.preloaders['tracks-list'] = [
'tracks', '$state'
(tracks, $state) ->
$.when.all [tracks.loadFilters().then(->
if !tracks.mainQuery.hasLoadedFilters
tracks.mainQuery.fromFilterString($state.params.filter)
if $state.params.page
tracks.mainQuery.setPage $state.params.page
tracks.mainQuery.fetch()
)]
]
angular.module('ponyfm').controller "tracks-list", [
'$scope', 'tracks', '$state',
($scope, tracks, $state) ->
tracks.mainQuery.fetch().done (searchResults) ->
$scope.tracks = searchResults.tracks
]

View file

@ -1,12 +1,33 @@
angular.module('ponyfm').controller "tracks", [ angular.module('ponyfm').controller "tracks", [
'$scope' '$scope', 'tracks', '$state'
($scope) -> ($scope, tracks, $state) ->
$scope.recentTracks = null $scope.recentTracks = null
$scope.query = tracks.mainQuery
$scope.filters = tracks.filters
$scope.refresh = () -> $scope.toggleListFilter = (filter, id) ->
$.getJSON('/api/web/tracks/recent') $scope.query.toggleListFilter filter, id
.done (res) -> $scope.$apply -> $state.transitionTo 'tracks.search.list', {filter: $scope.query.toFilterString()}
$scope.recentTracks = res
$scope.refresh() $scope.setFilter = (filter, value) ->
$scope.query.setFilter filter, value
$state.transitionTo 'tracks.search.list', {filter: $scope.query.toFilterString()}
$scope.setListFilter = (filter, id) ->
$scope.query.setListFilter filter, id
$state.transitionTo 'tracks.search.list', {filter: $scope.query.toFilterString()}
tracks.mainQuery.listen (searchResults) ->
$scope.tracks = searchResults.tracks
$scope.currentPage = parseInt(searchResults.current_page)
$scope.totalPages = parseInt(searchResults.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]
$scope.gotoPage = (page) ->
$state.transitionTo 'tracks.search.list', {filter: $state.params.filter, page: page}
] ]

View file

@ -0,0 +1,16 @@
angular.module('ponyfm').factory('dashboard', [
'$rootScope', '$http'
($rootScope, $http) ->
def = null
self =
refresh: (force) ->
force = force || false
return def if !force && def
def = new $.Deferred()
$http.get('/api/web/dashboard').success (dashboardContent) ->
def.resolve(dashboardContent)
def.promise()
self
])

View file

@ -5,19 +5,31 @@ angular.module('ponyfm').factory('taxonomies', [
self = self =
trackTypes: [] trackTypes: []
trackTypesWithTracks: []
licenses: [] licenses: []
genres: [] genres: []
genresWithTracks: []
showSongs: [] showSongs: []
showSongsWithTracks: []
refresh: () -> refresh: () ->
return def.promise() if def != null return def.promise() if def != null
def = new $.Deferred() def = new $.Deferred()
$http.get('/api/web/taxonomies/all') $http.get('/api/web/taxonomies/all')
.success (taxonomies) -> .success (taxonomies) ->
self.trackTypes.push t for t in taxonomies.track_types for t in taxonomies.track_types
self.trackTypes.push t
self.trackTypesWithTracks.push t if t.track_count > 0
for t in taxonomies.genres
self.genres.push t
self.genresWithTracks.push t if t.track_count > 0
for t in taxonomies.show_songs
self.showSongs.push t
self.showSongsWithTracks.push t if t.track_count > 0
self.licenses.push t for t in taxonomies.licenses self.licenses.push t for t in taxonomies.licenses
self.genres.push t for t in taxonomies.genres
self.showSongs.push t for t in taxonomies.show_songs
def.resolve self def.resolve self
def.promise() def.promise()

View file

@ -0,0 +1,182 @@
angular.module('ponyfm').factory('tracks', [
'$rootScope', '$http', 'taxonomies'
($rootScope, $http, taxonomies) ->
def = null
class Query
cachedDef: null
page: 1
listeners: []
constructor: (@availableFilters) ->
@filters = {}
@hasLoadedFilters = false
_.each @availableFilters, (filter, name) =>
if filter.type == 'single'
@filters[name] = _.find filter.values, (f) -> f.isDefault
else
@filters[name] = {title: 'Any', selectedArray: [], selectedObject: {}}
isIdSelected: (type, id) ->
@filters[type].selectedObject[id] != undefined
listen: (listener) ->
@listeners.push listener
@cachedDef.done listener if @cachedDef
setListFilter: (type, id) ->
@cachedDef = null
@page = 1
filterToAdd = _.find @availableFilters[type].values, (f) -> `f.id == id`
return if !filterToAdd
filter = @filters[type]
filter.selectedArray = [filterToAdd]
filter.selectedObject = {}
filter.selectedObject[id] = filterToAdd
filter.title = filterToAdd.title
toggleListFilter: (type, id) ->
@cachedDef = null
@page = 1
filter = @filters[type]
if filter.selectedObject[id]
delete filter.selectedObject[id]
filter.selectedArray.splice _.indexOf(filter.selectedArray, (f) -> f.id == id), 1
else
filterToAdd = _.find @availableFilters[type].values, (f) -> `f.id == id`
return if !filterToAdd
filter.selectedObject[id] = filterToAdd
filter.selectedArray.push filterToAdd
if filter.selectedArray.length == 0
filter.title = 'Any'
else if filter.selectedArray.length == 1
filter.title = filter.selectedArray[0].title
else
filter.title = filter.selectedArray.length + ' selected'
setPage: (page) ->
@page = page
@cachedDef = null
setFilter: (type, value) ->
@cachedDef = null
@page = 1
@filters[type] = value
toFilterString: ->
parts = []
_.each @availableFilters, (filter, name) =>
if filter.type == 'single'
return if @filters[name].query == ''
parts.push(name + '-' + @filters[name].query)
else
return if @filters[name].selectedArray.length == 0
parts.push(name + '-' + _.map(@filters[name].selectedArray, (f) -> f.id).join '-')
return parts.join '!'
fromFilterString: (str) ->
@hasLoadedFilters = true
return if !str
filters = str.split '!'
for filter in filters
parts = filter.split '-'
name = parts[0]
return if !@availableFilters[name]
if @availableFilters[name].type == 'single'
filterToSet = _.find @availableFilters[name].values, (f) -> f.query == parts[1]
filterToSet = _.find @availableFilters[name].values, (f) -> f.isDefault if filterToSet == null
else
@toggleListFilter name, id for id in _.rest parts, 1
fetch: () ->
return @cachedDef if @cachedDef
@cachedDef = new $.Deferred()
def = @cachedDef
query = '/api/web/tracks?'
parts = ['page=' + @page]
_.each @availableFilters, (filter, name) =>
if filter.type == 'single'
parts.push @filters[name].filter
else
queryName = filter.filterName
for item in @filters[name].selectedArray
parts.push queryName + "[]=" + item.id
query += parts.join '&'
$http.get(query).success (tracks) =>
@tracks = tracks
for listener in @listeners
listener tracks
def.resolve tracks
def.promise()
self =
filters: {}
createQuery: -> new Query self.filters
loadFilters: ->
return def if def
def = new $.Deferred()
self.filters.isVocal =
type: 'single'
values: [
{title: 'Either', query: '', isDefault: true, filter: ''},
{title: 'Yes', query: 'yes', isDefault: false, filter: 'is_vocal=true'},
{title: 'No', query: 'no', isDefault: false, filter: 'is_vocal=false'}
]
self.filters.sort =
type: 'single'
values: [
{title: 'Newest to Oldest', query: '', isDefault: true, filter: 'order=created_at,desc'},
{title: 'Oldest to Newest', query: 'created_at,asc', isDefault: true, filter: 'order=created_at,asc'}
]
self.filters.genres =
type: 'list'
values: []
filterName: 'genres'
self.filters.trackTypes =
type: 'list'
values: []
filterName: 'types'
self.filters.showSongs =
type: 'list'
values: []
filterName: 'songs'
taxonomies.refresh().done (taxes) ->
for genre in taxes.genresWithTracks
self.filters.genres.values.push
title: genre.name
id: genre.id
for type in taxes.trackTypesWithTracks
self.filters.trackTypes.values.push
title: type.title
id: type.id
for song in taxes.showSongsWithTracks
self.filters.showSongs.values.push
title: song.title
id: song.id
self.mainQuery = self.createQuery()
def.resolve self
def.promise()
self
])

View file

@ -1,5 +1,5 @@
@import 'base/bootstrap/bootstrap'; @import-once 'base/bootstrap/bootstrap';
@import 'mixins'; @import-once 'mixins';
@media (max-width: 1200px) { @media (max-width: 1200px) {
html { html {

View file

@ -1,5 +1,5 @@
@import 'base/bootstrap/bootstrap'; @import-once 'base/bootstrap/bootstrap';
@import 'mixins'; @import-once 'mixins';
.slide-down-enter, .slide-down-leave, .slide-down-enter, .slide-down-leave,
.slide-up-enter, .slide-up-leave, .slide-up-enter, .slide-up-leave,

View file

@ -1,10 +1,10 @@
@import 'base/bootstrap/bootstrap'; @import-once 'base/bootstrap/bootstrap';
@import 'base/font-awesome/font-awesome'; @import-once 'base/font-awesome/font-awesome';
@import 'mixins'; @import-once 'mixins';
@import 'layout'; @import-once 'layout';
@import 'home'; @import-once 'home';
@import 'account-content'; @import-once 'account-content';
@import 'components'; @import-once 'components';
@import 'forms'; @import-once 'forms';
@import 'tracks'; @import-once 'tracks';
@import 'animations'; @import-once 'animations';

View file

@ -1,5 +1,5 @@
@import 'base/bootstrap/bootstrap'; @import-once 'base/bootstrap/bootstrap';
@import 'mixins'; @import-once 'mixins';
.fade-hide, .fade-show { .fade-hide, .fade-show {
.transition(all cubic-bezier(0.250, 0.460, 0.450, 0.940) 350ms); .transition(all cubic-bezier(0.250, 0.460, 0.450, 0.940) 350ms);
@ -51,6 +51,25 @@ html .dropdown-menu {
padding: 3px 10px; padding: 3px 10px;
font-size: 8pt; font-size: 8pt;
} }
> .dont-close {
float: right;
display: block;
width: 24px;
text-align: center;
padding: 3px 3px;
}
> .dont-close + a {
margin-right: 30px;
clear: none;
}
&.selected {
> .dont-close i:before {
content: "\f068";
}
}
} }
} }
@ -79,12 +98,11 @@ html .dropdown-menu {
li.selected { li.selected {
a { a {
#gradient>.vertical(@green, darken(@green, 5%)); background: @green;
color: #fff; color: #fff;
font-weight: bold;
&:hover { &:hover {
#gradient>.vertical(fadeout(@green, 20%), fadeout(darken(@green, 5%), 20%)); background: fadeout(@green, 20%);
} }
} }
} }
@ -170,4 +188,25 @@ html .dropdown-menu {
.btn { .btn {
.border-radius(0px); .border-radius(0px);
} }
}
html {
.pagination {
border: none;
&.pagination-right {
float: right;
}
li a {
.border-radius(0px);
border: none;
padding: 1px 10px;
}
li.active a {
background: #444;
color: #ddd;
}
}
} }

View file

@ -1,5 +1,5 @@
@import 'base/bootstrap/bootstrap'; @import-once 'base/bootstrap/bootstrap';
@import 'mixins'; @import-once 'mixins';
html, body { html, body {
height: 100%; height: 100%;
@ -13,6 +13,7 @@ html body {
.background-color { .background-color {
background: rgba(42, 42, 42, 1); background: rgba(42, 42, 42, 1);
background-image: url('/images/pattern4.jpg');
position: fixed; position: fixed;
left: 0px; left: 0px;
top: 0px; top: 0px;
@ -32,7 +33,7 @@ ui-view {
content: ' '; content: ' ';
top: 0px; top: 0px;
left: 0px; left: 0px;
z-index: 1000; z-index: 999;
width: 0px; width: 0px;
height: 0px; height: 0px;
position: absolute; position: absolute;
@ -53,6 +54,7 @@ ui-view {
header { header {
.clearfix(); .clearfix();
.box-shadow(0px 1px 8px rgba(0, 0, 0, 0.2));
background: #222; background: #222;
> div { > div {

View file

@ -1,5 +1,5 @@
@import 'base/bootstrap/bootstrap'; @import-once 'base/bootstrap/bootstrap';
@import 'mixins'; @import-once 'mixins';
.dashboard { .dashboard {
section { section {
@ -24,25 +24,72 @@
padding: 0px; padding: 0px;
list-style: none; list-style: none;
&.two-column {
li {
.box-sizing(border-box);
width: 50%;
float: left;
margin: 0px;
padding: 5px;
}
}
li { li {
overflow: hidden; overflow: hidden;
margin: 5px 0px; margin: 10px 0px;
padding: 0px; padding: 0px;
padding-bottom: 5px;
img { &:hover {
.img-polaroid(); background: #eee;
padding: 3px;
padding: 0px; .image {
display: block; .play-button {
display: block;
}
}
}
.image {
height: 40px; height: 40px;
width: 40px; width: 40px;
float: left; float: left;
position: relative;
.play-button {
.transition(background 250ms ease-out);
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
line-height: 38px;
text-align: center;
font-size: 12pt;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, .4);
display: none;
&:hover {
background: rgba(0, 0, 0, .8);
}
}
img {
.img-polaroid();
padding: 3px;
padding: 0px;
display: block;
height: 38px;
width: 38px;
}
} }
.icons { .icons {
float: right; float: right;
font-size: 13px; font-size: 13px;
margin-right: 2px;
a, span { a, span {
display: block; display: block;
@ -56,7 +103,11 @@
} }
.info { .info {
margin-left: 51px; text-decoration: none;
display: block;
padding: 3px 0px;
margin-left: 40px;
padding-left: 5px;
line-height: normal; line-height: normal;
.title { .title {
@ -71,10 +122,6 @@
display: block; display: block;
color: #777; color: #777;
font-size: 8pt; font-size: 8pt;
a {
color: fadeOut(@blue, 10%);
}
} }
} }
} }

View file

@ -10,18 +10,23 @@
</li> </li>
</ul> </ul>
<div class="stretch-to-bottom"> <div class="stretch-to-bottom">
<div class="form-row" ng-class="{'has-error': errors.display_name != null}"> <div class="row-fluid">
<label for="sync_names" class="strong"><input ng-disabled="isSaving" ng-change="touchModel();" id="sync_names" type="checkbox" ng-model="settings.sync_names" /> Sync my MLP Forums display name with Pony.fm</label> <div class="form-row span6" ng-class="{'has-error': errors.display_name != null}">
<input type="text" ng-disabled="isSaving" ng-change="touchModel()" ng-show="!settings.sync_names" placeholder="Display Name" id="display_name" ng-model="settings.display_name" /> <label for="sync_names" class="strong"><input ng-disabled="isSaving" ng-change="touchModel();" id="sync_names" type="checkbox" ng-model="settings.sync_names" /> Sync my MLP Forums display name with Pony.fm</label>
<div ng-show="settings.sync_names" class="alert alert-info">Your current MLP Forums display name is <strong>{{settings.mlpforums_name}}</strong></div> <input type="text" ng-disabled="isSaving" ng-change="touchModel()" ng-show="!settings.sync_names" placeholder="Display Name" id="display_name" ng-model="settings.display_name" />
<div class="error">{{errors.display_name}}</div> <div ng-show="settings.sync_names" class="alert alert-info">Your current MLP Forums display name is <strong>{{settings.mlpforums_name}}</strong></div>
</div> <div class="error">{{errors.display_name}}</div>
<div class="form-row" ng-class="{'has-error': errors.bio != null}"> </div>
<label class="strong" for="bio">Bio</label> <div class="form-row span6">
<textarea id="bio" placeholder="bio (optional)" ng-model="settings.bio" ng-disabled="isLoading" ng-change="touchModel()"></textarea> <label for="can_see_explicit_content"><input ng-change="touchModel()" ng-disabled="isLoading" id="can_see_explicit_content" type="checkbox" ng-model="settings.can_see_explicit_content" /> Can See Explicit Content</label>
<div class="error">{{errors.description}}</div> </div>
</div> </div>
<div class="row-fluid"> <div class="row-fluid">
<div class="form-row span6" ng-class="{'has-error': errors.bio != null}">
<label class="strong" for="bio">Bio</label>
<textarea id="bio" placeholder="bio (optional)" ng-model="settings.bio" ng-disabled="isLoading" ng-change="touchModel()"></textarea>
<div class="error">{{errors.description}}</div>
</div>
<div class="form-row span6" ng-class="{'has-error': errors.avatar != null || errors.gravatar != null}"> <div class="form-row span6" ng-class="{'has-error': errors.avatar != null || errors.gravatar != null}">
<label for="uses_gravatar"> <label for="uses_gravatar">
<input ng-change="touchModel()" ng-disabled="isLoading" id="uses_gravatar" type="checkbox" ng-model="settings.uses_gravatar" /> Use Gravatar <input ng-change="touchModel()" ng-disabled="isLoading" id="uses_gravatar" type="checkbox" ng-model="settings.uses_gravatar" /> Use Gravatar
@ -33,9 +38,6 @@
<div class="error" ng-show="errors.avatar != null">{{errors.avatar}}</div> <div class="error" ng-show="errors.avatar != null">{{errors.avatar}}</div>
<div class="error" ng-show="errors.gravatar != null">{{errors.gravatar}}</div> <div class="error" ng-show="errors.gravatar != null">{{errors.gravatar}}</div>
</div> </div>
<div class="form-row span6">
<label for="can_see_explicit_content"><input ng-change="touchModel()" ng-disabled="isLoading" id="can_see_explicit_content" type="checkbox" ng-model="settings.can_see_explicit_content" /> Can See Explicit Content</label>
</div>
</div> </div>
</div> </div>
</form> </form>

View file

@ -1,53 +0,0 @@
<div class="dashboard">
<section class="recent-tracks">
<div>
<h1>
<a href="#"><i class="icon-music"></i> see more</a>
The Newest Tunes
</h1>
<ul class="tracks-listing stretch-to-bottom">
<li ng-repeat="track in recentTracks">
<img ng-src="{{track.covers.thumbnail}}" />
<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>
<div class="info">
<a href="{{track.url}}" class="title">{{track.title}}</a>
<span class="metadata">
by: <a href="{{track.user.url}}">{{track.user.name}}</a> /
<a href="#">{{track.genre.name}}</a> /
{{track.published_at.date | momentFromNow}}
</span>
</a>
</div>
</li>
</ul>
</div>
</section>
<section class="popular">
<div>
<h1>
<a href="#"><i class="icon-star"></i> see more</a>
What's Popular Today
</h1>
<ul class="tracks-listing stretch-to-bottom">
<li ng-repeat="track in recentTracks">
<img ng-src="{{track.covers.thumbnail}}" />
<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>
<div class="info">
<a href="{{track.url}}" class="title">{{track.title}}</a>
<span class="metadata">
by: <a href="{{track.user.url}}">{{track.user.name}}</a> /
<a href="#">{{track.genre.name}}</a> /
{{track.published_at.date | momentFromNow}}
</span>
</div>
</li>
</ul>
</div>
</section>
</div>

View file

@ -7,19 +7,22 @@
</h1> </h1>
<ul class="tracks-listing stretch-to-bottom"> <ul class="tracks-listing stretch-to-bottom">
<li ng-repeat="track in recentTracks"> <li ng-repeat="track in recentTracks">
<img ng-src="{{track.covers.thumbnail}}" /> <div class="image">
<a href="#" class="play-button"><i class="icon-play"></i></a>
<img ng-src="{{track.covers.thumbnail}}" />
</div>
<div class="icons"> <div class="icons">
<span><i ng-class="{'icon-microphone-off': !track.is_vocal, 'icon-microphone': track.is_vocal}"></i></span> <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> <a href="#"><i class="icon-star-empty"></i></a>
</div> </div>
<div class="info"> <a class="info" href="{{track.url}}">
<a href="{{track.url}}" class="title">{{track.title}}</a> <span class="title">{{track.title}}</span>
<span class="metadata"> <span class="metadata">
by: <a href="{{track.user.url}}">{{track.user.name}}</a> / by: <span class="artist">{{track.user.name}}</span> /
<a href="#">{{track.genre.name}}</a> / <span class="genre">{{track.genre.name}}</span> /
{{track.published_at.date | momentFromNow}} {{track.published_at.date | momentFromNow}}
</span> </span>
</div> </a>
</li> </li>
</ul> </ul>
</div> </div>
@ -32,19 +35,22 @@
</h1> </h1>
<ul class="tracks-listing stretch-to-bottom"> <ul class="tracks-listing stretch-to-bottom">
<li ng-repeat="track in recentTracks"> <li ng-repeat="track in recentTracks">
<img ng-src="{{track.covers.thumbnail}}" /> <div class="image">
<a href="#" class="play-button"><i class="icon-play"></i></a>
<img ng-src="{{track.covers.thumbnail}}" />
</div>
<div class="icons"> <div class="icons">
<span><i ng-class="{'icon-microphone-off': !track.is_vocal, 'icon-microphone': track.is_vocal}"></i></span> <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> <a href="#"><i class="icon-star-empty"></i></a>
</div> </div>
<div class="info"> <a class="info" href="{{track.url}}">
<a href="{{track.url}}" class="title">{{track.title}}</a> <span class="title">{{track.title}}</span>
<span class="metadata"> <span class="metadata">
by: <a href="{{track.user.url}}">{{track.user.name}}</a> / by: <span class="artist">{{track.user.name}}</span> /
<a href="#">{{track.genre.name}}</a> / <span class="genre">{{track.genre.name}}</span> /
{{track.published_at.date | momentFromNow}} {{track.published_at.date | momentFromNow}}
</span> </span>
</div> </a>
</li> </li>
</ul> </ul>
</div> </div>

View file

@ -0,0 +1,9 @@
<div>
<ul class="tabs">
<li ng-class="{active: $state.includes('tracks.search')}"><a href="/tracks">Search</a></li>
<li ng-class="{active: $state.includes('tracks.popular')}"><a href="/tracks/popular">Popular</a></li>
<li ng-class="{active: $state.includes('tracks.random')}"><a href="/tracks/random">Random</a></li>
</ul>
<ui-view></ui-view>
</div>

View file

@ -0,0 +1,20 @@
<ul class="tracks-listing two-column stretch-to-bottom">
<li ng-repeat="track in 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>

View file

@ -0,0 +1,67 @@
<ul class="dropdowns">
<li class="dropdown">
<a class="dropdown-toggle btn">
Type: <strong>{{query.filters.trackTypes.title}}</strong>
</a>
<ul class="dropdown-menu">
<li ng-repeat="type in filters.trackTypes.values" ng-class="{selected: query.isIdSelected('trackTypes', type.id)}">
<a class="dont-close" pfm-eat-click href="#" ng-click="toggleListFilter('trackTypes', type.id); $event.stopPropagation();"><i class="icon-plus"></i></a>
<a pfm-eat-click href="#" ng-click="setListFilter('trackTypes', type.id);">{{type.title}}</a>
</li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle btn">
Show Songs: <strong>{{query.filters.showSongs.title}}</strong>
</a>
<ul class="dropdown-menu">
<li ng-repeat="song in filters.showSongs.values" ng-class="{selected: query.isIdSelected('showSongs', song.id)}">
<a class="dont-close" pfm-eat-click href="#" ng-click="toggleListFilter('showSongs', song.id); $event.stopPropagation();"><i class="icon-plus"></i></a>
<a pfm-eat-click href="#" ng-click="setListFilter('showSongs', song.id);">{{song.title}}</a>
</li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle btn">
Genre: <strong>{{query.filters.genres.title}}</strong>
</a>
<ul class="dropdown-menu">
<li ng-repeat="genre in filters.genres.values" ng-class="{selected: query.isIdSelected('genres', genre.id)}">
<a class="dont-close" pfm-eat-click href="#" ng-click="toggleListFilter('genres', genre.id); $event.stopPropagation();"><i class="icon-plus"></i></a>
<a pfm-eat-click href="#" ng-click="setListFilter('genres', genre.id);">{{genre.title}}</a>
</li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle btn">
Is Vocal: <strong>{{query.filters.isVocal.title}}</strong>
</a>
<ul class="dropdown-menu">
<li ng-repeat="item in filters.isVocal.values" ng-class="{selected: item == query.filters.isVocal}">
<a pfm-eat-click href="#" ng-click="setFilter('isVocal', item);">{{item.title}}</a>
</li>
</ul>
</li>
<li class="dropdown">
<a class="dropdown-toggle btn">
Order: <strong>{{query.filters.sort.title}}</strong>
</a>
<ul class="dropdown-menu">
<li ng-repeat="filter in filters.sort.values" ng-class="{selected: filter == query.filters.sort}">
<a pfm-eat-click href="#" ng-click="setFilter('sort', filter)">{{filter.title}}</a>
</li>
</ul>
</li>
</ul>
<div class="pagination" ng-show="totalPages > 0">
<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>