mirror of
https://github.com/Poniverse/Pony.fm.git
synced 2025-01-31 03:16:42 +01:00
Merge pull request #69 from Poniverse/feature/moderation
Feature/moderation
This commit is contained in:
commit
12a8dac160
28 changed files with 361 additions and 260 deletions
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
35
app/Policies/AlbumPolicy.php
Normal file
35
app/Policies/AlbumPolicy.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
34
app/Policies/UserPolicy.php
Normal file
34
app/Policies/UserPolicy.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
|
||||||
]
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
170
resources/assets/styles/account-content.less
vendored
170
resources/assets/styles/account-content.less
vendored
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
9
resources/assets/styles/app.less
vendored
9
resources/assets/styles/app.less
vendored
|
@ -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';
|
||||||
|
|
|
@ -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;
|
|
@ -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 {
|
|
@ -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;
|
192
resources/assets/styles/components/track-editor.less
vendored
Normal file
192
resources/assets/styles/components/track-editor.less
vendored
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
Loading…
Reference in a new issue