Working on albums

This commit is contained in:
nelsonlaquet 2013-07-28 02:09:10 -05:00
parent f2b1bc82cf
commit f093d7a570
21 changed files with 330 additions and 25 deletions

View file

@ -0,0 +1,66 @@
<?php
namespace Api\Web;
use Commands\DeleteTrackCommand;
use Commands\EditTrackCommand;
use Commands\UploadTrackCommand;
use Cover;
use Entities\Album;
use Entities\Image;
use Entities\Track;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Response;
class AlbumsController extends \ApiControllerBase {
public function getOwned() {
$query = Album::summary()->where('user_id', \Auth::user()->id);
return Response::json($query->get(), 200);
}
public function getEdit($id) {
$track = Track::with('showSongs')->find($id);
if (!$track)
return $this->notFound('Track ' . $id . ' not found!');
if ($track->user_id != Auth::user()->id)
return $this->notAuthorized();
$showSongs = [];
foreach ($track->showSongs as $showSong) {
$showSongs[] = ['id' => $showSong->id, 'title' => $showSong->title];
}
return Response::json([
'id' => $track->id,
'title' => $track->title,
'user_id' => $track->user_id,
'slug' => $track->slug,
'is_vocal' => (bool)$track->is_vocal,
'is_explicit' => (bool)$track->is_explicit,
'is_downloadable' => !$track->isPublished() ? true : (bool)$track->is_downloadable,
'is_published' => $track->published_at != null,
'created_at' => $track->created_at,
'published_at' => $track->published_at,
'duration' => $track->duration,
'genre_id' => $track->genre_id,
'track_type_id' => $track->track_type_id,
'license_id' => $track->license_id != null ? $track->license_id : 3,
'description' => $track->description,
'lyrics' => $track->lyrics,
'released_at' => $track->released_at,
'cover_url' => $track->hasCover() ? $track->getCoverUrl(Image::NORMAL) : null,
'real_cover_url' => $track->getCoverUrl(Image::NORMAL),
'show_songs' => $showSongs
], 200);
}
public function postDelete($id) {
return $this->execute(new DeleteTrackCommand($id));
}
public function putEdit($id) {
return $this->execute(new EditTrackCommand($id, Input::all()));
}
}

View file

@ -39,6 +39,7 @@ class CreateTracksTable extends Migration {
$table->text('lyrics')->nullable(); $table->text('lyrics')->nullable();
$table->boolean('is_vocal'); $table->boolean('is_vocal');
$table->boolean('is_explicit'); $table->boolean('is_explicit');
$table->integer('cover_id')->unsigned()->nullable();
$table->boolean('is_downloadable'); $table->boolean('is_downloadable');
$table->float('duration')->unsigned(); $table->float('duration')->unsigned();

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
class CreateAlbums extends Migration {
public function up() {
Schema::create('albums', function($table) {
$table->increments('id');
$table->integer('user_id')->unsigned();
$table->string('title')->index();
$table->string('slug')->index();
$table->text('description');
$table->integer('cover_id')->unsigned()->nullable();
$table->timestamps();
$table->timestamp('deleted_at')->nullable()->index();
$table->foreign('cover_id')->references('id')->on('images');
$table->foreign('user_id')->references('id')->on('users');
});
Schema::table('tracks', function($table) {
$table->integer('album_id')->unsigned()->nullable();
$table->integer('track_number')->unsigned()->nullable();
$table->foreign('album_id')->references('id')->on('albums');
});
}
public function down() {
Schema::table('tracks', function($table) {
$table->dropForeign('tracks_album_id_foreign');
$table->dropColumn('album_id');
$table->dropColumn('track_number');
});
Schema::drop('albums');
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Entities;
use Cover;
use Whoops\Example\Exception;
class Album extends \Eloquent {
protected $softDelete = true;
public static function summary() {
return self::select('id', 'title', 'user_id', 'slug', 'created_at');
}
protected $table = 'albums';
public function user() {
return $this->belongsTo('Entities\User');
}
public function cover() {
return $this->belongsTo('Entities\Image');
}
public function hasCover() {
return $this->cover_id != null;
}
public function getCoverUrl($type = Image::NORMAL) {
if (!$this->hasCover())
return $this->user->getAvatarUrl($type);
return $this->cover->getUrl($type);
}
public function getDirectory() {
$dir = (string) ( floor( $this->id / 100 ) * 100 );
return \Config::get('app.files_directory') . '/tracks/' . $dir;
}
public function getDates() {
return ['created_at', 'deleted_at', 'published_at'];
}
public function getFilenameFor($format) {
if (!isset(Track::$Formats[$format]))
throw new Exception("$format is not a valid format!");
$format = Track::$Formats[$format];
return "{$this->id}.{$format['extension']}.zip";
}
}

View file

@ -45,7 +45,7 @@
return $this->published_at != null && $this->deleted_at == null; return $this->published_at != null && $this->deleted_at == null;
} }
public function getCoverUrl($type = Cover::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

@ -40,8 +40,12 @@
Route::group(['before' => 'auth'], function() { Route::group(['before' => 'auth'], function() {
Route::get('/images/owned', 'Api\Web\ImagesController@getOwned'); Route::get('/images/owned', 'Api\Web\ImagesController@getOwned');
Route::get('/tracks/owned', 'Api\Web\TracksController@getOwned'); Route::get('/tracks/owned', 'Api\Web\TracksController@getOwned');
Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit'); Route::get('/tracks/edit/{id}', 'Api\Web\TracksController@getEdit');
Route::get('/albums/owned', 'Api\Web\AlbumsController@getOwned');
Route::get('/albums/edit/{id}', 'Api\Web\AlbumsController@getEdit');
}); });
Route::group(['before' => 'csrf'], function(){ Route::group(['before' => 'csrf'], function(){
@ -52,14 +56,16 @@
Route::group(['prefix' => 'account'], function() { Route::group(['prefix' => 'account'], function() {
Route::group(['before' => 'auth'], function(){ Route::group(['before' => 'auth'], function(){
Route::get('/favourites', 'FavouritesController@getTracks'); Route::get('/favourites/tracks', 'FavouritesController@getTracks');
Route::get('/favourites/albums', 'FavouritesController@getAlbums'); Route::get('/favourites/albums', 'FavouritesController@getAlbums');
Route::get('/favourites/playlists', 'FavouritesController@getPlaylists'); Route::get('/favourites/playlists', 'FavouritesController@getPlaylists');
Route::get('/content/tracks', 'ContentController@getTracks'); Route::get('/tracks', 'ContentController@getTracks');
Route::get('/content/tracks/{id}', 'ContentController@getTracks'); Route::get('/tracks/edit/{id}', 'ContentController@getTracks');
Route::get('/content/albums', 'ContentController@getAlbums'); Route::get('/albums', 'ContentController@getAlbums');
Route::get('/content/playlists', 'ContentController@getPlaylists'); Route::get('/albums/edit/{id}', 'ContentController@getAlbums');
Route::get('/albums/create', 'ContentController@getAlbums');
Route::get('/playlists', 'ContentController@getPlaylists');
Route::get('/', 'AccountController@getIndex'); Route::get('/', 'AccountController@getIndex');
}); });

View file

@ -43,7 +43,7 @@
<h3>Account</h3> <h3>Account</h3>
</li> </li>
<li ng-class="{selected: $state.includes('account-favourites')}"><a href="/account/favourites">Favourites</a></li> <li ng-class="{selected: $state.includes('account-favourites')}"><a href="/account/favourites">Favourites</a></li>
<li ng-class="{selected: $state.includes('account-content')}"><a href="/account/content/tracks">Your Content</a></li> <li ng-class="{selected: $state.includes('account-content')}"><a href="/account/tracks">Your Content</a></li>
<li ng-class="{selected: isActive('/account')}"><a href="/account">Settings</a></li> <li ng-class="{selected: isActive('/account')}"><a href="/account">Settings</a></li>
@endif @endif

View file

@ -10,21 +10,32 @@ angular.module 'ponyfm', ['ui.bootstrap', 'ui.state', 'ui.date'], [
controller: 'account-settings' controller: 'account-settings'
state.state 'account-content', state.state 'account-content',
url: '/account/content' url: '/account'
abstract: true abstract: true
templateUrl: '/templates/account/content/_layout.html' templateUrl: '/templates/account/content/_layout.html'
state.state 'account-content.tracks', state.state 'account-content.tracks',
url: '/tracks' url: '/tracks'
templateUrl: '/templates/account/content/tracks.html' templateUrl: '/templates/account/content/tracks.html'
controller: 'account-content-tracks' controller: 'account-tracks'
state.state 'account-content.tracks.edit', state.state 'account-content.tracks.edit',
url: '/:track_id' url: '/edit/:track_id'
state.state 'account-content.albums', state.state 'account-content.albums',
url: '/albums' url: '/albums'
templateUrl: '/templates/account/content/albums.html' templateUrl: '/templates/account/content/albums.html'
controller: 'account-albums'
state.state 'account-content.albums.create',
url: '/create'
templateUrl: '/templates/account/content/album.html'
controller: 'account-albums-edit'
state.state 'account-content.albums.edit',
url: '/edit/:album_id'
templateUrl: '/templates/account/content/album.html'
controller: 'account-albums-edit'
state.state 'account-content.playlists', state.state 'account-content.playlists',
url: '/playlists' url: '/playlists'

View file

@ -0,0 +1,17 @@
angular.module('ponyfm').controller "account-albums-edit", [
'$scope', '$state', 'taxonomies', '$dialog', 'lightbox'
($scope, $state, taxonomies, $dialog, lightbox) ->
$scope.isNew = $state.params.album_id == null
$scope.data.isEditorOpen = true
$scope.errors = {}
$scope.isDirty = false
$scope.touchModel = -> $scope.isDirty = true
if $scope.isNew
$scope.album =
title: ''
description: ''
$scope.$on '$destroy', -> $scope.data.isEditorOpen = false
]

View file

@ -0,0 +1,19 @@
angular.module('ponyfm').controller "account-albums", [
'$scope', '$state', 'taxonomies', '$dialog', 'lightbox'
($scope, $state, taxonomies, $dialog, lightbox) ->
refreshList = () ->
$.getJSON('/api/web/albums/owned')
.done (albums) ->
$scope.albums = albums
refreshList()
$scope.data =
isEditorOpen: false
selectedAlbum: null
$scope.$on '$stateChangeSuccess', () ->
if $state.params.album_id
selectAlbum albumsDb[$state.params.album_id]
else
selectAlbum null
]

View file

@ -1,4 +1,4 @@
angular.module('ponyfm').controller "account-content-tracks", [ angular.module('ponyfm').controller "account-tracks", [
'$scope', '$state', 'taxonomies', '$dialog', 'lightbox' '$scope', '$state', 'taxonomies', '$dialog', 'lightbox'
($scope, $state, taxonomies, $dialog, lightbox) -> ($scope, $state, taxonomies, $dialog, lightbox) ->
$('#coverPreview').load () -> $('#coverPreview').load () ->

View file

@ -0,0 +1,9 @@
angular.module('ponyfm').directive 'pfmImageUpload',
restrict: 'E'
scope:
setUploadedImage: '&'
setGalleryImage: '&'
controller: [
'upload'
(upload) -> (scope) ->
]

View file

@ -47,7 +47,7 @@
} }
} }
.account-tracks-listing { .account-tracks-listing, .account-albums-listing {
overflow-y: auto; overflow-y: auto;
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
@ -60,6 +60,15 @@
margin: 0px; margin: 0px;
line-height: normal; line-height: normal;
&.empty {
.alert();
float: none !important;
width: auto !important;
display: block;
padding: 5px;
font-size: 9pt;
}
&.is-published a { &.is-published a {
background: transparent; background: transparent;
} }
@ -329,12 +338,15 @@
} }
&.closed { &.closed {
.account-tracks-listing { .account-tracks-listing, .account-albums-listing {
.clearfix(); .clearfix();
li { li {
float: left; float: left;
width: 25%; width: 25%;
&.empty {
}
} }
} }
} }

View file

@ -3,5 +3,5 @@
@import 'mixins'; @import 'mixins';
@import 'layout'; @import 'layout';
@import 'home'; @import 'home';
@import 'account-tracks'; @import 'account-content';
@import 'components'; @import 'components';

View file

@ -1,8 +1,8 @@
<div> <div>
<ul class="tabs"> <ul class="tabs">
<li ng-class="{active: $state.includes('account-content.tracks')}"><a href="/account/content/tracks">Tracks</a></li> <li ng-class="{active: $state.includes('account-content.tracks')}"><a href="/account/tracks">Tracks</a></li>
<li ng-class="{active: $state.includes('account-content.albums')}"><a href="/account/content/albums">Albums</a></li> <li ng-class="{active: $state.includes('account-content.albums')}"><a href="/account/albums">Albums</a></li>
<li ng-class="{active: $state.includes('account-content.playlists')}"><a href="/account/content/playlists">Playlists</a></li> <li ng-class="{active: $state.includes('account-content.playlists')}"><a href="/account/playlists">Playlists</a></li>
</ul> </ul>
<div ui-view></div> <div ui-view></div>

View file

@ -0,0 +1,46 @@
<form novalidate ng-submit="updateAlbum()">
<ul class="toolbar">
<li>
<button type="submit" class="btn" ng-class="{disabled: !isDirty || isSaving, 'btn-primary': isDirty}">
Save Changes
<i ng-show="isSaving" class="icon-cog icon-spin icon-large"></i>
</button>
</li>
<li class="delete"><a ng-class="{disabled: isSaving}" class="btn btn-danger" href="#" ng-click="deleteAlbum(selectedAlbum)" pfm-eat-click>Delete Album</a></li>
</ul>
<div class="strech-to-bottom">
<div class="form-row" ng-class="{'has-error': errors.title != null}">
<label for="title" class="strong">Title:</label>
<input ng-disabled="isSaving" ng-change="touchModel()" placeholder="Album Title" type="text" id="title" ng-model="album.title" />
<div class="error">{{errors.title}}</div>
</div>
<div class="form-row" ng-class="{'has-error': errors.description != null}">
<label for="description" class="strong">Description:</label>
<textarea ng-disabled="isSaving" ng-change="touchModel()" placeholder="Description (optional)" id="description" ng-model="album.description"></textarea>
<div class="error">{{errors.description}}</div>
</div>
<div class="form-row" ng-class="{'has-error': errors.cover != null}">
<label class="strong">Album Cover: </label>
<div class="cover-upload">
<div class="preview" ng-class="{canOpen: isCoverLoaded}" ng-click="previewCover()"><img id="coverPreview" ng-show="isCoverLoaded" /></div>
<p>
Image must be a PNG that is at least 350x350. <br />
<input type="file" id="coverImage" onchange="angular.element(this).scope().setCoverImage(this)" />
</p>
<div class="btn-group">
<a href="#" pfm-popup="image-selector" class="btn btn-small"><i class="icon-picture"></i> Gallery</a>
<a href="#" pfm-eat-click ng-click="uploadAlbumCover()" class="btn btn-info btn-small"><i class="icon-upload"></i> Upload</a>
<a href="#" pfm-eat-click ng-click="clearAlbumCover()" class="btn btn-danger btn-small" ng-show="album.cover || album.cover_id"><i class="icon-remove"></i></a>
</div>
<div id="image-selector" class="pfm-popup image-selector" ng-controller="account-image-select">
<ul>
<li ng-repeat="image in images" ng-click="selectGalleryImage(image)">
<img ng-src="{{image.url}}" />
</li>
</ul>
</div>
<div class="error">{{errors.cover}}</div>
</div>
</div>
</div>
</form>

View file

@ -1 +1,27 @@
<h1>Your Albums</h1> <ul class="dropdowns">
<li class="dropdown">
<a class="btn" href="/account/albums/create">
<i class="icon-plus"></i> Create Album
</a>
</li>
</ul>
<div class="two-pane-view" ng-class="{open: data.isEditorOpen, closed: !data.isEditorOpen}">
<div class="list">
<ul class="account-albums-listing strech-to-bottom">
<li ng-repeat="album in albums" ng-class="{selected: album.id == data.selectedAlbum.id">
<a href="/account/albums/edit/{{album.id}}">
<img class="image" ng-src="{{album.cover_url}}" />
<span class="title">{{albums.title}}</span>
<span class="published">{{albums.created_at | pfmdate:'MM/dd/yyyy'}}</span>
</a>
</li>
<li ng-show="!albums.length" class="empty">
No albums found...
</li>
</ul>
</div>
<div class="editor" ui-view>
</div>
</div>

View file

@ -45,8 +45,8 @@
<div class="list"> <div class="list">
<ul class="account-tracks-listing strech-to-bottom"> <ul class="account-tracks-listing strech-to-bottom">
<li ng-repeat="track in tracks" ng-class="{selected: track.id == selectedTrack.id, 'is-published': track.is_published}"> <li ng-repeat="track in tracks" ng-class="{selected: track.id == selectedTrack.id, 'is-published': track.is_published}">
<a href="/account/content/tracks/{{track.id}}"> <a href="/account/tracks/edit/{{track.id}}">
<img class="image" src="{{track.cover_url}}" /> <img class="image" ng-src="{{track.cover_url}}" />
<span class="title">{{track.title}}</span> <span class="title">{{track.title}}</span>
<span class="published">{{track.created_at | pfmdate:'MM/dd/yyyy'}}</span> <span class="published">{{track.created_at | pfmdate:'MM/dd/yyyy'}}</span>
</a> </a>
@ -135,7 +135,7 @@
<div id="image-selector" class="pfm-popup image-selector" ng-controller="account-image-select"> <div id="image-selector" class="pfm-popup image-selector" ng-controller="account-image-select">
<ul> <ul>
<li ng-repeat="image in images" ng-click="selectGalleryImage(image)"> <li ng-repeat="image in images" ng-click="selectGalleryImage(image)">
<img src="{{image.url}}" /> <img ng-src="{{image.url}}" />
</li> </li>
</ul> </ul>
</div> </div>

2
vendor/autoload.php vendored
View file

@ -4,4 +4,4 @@
require_once __DIR__ . '/composer' . '/autoload_real.php'; require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInita1fb7abacafe9eba7b46e2d4ded6e3a5::getLoader(); return ComposerAutoloaderInit68362e9063d9f6e829919d3debf84f51::getLoader();

View file

@ -130,6 +130,7 @@ return array(
'Commands\\EditTrackCommand' => $baseDir . '/app/models/Commands/EditTrackCommand.php', 'Commands\\EditTrackCommand' => $baseDir . '/app/models/Commands/EditTrackCommand.php',
'Commands\\UploadTrackCommand' => $baseDir . '/app/models/Commands/UploadTrackCommand.php', 'Commands\\UploadTrackCommand' => $baseDir . '/app/models/Commands/UploadTrackCommand.php',
'ContentController' => $baseDir . '/app/controllers/ContentController.php', 'ContentController' => $baseDir . '/app/controllers/ContentController.php',
'CreateAlbums' => $baseDir . '/app/database/migrations/2013_07_28_060804_create_albums.php',
'CreateImagesTable' => $baseDir . '/app/database/migrations/2013_07_26_230827_create_images_table.php', 'CreateImagesTable' => $baseDir . '/app/database/migrations/2013_07_26_230827_create_images_table.php',
'CreateSongsTable' => $baseDir . '/app/database/migrations/2013_07_28_034328_create_songs_table.php', 'CreateSongsTable' => $baseDir . '/app/database/migrations/2013_07_28_034328_create_songs_table.php',
'CreateTracksTable' => $baseDir . '/app/database/migrations/2013_06_27_015259_create_tracks_table.php', 'CreateTracksTable' => $baseDir . '/app/database/migrations/2013_06_27_015259_create_tracks_table.php',
@ -403,6 +404,7 @@ return array(
'Entities\\Genre' => $baseDir . '/app/models/Entities/Genre.php', 'Entities\\Genre' => $baseDir . '/app/models/Entities/Genre.php',
'Entities\\Image' => $baseDir . '/app/models/Entities/Image.php', 'Entities\\Image' => $baseDir . '/app/models/Entities/Image.php',
'Entities\\License' => $baseDir . '/app/models/Entities/License.php', 'Entities\\License' => $baseDir . '/app/models/Entities/License.php',
'Entities\\ShowSong' => $baseDir . '/app/models/Entities/ShowSong.php',
'Entities\\Track' => $baseDir . '/app/models/Entities/Track.php', 'Entities\\Track' => $baseDir . '/app/models/Entities/Track.php',
'Entities\\TrackType' => $baseDir . '/app/models/Entities/TrackType.php', 'Entities\\TrackType' => $baseDir . '/app/models/Entities/TrackType.php',
'Entities\\User' => $baseDir . '/app/models/Entities/User.php', 'Entities\\User' => $baseDir . '/app/models/Entities/User.php',

View file

@ -2,7 +2,7 @@
// autoload_real.php generated by Composer // autoload_real.php generated by Composer
class ComposerAutoloaderInita1fb7abacafe9eba7b46e2d4ded6e3a5 class ComposerAutoloaderInit68362e9063d9f6e829919d3debf84f51
{ {
private static $loader; private static $loader;
@ -19,9 +19,9 @@ class ComposerAutoloaderInita1fb7abacafe9eba7b46e2d4ded6e3a5
return self::$loader; return self::$loader;
} }
spl_autoload_register(array('ComposerAutoloaderInita1fb7abacafe9eba7b46e2d4ded6e3a5', 'loadClassLoader'), true, true); spl_autoload_register(array('ComposerAutoloaderInit68362e9063d9f6e829919d3debf84f51', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(); self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInita1fb7abacafe9eba7b46e2d4ded6e3a5', 'loadClassLoader')); spl_autoload_unregister(array('ComposerAutoloaderInit68362e9063d9f6e829919d3debf84f51', 'loadClassLoader'));
$vendorDir = dirname(__DIR__); $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);