Merge pull request #69 from Poniverse/feature/moderation

Feature/moderation
This commit is contained in:
Peter Deltchev 2016-03-12 17:39:10 -08:00
commit 12a8dac160
28 changed files with 361 additions and 260 deletions

View file

@ -20,6 +20,7 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Gate;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
class DeleteTrackCommand extends CommandBase class DeleteTrackCommand extends CommandBase
@ -41,9 +42,7 @@ class DeleteTrackCommand extends CommandBase
*/ */
public function authorize() public function authorize()
{ {
$user = \Auth::user(); return Gate::allows('delete', $this->_track);
return $this->_track && $user != null && $this->_track->user_id == $user->id;
} }
/** /**

View file

@ -20,6 +20,7 @@
namespace Poniverse\Ponyfm\Commands; namespace Poniverse\Ponyfm\Commands;
use Gate;
use Poniverse\Ponyfm\Models\Album; use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Models\Image; use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
@ -46,9 +47,7 @@ class EditTrackCommand extends CommandBase
*/ */
public function authorize() public function authorize()
{ {
$user = \Auth::user(); return $this->_track && Gate::allows('edit', $this->_track);
return $this->_track && $user != null && $this->_track->user_id == $user->id;
} }
/** /**
@ -143,7 +142,7 @@ class EditTrackCommand extends CommandBase
} else { } else {
if (isset($this->_input['cover'])) { if (isset($this->_input['cover'])) {
$cover = $this->_input['cover']; $cover = $this->_input['cover'];
$track->cover_id = Image::upload($cover, Auth::user())->id; $track->cover_id = Image::upload($cover, $track->user_id)->id;
} else { } else {
if ($this->_input['remove_cover'] == 'true') { if ($this->_input['remove_cover'] == 'true') {
$track->cover_id = null; $track->cover_id = null;

View file

@ -30,6 +30,7 @@ use Poniverse\Ponyfm\Models\Image;
use Poniverse\Ponyfm\Models\ResourceLogItem; use Poniverse\Ponyfm\Models\ResourceLogItem;
use Auth; use Auth;
use Input; use Input;
use Poniverse\Ponyfm\Models\User;
use Response; use Response;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
@ -140,10 +141,13 @@ class AlbumsController extends ApiControllerBase
200); 200);
} }
public function getOwned() public function getOwned(User $user)
{ {
$query = Album::summary()->where('user_id', \Auth::user()->id)->orderBy('created_at', 'desc')->get(); $this->authorize('get-albums', $user);
$query = Album::summary()->where('user_id', $user->id)->orderBy('created_at', 'desc')->get();
$albums = []; $albums = [];
foreach ($query as $album) { foreach ($query as $album) {
$albums[] = [ $albums[] = [
'id' => $album->id, 'id' => $album->id,

View file

@ -20,17 +20,21 @@
namespace Poniverse\Ponyfm\Http\Controllers\Api\Web; namespace Poniverse\Ponyfm\Http\Controllers\Api\Web;
use Auth;
use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase; use Poniverse\Ponyfm\Http\Controllers\ApiControllerBase;
use Poniverse\Ponyfm\Models\Image; use Poniverse\Ponyfm\Models\Image;
use Cover; use Poniverse\Ponyfm\Models\User;
use Illuminate\Support\Facades\Response; use Response;
class ImagesController extends ApiControllerBase class ImagesController extends ApiControllerBase
{ {
public function getOwned() public function getOwned(User $user)
{ {
$query = Image::where('uploaded_by', \Auth::user()->id); $this->authorize('get-images', $user);
$query = Image::where('uploaded_by', $user->id);
$images = []; $images = [];
foreach ($query->get() as $image) { foreach ($query->get() as $image) {
$images[] = [ $images[] = [
'id' => $image->id, 'id' => $image->id,

View file

@ -183,9 +183,7 @@ class TracksController extends ApiControllerBase
return $this->notFound('Track ' . $id . ' not found!'); return $this->notFound('Track ' . $id . ' not found!');
} }
if ($track->user_id != Auth::user()->id) { $this->authorize('edit', $track);
return $this->notAuthorized();
}
return Response::json(Track::mapPrivateTrackShow($track), 200); return Response::json(Track::mapPrivateTrackShow($track), 200);
} }

View file

@ -132,12 +132,12 @@ Route::group(['prefix' => 'api/web'], function() {
Route::group(['middleware' => 'auth'], function() { Route::group(['middleware' => 'auth'], function() {
Route::get('/account/settings', 'Api\Web\AccountController@getSettings'); Route::get('/account/settings', 'Api\Web\AccountController@getSettings');
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('/users/{userId}/albums', 'Api\Web\AlbumsController@getOwned')->where('id', '\d+');
Route::get('/users/{userId}/images', 'Api\Web\ImagesController@getOwned')->where('id', '\d+');
Route::get('/albums/edit/{id}', 'Api\Web\AlbumsController@getEdit'); Route::get('/albums/edit/{id}', 'Api\Web\AlbumsController@getEdit');
Route::get('/playlists/owned', 'Api\Web\PlaylistsController@getOwned'); Route::get('/playlists/owned', 'Api\Web\PlaylistsController@getOwned');

View file

@ -68,7 +68,7 @@ class Image extends Model
/** /**
* @param UploadedFile $file * @param UploadedFile $file
* @param $user * @param int|User $user
* @param bool $forceReupload forces the image to be re-processed even if a matching hash is found * @param bool $forceReupload forces the image to be re-processed even if a matching hash is found
* @return Image * @return Image
* @throws \Exception * @throws \Exception

View file

@ -24,6 +24,7 @@ use Auth;
use Cache; use Cache;
use Config; use Config;
use DB; use DB;
use Gate;
use Poniverse\Ponyfm\Contracts\Searchable; use Poniverse\Ponyfm\Contracts\Searchable;
use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException; use Poniverse\Ponyfm\Exceptions\TrackFileNotFoundException;
use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait; use Poniverse\Ponyfm\Traits\IndexedInElasticsearchTrait;
@ -423,8 +424,8 @@ class Track extends Model implements Searchable
], ],
'user_data' => $userData, 'user_data' => $userData,
'permissions' => [ 'permissions' => [
'delete' => Auth::check() && Auth::user()->id == $track->user_id, 'delete' => Gate::allows('delete', $track),
'edit' => Auth::check() && Auth::user()->id == $track->user_id 'edit' => Gate::allows('edit', $track)
] ]
]; ];
} }

View file

@ -0,0 +1,35 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Policies;
use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Models\User;
class AlbumPolicy
{
public function edit(User $user, Album $album) {
return $user->id === $album->user_id || $user->hasRole('admin');
}
public function delete(User $user, Album $album) {
return $user->id === $album->user_id || $user->hasRole('admin');
}
}

View file

@ -0,0 +1,34 @@
<?php
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Poniverse\Ponyfm\Policies;
use Poniverse\Ponyfm\Models\User;
class UserPolicy
{
public function getAlbums(User $userToAuthorize, User $user) {
return $userToAuthorize->id === $user->id || $userToAuthorize->hasRole('admin');
}
public function getImages(User $userToAuthorize, User $user) {
return $userToAuthorize->id === $user->id || $userToAuthorize->hasRole('admin');
}
}

View file

@ -22,11 +22,14 @@ namespace Poniverse\Ponyfm\Providers;
use Illuminate\Contracts\Auth\Access\Gate as GateContract; use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Poniverse\Ponyfm\Models\Album;
use Poniverse\Ponyfm\Models\Genre; use Poniverse\Ponyfm\Models\Genre;
use Poniverse\Ponyfm\Policies\AlbumPolicy;
use Poniverse\Ponyfm\Policies\GenrePolicy; use Poniverse\Ponyfm\Policies\GenrePolicy;
use Poniverse\Ponyfm\Policies\TrackPolicy; use Poniverse\Ponyfm\Policies\TrackPolicy;
use Poniverse\Ponyfm\Models\Track; use Poniverse\Ponyfm\Models\Track;
use Poniverse\Ponyfm\Models\User; use Poniverse\Ponyfm\Models\User;
use Poniverse\Ponyfm\Policies\UserPolicy;
class AuthServiceProvider extends ServiceProvider class AuthServiceProvider extends ServiceProvider
{ {
@ -38,6 +41,8 @@ class AuthServiceProvider extends ServiceProvider
protected $policies = [ protected $policies = [
Genre::class => GenrePolicy::class, Genre::class => GenrePolicy::class,
Track::class => TrackPolicy::class, Track::class => TrackPolicy::class,
Album::class => AlbumPolicy::class,
User::class => UserPolicy::class,
]; ];
/** /**

View file

@ -22,6 +22,7 @@ namespace Poniverse\Ponyfm\Providers;
use Illuminate\Routing\Router; use Illuminate\Routing\Router;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Poniverse\Ponyfm\Models\User;
class RouteServiceProvider extends ServiceProvider class RouteServiceProvider extends ServiceProvider
{ {
@ -42,9 +43,9 @@ class RouteServiceProvider extends ServiceProvider
*/ */
public function boot(Router $router) public function boot(Router $router)
{ {
//
parent::boot($router); parent::boot($router);
$router->model('userId', User::class);
} }
/** /**

View file

@ -1,4 +1,4 @@
<form novalidate ng-submit="updateTrack(track)"> <form class="track-editor" novalidate ng-submit="updateTrack(track)">
<ul class="toolbar"> <ul class="toolbar">
<li> <li>
<button type="submit" class="btn" ng-class="{disabled: (track.is_published && !isDirty) || isSaving, 'btn-primary': !track.is_published || isDirty}"> <button type="submit" class="btn" ng-class="{disabled: (track.is_published && !isDirty) || isSaving, 'btn-primary': !track.is_published || isDirty}">
@ -71,7 +71,7 @@
<a pfm-popup="song-selector" pfm-popup-close-on-click href="#" class="btn btn-small">Show Songs: <strong>{{selectedSongsTitle}}</strong></a> <a pfm-popup="song-selector" pfm-popup-close-on-click href="#" class="btn btn-small">Show Songs: <strong>{{selectedSongsTitle}}</strong></a>
<div id="song-selector" class="pfm-popup"> <div id="song-selector" class="pfm-popup">
<ul> <ul>
<li ng-repeat="song in ::taxonomies.showSongs track by song.id" ng-class="{selected: selectedSongs[song.id]}"> <li ng-repeat="song in taxonomies.showSongs track by song.id" ng-class="{selected: selectedSongs[song.id]}">
<a pfm-eat-click href="#" ng-click="toggleSong(song); $event.stopPropagation();">{{::song.title}}</a> <a pfm-eat-click href="#" ng-click="toggleSong(song); $event.stopPropagation();">{{::song.title}}</a>
</li> </li>
</ul> </ul>
@ -82,7 +82,7 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="form-row span6" ng-class="{'has-error': errors.cover != null}"> <div class="form-row span6" ng-class="{'has-error': errors.cover != null}">
<label class="strong">Track Cover: </label> <label class="strong">Track Cover: </label>
<pfm-image-upload set-image="setCover" image="track.cover_url" /> <pfm-image-upload set-image="setCover" image="track.cover_url" user-id="track.user_id"></pfm-image-upload>
</div> </div>
<div class="form-row span6" ng-class="{'has-error': errors.released_at != null}"> <div class="form-row span6" ng-class="{'has-error': errors.released_at != null}">
<label for="released_at" class="strong">Release Date:</label> <label for="released_at" class="strong">Release Date:</label>

View file

@ -1,7 +1,7 @@
<div class="single-player"> <div class="single-player">
<a href="#" class="play-button" pfm-eat-click ng-click="play()"> <a href="#" class="play-button" pfm-eat-click ng-click="play()">
<i class="icon-play" ng-show="!track.isPlaying"></i> <i class="icon-play" ng-if="!track.isPlaying"></i>
<i class="icon-pause" ng-hide="!track.isPlaying"></i> <i class="icon-pause" ng-if="track.isPlaying"></i>
</a> </a>
<img pfm-src-loader="::track.covers.thumbnail" pfm-src-size="thumbnail" /> <img pfm-src-loader="::track.covers.thumbnail" pfm-src-size="thumbnail" />
</div> </div>

View file

@ -1,26 +0,0 @@
# Pony.fm - A community for pony fan music.
# Copyright (C) 2015 Peter Deltchev
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
module.exports = angular.module('ponyfm').controller "account-image-select", [
'$scope'
($scope) ->
$scope.images = []
$scope.isLoading = true
$.getJSON('/api/web/images/owned').done (images) -> $scope.$apply ->
$scope.images = images
$scope.isLoading = false
]

View file

@ -30,6 +30,11 @@ module.exports = angular.module('ponyfm').controller "track", [
$scope.$on 'track-updated', () -> $scope.$on 'track-updated', () ->
updateTrackData(true) updateTrackData(true)
$scope.$on 'track-deleted', () ->
# This is meant to take you back to whatever state you found
# this track from.
$window.history.go(-2)
$scope.playlists = [] $scope.playlists = []
if auth.data.isLogged if auth.data.isLogged

View file

@ -23,6 +23,8 @@ module.exports = angular.module('ponyfm').directive 'pfmImageUpload', () ->
scope: scope:
setImage: '=setImage' setImage: '=setImage'
image: '=image' image: '=image'
# ID of the user to upload images on behalf of
userId: '=userId'
compile: (element) -> compile: (element) ->
$image = element.find 'img' $image = element.find 'img'
@ -31,6 +33,7 @@ module.exports = angular.module('ponyfm').directive 'pfmImageUpload', () ->
controller: [ controller: [
'images', '$scope', 'lightbox' 'images', '$scope', 'lightbox'
(images, $scope, lightbox) -> (images, $scope, lightbox) ->
$scope.imageObject = null $scope.imageObject = null
$scope.imageFile = null $scope.imageFile = null
$scope.imageUrl = null $scope.imageUrl = null
@ -40,7 +43,7 @@ module.exports = angular.module('ponyfm').directive 'pfmImageUpload', () ->
$scope.$watch 'image', (val) -> $scope.$watch 'image', (val) ->
$scope.imageObject = $scope.imageFile = $scope.imageUrl = null $scope.imageObject = $scope.imageFile = $scope.imageUrl = null
$scope.isImageLoaded = false $scope.isImageLoaded = false
return if !val return unless val?
$scope.imageUrl = val $scope.imageUrl = val
$image.attr 'src', val $image.attr 'src', val
@ -50,7 +53,9 @@ module.exports = angular.module('ponyfm').directive 'pfmImageUpload', () ->
$scope.isImageLoaded = true $scope.isImageLoaded = true
window.setTimeout (() -> window.alignVertically($image)), 0 window.setTimeout (() -> window.alignVertically($image)), 0
images.refresh().done (images) -> $scope.images = images $scope.$watch 'userId', (val)->
return unless val?
images.refresh(false, $scope.userId).done (images) -> $scope.images = images
$scope.previewImage = () -> $scope.previewImage = () ->
return if !$scope.isImageLoaded return if !$scope.isImageLoaded

View file

@ -91,7 +91,7 @@ module.exports = angular.module('ponyfm').directive 'pfmTrackEditor', () ->
$scope.track.is_published = true $scope.track.is_published = true
$scope.isDirty = false $scope.isDirty = false
$scope.errors = {} $scope.errors = {}
images.refresh true images.refresh(true, track.user_id)
formData = new FormData(); formData = new FormData();
_.each $scope.track, (value, name) -> _.each $scope.track, (value, name) ->
@ -127,17 +127,21 @@ module.exports = angular.module('ponyfm').directive 'pfmTrackEditor', () ->
# ======================================== # ========================================
# The part where everything gets loaded! # The part where everything gets loaded!
# ======================================== # ========================================
$.when( tracks.getEdit($scope.trackId, true)
albums.refresh(), .then (track)->
taxonomies.refresh(), images.refresh(true, track.user_id)
tracks.getEdit($scope.trackId, true) $.when(
).done (albums, taxonomies, track)-> albums.refresh(false, track.user_id),
# Update album data taxonomies.refresh()
$scope.albums.length = 0 ).done (albums, taxonomies)->
albumsDb = {} # Update album data
for album in albums $scope.albums.length = 0
albumsDb[album.id] = album albumsDb = {}
$scope.albums.push album for album in albums
albumsDb[album.id] = album
$scope.albums.push album
$scope.selectedAlbum = if track.album_id then albumsDb[track.album_id] else null
# Update track data # Update track data
@ -151,6 +155,7 @@ module.exports = angular.module('ponyfm').directive 'pfmTrackEditor', () ->
$scope.track = $scope.track =
id: track.id id: track.id
title: track.title title: track.title
user_id: track.user_id
description: track.description description: track.description
lyrics: track.lyrics lyrics: track.lyrics
is_explicit: track.is_explicit is_explicit: track.is_explicit
@ -167,7 +172,6 @@ module.exports = angular.module('ponyfm').directive 'pfmTrackEditor', () ->
is_published: track.is_published is_published: track.is_published
is_listed: track.is_listed is_listed: track.is_listed
$scope.selectedAlbum = if track.album_id then albumsDb[track.album_id] else null
$scope.selectedSongs = {} $scope.selectedSongs = {}
$scope.selectedSongs[song.id] = song for song in track.show_songs $scope.selectedSongs[song.id] = song for song in track.show_songs
updateSongDisplay() updateSongDisplay()

View file

@ -18,6 +18,9 @@ module.exports = angular.module('ponyfm').factory('account-albums', [
'$rootScope', '$http' '$rootScope', '$http'
($rootScope, $http) -> ($rootScope, $http) ->
def = null def = null
# the ID of the user whose albums are currently cached
currentlyLoadedUserId = null
albums = [] albums = []
self = self =
@ -31,11 +34,12 @@ module.exports = angular.module('ponyfm').factory('account-albums', [
$http.get(url).success (album) -> editDef.resolve album $http.get(url).success (album) -> editDef.resolve album
editDef.promise() editDef.promise()
refresh: (force) -> refresh: (force = false, user_id = window.pfm.auth.user.id) ->
force = force || false return def if !force && def && user_id == currentlyLoadedUserId
return def if !force && def
def = new $.Deferred() def = new $.Deferred()
$http.get('/api/web/albums/owned').success (ownedAlbums) -> $http.get("/api/web/users/#{user_id}/albums").success (ownedAlbums) ->
currentlyLoadedUserId = user_id
def.resolve(ownedAlbums) def.resolve(ownedAlbums)
def.promise() def.promise()

View file

@ -18,17 +18,21 @@ module.exports = angular.module('ponyfm').factory('images', [
'$rootScope' '$rootScope'
($rootScope) -> ($rootScope) ->
def = null def = null
currentlyLoadedUserId = null
self = self =
images: [] images: []
isLoading: true isLoading: true
refresh: (force) ->
return def if !force && def refresh: (force, userId = window.pfm.auth.user.id) ->
return def if !force && def && userId == currentlyLoadedUserId
def = new $.Deferred() def = new $.Deferred()
self.images = [] self.images = []
self.isLoading = true self.isLoading = true
$.getJSON('/api/web/images/owned').done (images) -> $rootScope.$apply -> $.getJSON("/api/web/users/#{userId}/images").done (images) -> $rootScope.$apply ->
currentlyLoadedUserId = userId
self.images = images self.images = images
self.isLoading = false self.isLoading = false
def.resolve images def.resolve images
@ -38,4 +42,3 @@ module.exports = angular.module('ponyfm').factory('images', [
self.refresh() self.refresh()
return self return self
]) ])

View file

@ -27,6 +27,7 @@ module.exports = angular.module('ponyfm').factory('taxonomies', [
genresWithTracks: [] genresWithTracks: []
showSongs: [] showSongs: []
showSongsWithTracks: [] showSongsWithTracks: []
refresh: () -> refresh: () ->
return def.promise() if def != null return def.promise() if def != null

View file

@ -227,120 +227,10 @@ html {
} }
} }
// Used in the Accounts > Tracks area
.two-pane-view { .two-pane-view {
.list {
.dropdowns {
margin-top: 0px;
}
}
.editor { .editor {
display: none; display: none;
.album-track-listing {
padding: 0px;
clear: both;
margin: 0px;
margin-top: 10px;
list-style: none;
li {
overflow: hidden;
line-height: normal;
padding: 0px;
margin: 0px;
font-size: 8pt;
border-bottom: 1px dashed #ddd;
div {
padding: 2px;
}
span {
display: block;
float: left;
margin-left: 5px;
margin-top: 2px;
}
.btn {
line-height: normal;
padding: 2px 5px;
margin: 0px;
}
&.ui-sortable-helper {
#gradient>.vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%));
border: none;
color: #fff;
}
&.ui-sortable-placeholder {
background: @yellow;
}
}
}
.show-songs, .album {
.btn {
display: block;
float: none;
}
}
.show-songs, .album, .track-selector {
.btn {
.border-radius(0px);
padding: 3px 10px;
font-size: 8pt;
text-align: left;
}
.error {
margin-top: 10px;
}
.pfm-popup {
width: 300px;
ul {
margin: 0px;
padding: 0px;
list-style: none;
li {
margin: 0px;
padding: 0px;
a {
.ellipsis();
display: block;
padding: 3px 10px;
font-size: 8pt;
color: #333333;
&:hover {
#gradient>.vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%));
text-decoration: none;
color: @dropdownLinkColorHover;
}
}
&.selected {
a {
#gradient>.vertical(@green, darken(@green, 5%));
color: #fff;
font-weight: bold;
&:hover {
#gradient>.vertical(fadeout(@green, 20%), fadeout(darken(@green, 5%), 20%));
}
}
}
}
}
}
}
} }
&.closed { &.closed {
@ -376,61 +266,3 @@ html {
} }
} }
} }
.license-grid {
margin: 0px;
padding: 0px;
overflow: hidden;
list-style: none;
li {
float: left;
width: 25%;
> div {
margin: 0px 5px;
border: 1px solid #ddd;
padding: 10px;
cursor: pointer;
strong {
font-size: 9pt;
display: block;
margin: 0px;
margin-bottom: 5px;
padding: 0px;
line-height: normal;
}
}
p {
min-height: 120px;
font-size: 9pt;
}
a {
.border-radius(0px);
display: block;
width: auto;
}
&.selected {
> div {
cursor: default;
border-color: @blue;
}
}
&:hover > div {
border: 1px solid #3366CC;
}
&:first-child > div {
margin-left: 0px;
}
&:last-child > div {
margin-right: 0px;
}
}
}

View file

@ -24,12 +24,13 @@
@import 'layout'; @import 'layout';
@import 'account-content'; @import 'account-content';
@import 'admin'; @import 'admin';
@import 'components'; @import 'components/components';
@import 'forms'; @import 'forms';
@import 'animations'; @import 'animations';
@import 'body'; @import 'body';
@import 'player'; @import 'components/player';
@import 'content'; @import 'content';
@import 'dashboard'; @import 'dashboard';
@import 'uploader'; @import 'components/uploader';
@import 'search'; @import 'components/search';
@import 'components/track-editor';

View file

@ -16,9 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import 'base/bootstrap/bootstrap'; @import '../base/bootstrap/bootstrap';
@import 'mixins'; @import '../mixins';
@import 'variables'; @import '../variables';
.stretch-to-bottom { .stretch-to-bottom {
overflow-y: auto; overflow-y: auto;

View file

@ -16,8 +16,8 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import 'variables'; @import '../variables';
@import 'mixins'; @import '../mixins';
body.is-logged { body.is-logged {
.track-player { .track-player {

View file

@ -16,9 +16,9 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import 'base/bootstrap/bootstrap'; @import '../base/bootstrap/bootstrap';
@import 'mixins'; @import '../mixins';
@import 'variables'; @import '../variables';
.search { .search {
position: relative; position: relative;

View file

@ -0,0 +1,192 @@
/**
* Pony.fm - A community for pony fan music.
* Copyright (C) 2016 Peter Deltchev
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
@import '../base/bootstrap/bootstrap';
@import '../mixins';
.track-editor {
.list {
.dropdowns {
margin-top: 0px;
}
}
.album-track-listing {
padding: 0px;
clear: both;
margin: 0px;
margin-top: 10px;
list-style: none;
li {
overflow: hidden;
line-height: normal;
padding: 0px;
margin: 0px;
font-size: 8pt;
border-bottom: 1px dashed #ddd;
div {
padding: 2px;
}
span {
display: block;
float: left;
margin-left: 5px;
margin-top: 2px;
}
.btn {
line-height: normal;
padding: 2px 5px;
margin: 0px;
}
&.ui-sortable-helper {
#gradient> .vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%));
border: none;
color: #fff;
}
&.ui-sortable-placeholder {
background: @yellow;
}
}
}
.show-songs, .album {
.btn {
display: block;
float: none;
}
}
.show-songs, .album, .track-selector {
.btn {
.border-radius(0px);
padding: 3px 10px;
font-size: 8pt;
text-align: left;
}
.error {
margin-top: 10px;
}
.pfm-popup {
width: 300px;
ul {
margin: 0px;
padding: 0px;
list-style: none;
li {
margin: 0px;
padding: 0px;
a {
.ellipsis();
display: block;
padding: 3px 10px;
font-size: 8pt;
color: #333333;
&:hover {
#gradient> .vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%));
text-decoration: none;
color: @dropdownLinkColorHover;
}
}
&.selected {
a {
#gradient> .vertical(@green, darken(@green, 5%));
color: #fff;
font-weight: bold;
&:hover {
#gradient> .vertical(fadeout(@green, 20%), fadeout(darken(@green, 5%), 20%));
}
}
}
}
}
}
}
}
.license-grid {
margin: 0px;
padding: 0px;
overflow: hidden;
list-style: none;
li {
float: left;
width: 25%;
> div {
margin: 0px 5px;
border: 1px solid #ddd;
padding: 10px;
cursor: pointer;
strong {
font-size: 9pt;
display: block;
margin: 0px;
margin-bottom: 5px;
padding: 0px;
line-height: normal;
}
}
p {
min-height: 120px;
font-size: 9pt;
}
a {
.border-radius(0px);
display: block;
width: auto;
}
&.selected {
> div {
cursor: default;
border-color: @blue;
}
}
&:hover > div {
border: 1px solid #3366CC;
}
&:first-child > div {
margin-left: 0px;
}
&:last-child > div {
margin-right: 0px;
}
}
}

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
@import 'variables'; @import '../variables';
.uploader { .uploader {
h1 { h1 {