加入後台媒體庫功能

This commit is contained in:
kroutony 2020-02-22 23:39:58 +08:00
parent 179e369f0b
commit d896c085a3
46 changed files with 4093 additions and 2 deletions

33
app/Disk.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* App\Disk
*
* @property int $id
* @property string $name 名稱
* @property-read mixed $path
* @property-read \Illuminate\Database\Eloquent\Collection|\App\MediaFile[] $mediaFiles
* @property-read int|null $media_files_count
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Disk whereName($value)
* @mixin \Eloquent
*/
class Disk extends Model
{
public function getPathAttribute()
{
return config('filesystems.disks.' . $this->name)['root'];
}
public function mediaFiles()
{
return $this->hasMany(MediaFile::class);
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Events;
use App\MediaCategory;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MediaCategoryDeletingEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $mediaCategory;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(MediaCategory $mediaCategory)
{
$this->mediaCategory = $mediaCategory;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Events;
use App\MediaFile;
use Illuminate\Broadcasting\Channel;
use Illuminate\Queue\SerializesModels;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class MediaFileDeletedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $model;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(MediaFile $model)
{
$this->model = $model;
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -0,0 +1,300 @@
<?php
namespace App\Http\Controllers;
use App\Repositories\MediaCategoriesRepository;
use App\Repositories\MediaFileRepository;
use App\Traits\PureAjaxMethodProtectable;
use Illuminate\Http\Request;
use Gate;
use Auth;
use Validator;
/**
* 媒體庫分類的Controller
*
* Class MediaCategoryController
* @package App\Http\Controllers
*/
class MediaCategoryController extends Controller
{
use PureAjaxMethodProtectable;
private $mediaCategoriesRepository;
private $mediaFileRepository;
public function __construct(MediaCategoriesRepository $mediaCategoriesRepository, MediaFileRepository $mediaFileRepository)
{
$this->mediaCategoriesRepository = $mediaCategoriesRepository;
$this->mediaFileRepository = $mediaFileRepository;
}
public function hasAppMediaCategoryPermission()
{
return Gate::allows('permission:manage-app-media-categories');
}
/**
* 取得所有媒體庫分類
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function index(Request $request)
{
$this->protectFromNoneAjaxRequest($request);
$responseData = [
'mediaCategories' => []
];
if(Auth::check()) {
$userId = Auth::id();
$categories = [];
$hasAppCategoryPermission = $this->hasAppMediaCategoryPermission();
if($hasAppCategoryPermission) {
$categories = $this->mediaCategoriesRepository->getAllAppMediaCategories();
} else {
$categories = $this->mediaCategoriesRepository->getAllUserCategories($userId);
}
foreach ($categories as $category) {
$category->count = $this->mediaCategoriesRepository->getMediaFilesCount($category->id);
}
$categories = array_merge([
[
'id' => 'all',
'name' => trans('mediaLibrary.all'),
'count' => $hasAppCategoryPermission
? $this->mediaFileRepository->getAppMediasCount()
: $this->mediaFileRepository->getUserMediasCount($userId)
],
[
'id' => 'uncategorized',
'name' => trans('mediaLibrary.uncategorized'),
'count' => $hasAppCategoryPermission
? $this->mediaFileRepository->getAppUncategorizedMediasCount()
: $this->mediaFileRepository->getUserUncategorizedMediasCount($userId)
]
] , $categories->toArray());
$responseData['mediaCategories'] = $categories;
return response()->json($responseData);
} else {
return response()->json($responseData, 401);
}
}
/**
* 新增媒體庫分類
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request)
{
$this->protectFromNoneAjaxRequest($request);
Validator::make($request->all(), [
'name' => 'required|min:1'
])->validate();
$status = 200;
if(Auth::check()) {
$userId = Auth::id();
$hasAppMediaCategoryPermission = $this->hasAppMediaCategoryPermission();
$category = $this->mediaCategoriesRepository->addCategory($request->get('name'), $userId, $hasAppMediaCategoryPermission);
if(!$category) {
$status = 500;
}
} else {
$status = 401;
}
if($status == 200) {
return response()->json([
'mediaCategory' => $category,
'message' => trans('message.categoryNameHasBeenAdded', ['name' => $category->name])
]);
} else {
return response()->json([
'message' => trans('message.failToAddCategory')
], $status);
}
}
/**
* 更新媒體庫分類
*
* @param Request $request
* @param $categoryId
* @return \Illuminate\Http\JsonResponse
*/
public function update(Request $request, $categoryId)
{
$this->protectFromNoneAjaxRequest($request);
Validator::make($request->all(), [
'name' => 'required|min:1'
])->validate();
$category = $this->mediaCategoriesRepository->getCategory($categoryId);
$status = 200;
if(Auth::check()) {
if($category) {
$permissionCheck = true;
if($this->hasAppMediaCategoryPermission()) {
if(!$category->is_app_media_category) {
$permissionCheck = false;
}
} else {
if($category->user_id != Auth::id()) {
$permissionCheck = false;
}
}
if($permissionCheck) {
$category->name = $request->get('name');
$saved = $category->save();
if(!$saved) {
$status = 500;
}
} else {
$status = 403;
}
} else {
$status = 400;
}
} else {
$status = 401;
}
if($status == 200) {
return response()->json([
'id' => $categoryId,
'name' => $request->get('name'),
'message' => trans('message.categoryNameHasBeenUpdated', ['name' => $request->get('name')])
]);
} else {
return response()->json([
'message' => trans('message.failToUpdateCategoryName')
], $status);
}
}
/**
* 刪除媒體庫分類
*
* @param Request $request
* @param $categoryId
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy(Request $request, $categoryId)
{
$this->protectFromNoneAjaxRequest($request);
$status = 200;
if(Auth::check()) {
$category = $this->mediaCategoriesRepository->getCategory($categoryId);
if($category) {
$permissionCheck = true;
if($this->hasAppMediaCategoryPermission()) {
if(!$category->is_app_media_category) {
$permissionCheck = false;
}
} else {
if($category->user_id != Auth::id()) {
$permissionCheck = false;
}
}
if($permissionCheck) {
$mediaFilesCount = $category->mediaFiles->count();
$category->delete();
} else {
$status = 403;
}
} else {
$status = 400;
}
} else {
$status = 401;
}
if($status == 200) {
return response()->json([
'id' => $category->id,
'count' => $mediaFilesCount,
'message' => trans('message.categoryNameHasBeenDeleted', ['name' => $category->name])
]);
} else {
return response()->json([
'message' => trans('message.failToDeleteCategory')
]);
}
}
/**
* 更新分類順序
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function updateOrder(Request $request)
{
$this->protectFromNoneAjaxRequest($request);
$this->validate($request, [
'ids' => 'required|min:1'
]);
$status = 200;
$mediaCategories = [];
$ids = explode(',', $request->get('ids'));
foreach ($ids as $id) {
$mediaCategories[] = $this->mediaCategoriesRepository->getCategory($id);
}
if(Auth::check()) {
$existenceCheck = true;
foreach ($mediaCategories as $mediaCategory) {
$existenceCheck = $existenceCheck && $mediaCategory;
}
if($existenceCheck) {
$permissionCheck = true;
if($this->hasAppMediaCategoryPermission()) {
foreach ($mediaCategories as $mediaCategory) {
$permissionCheck = $permissionCheck && $mediaCategory->is_app_media_category;
}
} else {
foreach ($mediaCategories as $mediaCategory) {
$permissionCheck = $permissionCheck && ($mediaCategory->user_id == Auth::id());
}
}
if($permissionCheck) {
foreach ($mediaCategories as $index => $mediaCategory) {
$mediaCategory->seq = $index;
$mediaCategory->save();
}
} else {
$status = 403;
}
} else {
$status = 400;
}
} else {
$status = 401;
}
if($status == 200) {
return response()->json([]);
} else {
return response()->json([
'message' => trans('message.failToReOrderCategory', $status)
]);
}
}
}

View File

@ -0,0 +1,374 @@
<?php
namespace App\Http\Controllers;
use App\Repositories\MediaCategoriesRepository;
use App\Traits\PureAjaxMethodProtectable;
use App\Traits\UploadedFileProccessable;
use App\MediaFile;
use App\Repositories\MediaFileRepository;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Storage;
use Str;
use Auth;
use Gate;
use Validator;
/**
* 媒體檔案
*
* Class MediaLibraryController
* @package App\Http\Controllers
*/
class MediaLibraryController extends Controller
{
use PureAjaxMethodProtectable;
use UploadedFileProccessable;
private $mediaFileRepository;
private $mediaCategoriesRepository;
public function __construct(MediaFileRepository $mediaFileRepository, MediaCategoriesRepository $mediaCategoriesRepository)
{
$this->mediaFileRepository = $mediaFileRepository;
$this->mediaCategoriesRepository = $mediaCategoriesRepository;
}
/**
* 建立response資料
*
* @param MediaFile $mediaFile
* @return array
*/
private function createResponseMediaFile(MediaFile $mediaFile)
{
$mediaCategory = $mediaFile->mediaCategory;
return [
'id' => $mediaFile->id,
'fileName' => $mediaFile->file_name,
'url' => $mediaFile->url,
'date' => $mediaFile->created_at->format('Y-m-d H:i:s'),
'mimeType' => $mediaFile->mime_type,
'description' => $mediaFile->description,
'sizeInBytes' => $mediaFile->size,
'width' => $mediaFile->width,
'height' => $mediaFile->height,
'category' => [
'id' => $mediaCategory ? $mediaCategory->id : 'uncategorized',
'name' => $mediaCategory ? $mediaCategory->name : ''
]
];
}
private function hasAppMediaPermission()
{
return Gate::allows("permission:manage-app-medias");
}
public function hasAppMediaCategoryPermission()
{
return Gate::allows('permission:manage-app-media-categories');
}
/**
* 新增媒體檔案
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function fileUpload(Request $request)
{
$this->protectFromNoneAjaxRequest($request);
if(Auth::check()) {
/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $file */
$file = $request->files->get('media_file');
if($file) {
$userId = Auth::id();
if($this->hasAppMediaPermission()) {
$uploadedMediaFile = $this->uploadMediaFile($file, 1, $userId, null, true);
} else {
$uploadedMediaFile = $this->uploadMediaFile($file, 2, $userId, $userId);
}
$success = false;
if($uploadedMediaFile) {
$category = $request->get('category');
if($this->mediaCategoriesRepository->hasCategory($category)) {
$uploadedMediaFile->media_category_id = $category;
$uploadedMediaFile->save();
}
$success = true;
}
if($success) {
return response()->json([
'media' => $this->createResponseMediaFile($uploadedMediaFile),
'category' => $category
]);
} else {
return response()->json([], 400);
}
}
} else {
return response()->json([], 401);
}
}
/**
* 取得媒體檔案
*
* @param Request $request
* @param null $page
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
public function getMedias(Request $request, $page = null)
{
$this->protectFromNoneAjaxRequest($request);
$this->validate($request, [
'category_id' => 'required'
]);
$responseData = [
'medias' => []
];
$lastQueryId = $request->get('last_fetched_media_id');
$category = $request->get('category_id');
$limit = $request->get('limit');
$mediaFiles = [];
if(Auth::check()) {
$userId = Auth::id();
if($this->hasAppMediaPermission()) {
if($lastQueryId) {
$mediaFiles = $this->mediaFileRepository->getAppMediasWithLastQueryIdAndCategory($lastQueryId, $category, $limit ? $limit : 14);
} else {
$mediaFiles = $this->mediaFileRepository->getAppMediasWithCategory($category, $limit ? $limit : 35);
}
} else {
if($lastQueryId) {
$mediaFiles = $this->mediaFileRepository->getUserMediasWithLastQueryIdAndCategory($userId, $category, $lastQueryId, $limit ? $limit : 14);
} else {
$mediaFiles = $this->mediaFileRepository->getUserMediasWithCategory($userId, $category, $limit ? $limit : 35);
}
}
foreach ($mediaFiles as $mediaFile) {
$responseData['medias'][] = $this->createResponseMediaFile($mediaFile);
}
return response()->json($responseData);
} else {
return response()->json($responseData, 401);
}
}
/**
* 更新媒體檔案
*
* @param Request $request
* @param $mediaId
* @return \Illuminate\Http\JsonResponse
*/
public function updateMedia(Request $request, $mediaId)
{
$this->protectFromNoneAjaxRequest($request);
$status = 200;
if(Auth::check()) {
$media = $this->mediaFileRepository->getMedia($mediaId);
if($media) {
if($this->hasAppMediaPermission()) {
if(!$media->is_app_media) {
$status = 403;
}
} else {
if(!$media->user->id == Auth::id()) {
$status = 403;
}
}
} else {
$status = 404;
}
} else {
$status = 401;
}
if($status == 200) {
$media->description = $request->get('description');
$mediaCategoryId = $request->get('category_id');
$mediaCategoryId = $mediaCategoryId == 'uncategorized' ? null : $mediaCategoryId;
$media->media_category_id = $mediaCategoryId;
$saved = $media->save();
if($saved) {
return response()->json([
'message' => trans('message.descriptionHasBeenUpdated')
]);
} else {
return response()->json([
'message' => trans('message.failToUpdateDescription')
], 400);
}
} else {
return response()->json([
'message' => trans('message.failToUpdateDescription')
], $status);
}
}
/**
* 刪除媒體檔案
*
* @param Request $request
* @param $mediaId
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function deleteMedia(Request $request, $mediaId)
{
$this->protectFromNoneAjaxRequest($request);
$status = 200;
if(Auth::check()) {
$media = $this->mediaFileRepository->getMedia($mediaId);
if($media) {
if($this->hasAppMediaPermission()) {
if(!$media->is_app_media) {
$status = 403;
}
} else {
if(!$media->user->id == Auth::id()) {
$status = 403;
}
}
} else {
$status = 404;
}
} else {
$status = 401;
}
if($status == 200) {
$deleted = $media->delete();
if($deleted) {
return response()->json([
'id' => $media->id,
'categoryId' => $media->media_category_id ? $media->media_category_id : 'uncategorized',
'message' => trans('message.mediaHasBeenDeleted')
]);
} else {
return response()->json([
'message' => trans('message.failToDeleteMedia')
], 400);
}
} else {
return response()->json([
'message' => trans('message.failToDeleteMedia')
], $status);
}
}
/**
* 更新媒體檔案的分類
*
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Illuminate\Validation\ValidationException
*/
public function updateCategory(Request $request)
{
$this->protectFromNoneAjaxRequest($request);
$this->validate($request, [
'category_id' => 'required',
'media_ids' => 'required'
]);
$status = 200;
if(Auth::check()) {
$userId = Auth::id();
$categoryId = $request->get('category_id');
$mediaIds = $request->get('media_ids');
$mediaIds = is_array($mediaIds) ? $mediaIds : explode(',', $request->get('media_ids'));
$mediaFiles = collect($mediaIds)->map(function($mediaId){
return $this->mediaFileRepository->getMedia($mediaId);
});
$mediaCategory = $this->mediaCategoriesRepository->getCategory($categoryId);
$existenceCheck = true;
if($categoryId != 'uncategorized') {
$existenceCheck = $existenceCheck && $mediaCategory;
}
foreach ($mediaFiles as $mediaFile) {
$existenceCheck = $existenceCheck && $mediaFile;
}
if($existenceCheck) {
$permissionCheck = true;
$hasAppMediaCategoryPermission = $this->hasAppMediaCategoryPermission();
$hasAppMediaPermisssion = $this->hasAppMediaPermission();
foreach ($mediaFiles as $mediaFile) {
if($hasAppMediaPermisssion) {
$permissionCheck = $permissionCheck && $mediaFile->is_app_media;
} else {
$permissionCheck = $permissionCheck && ($mediaFile->user_id == $userId);
}
}
if($mediaCategory) {
if($hasAppMediaCategoryPermission) {
$permissionCheck = $permissionCheck && $mediaCategory->is_app_media_category;
} else {
$permissionCheck = $permissionCheck && ($mediaCategory->user_id == $userId);
}
}
if($permissionCheck) {
$categorySources = [];
$mediaCategoryId = $mediaCategory ? $mediaCategory->id : null;
foreach ($mediaFiles as $mediaFile) {
$oldCategoryId = $mediaFile->media_category_id ? $mediaFile->media_category_id : 'uncategorized';
if(!isset($categorySources[$oldCategoryId])) {
$categorySources[$oldCategoryId] = 0;
}
$categorySources[$oldCategoryId]++;
$mediaFile->media_category_id = $mediaCategoryId;
$mediaFile->save();
}
} else {
$status = 403;
}
} else {
$status = 403;
}
} else {
$status = 401;
}
if($status == 200) {
$message = $mediaCategory
? trans('message.mediaHasBeenMoveToCategory', ['name' => $mediaCategory->name])
: trans('message.mediaHasBeenSetToUncategorized');
return response()->json([
'mediaIds' => $mediaIds,
'categoryId' => $categoryId,
'categorySources' => $categorySources,
'message' => $message
]);
} else {
return response()->json([
'message' => trans('message.failToMoveMediaToCategory')
], $status);
}
}
}

View File

@ -23,8 +23,9 @@ class SetSiteStates
public function handle($request, Closure $next) public function handle($request, Closure $next)
{ {
$siteState = app(SiteStateService::class); $siteState = app(SiteStateService::class);
$adminRoute = config('admin.route');
//是否網站後台判斷route prefix //是否網站後台判斷route prefix
if($request->is(config('admin.route') . '/*')) { if($request->is($adminRoute . '/*', $adminRoute)) {
$siteState->isAdminArea = true; $siteState->isAdminArea = true;
} }

View File

@ -0,0 +1,37 @@
<?php
namespace App\Listeners;
use App\Events\MediaCategoryDeletingEvent;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class MediaCategoryDeletingListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(MediaCategoryDeletingEvent $event)
{
$mediaCategory = $event->mediaCategory;
$mediaFiles = $mediaCategory->mediaFiles;
$mediaFiles->each(function($mediaFile){
$mediaFile->media_category_id = null;
$mediaFile->save();
});
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Listeners;
use App\Events\MediaFileDeletedEvent;
use App\MediaFile;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Storage;
class MediaFileDeletedListener
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(MediaFileDeletedEvent $event)
{
/**
* 刪除檔案
*/
/** @var MediaFile $mediaFile */
$mediaFile = $event->model;
if($mediaFile->disk) {
$disk = Storage::disk($mediaFile->disk->name);
$disk->delete($mediaFile->fileNameWithPath);
if($mediaFile->path) {
if(empty($disk->allFiles($mediaFile->path)) && empty($disk->allDirectories($mediaFile->path))) {
$disk->deleteDirectory($mediaFile->path);
}
}
}
}
}

57
app/MediaCategory.php Normal file
View File

@ -0,0 +1,57 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* App\MediaCategory
*
* @property int $id
* @property string $name 名稱
* @property int $is_app_media_category 是否為網站媒體庫分類
* @property int|null $seq 排序
* @property int|null $user_id 所屬使用者
* @property-read \Illuminate\Database\Eloquent\Collection|\App\MediaFile[] $mediaFiles
* @property-read int|null $media_files_count
* @property-read \App\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereIsAppMediaCategory($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereName($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereSeq($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaCategory whereUserId($value)
* @mixin \Eloquent
*/
class MediaCategory extends Model
{
public $timestamps = false;
protected $dispatchesEvents = [
'deleting' => \App\Events\MediaCategoryDeletingEvent::class
];
protected $fillable = [
'name',
'is_app_media_category',
'user_id'
];
protected $hidden = [
'is_app_media_category',
'user_id',
'seq'
];
public function user()
{
return $this->belongsTo(User::class);
}
public function mediaFiles()
{
return $this->hasMany(MediaFile::class);
}
}

174
app/MediaFile.php Normal file
View File

@ -0,0 +1,174 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Storage;
use File;
/**
* App\MediaFile
*
* @property int $id
* @property string|null $path 子資料夾路徑
* @property string $file_name 檔名
* @property string|null $ext 副檔名
* @property int|null $disk_id disk名稱
* @property int|null $media_source_id 外部來源id
* @property int|null $size 檔案大小
* @property int|null $width 圖片寬度
* @property int|null $height 圖片高度
* @property string|null $mime_type 檔案的Mime Type
* @property int $is_app_media 是否為網站的媒體檔案
* @property string|null $description 描述
* @property int|null $media_category_id 媒體分類
* @property int|null $user_id 所屬使用者
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
* @property-read \App\Disk|null $disk
* @property-read mixed $file_name_with_path
* @property-read mixed $file_path
* @property-read mixed $url
* @property-read mixed $youtube_embed_url
* @property-read \App\MediaCategory|null $mediaCategory
* @property-read \App\MediaSource|null $mediaSource
* @property-read \App\User|null $user
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile category($categoryId)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile isAppMedia()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile uncategorized()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile userMedia($userId)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereCreatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereDescription($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereDiskId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereExt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereFileName($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereHeight($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereIsAppMedia($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereMediaCategoryId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereMediaSourceId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereMimeType($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile wherePath($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereSize($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereUpdatedAt($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaFile whereWidth($value)
* @mixin \Eloquent
*/
class MediaFile extends Model
{
protected $dispatchesEvents = [
'deleted' => \App\Events\MediaFileDeletedEvent::class
];
protected $fillable = [
'path',
'file_name',
'ext',
'disk_id',
'media_source_id',
'size',
'width',
'height',
'is_app_media',
'user_id',
'mime_type',
'description'
];
protected $hidden = [
'path',
'ext',
'disk_id',
'disk',
'is_app_media',
'user_id'
];
public function getUrlAttribute()
{
return Storage::disk($this->disk->name)->url(trim($this->path, '/'). '/' . trim($this->file_name, '/'));
}
public function getFilePathAttribute()
{
return Storage::disk($this->disk->name)->path(trim($this->path, '/'). '/' . trim($this->file_name, '/'));
}
public function getFileNameWithPathAttribute()
{
return trim(trim($this->path, '/') . '/' . $this->file_name, '/');
}
public function getYoutubeEmbedUrlAttribute()
{
$mediaSource = $this->mediaSource;
if($mediaSource && 'youtube' == $mediaSource->name) {
return "https://www.youtube.com/embed/$this->file_name";
}
return '';
}
/**
* @param \Illuminate\Database\Query\Builder $query
*
* @return \Illuminate\Database\Query\Builder
*/
public function scopeUncategorized($query)
{
return $query->where('media_category_id', null);
}
/**
* @param \Illuminate\Database\Query\Builder $query
*
* @return \Illuminate\Database\Query\Builder
*/
public function scopeCategory($query, $categoryId)
{
return $query->where('media_category_id', $categoryId);
}
/**
* @param \Illuminate\Database\Query\Builder $query
*
* @return \Illuminate\Database\Query\Builder
*/
public function scopeIsAppMedia($query)
{
return $query->where('is_app_media', true);
}
/**
* @param \Illuminate\Database\Query\Builder $query
*
* @return \Illuminate\Database\Query\Builder
*/
public function scopeUserMedia($query, $userId)
{
return $query->where('is_app_media', false)->where('user_id', $userId);
}
public function user()
{
return $this->belongsTo(User::class);
}
public function disk()
{
return $this->belongsTo(Disk::class);
}
public function mediaCategory()
{
return $this->belongsTo(MediaCategory::class);
}
public function mediaSource()
{
return $this->belongsTo(MediaSource::class);
}
}

27
app/MediaSource.php Normal file
View File

@ -0,0 +1,27 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
/**
* App\MediaSource
*
* @property int $id
* @property string $name 外部來源名稱
* @property-read \Illuminate\Database\Eloquent\Collection|\App\MediaFile[] $mediaFiles
* @property-read int|null $media_files_count
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\MediaSource whereName($value)
* @mixin \Eloquent
*/
class MediaSource extends Model
{
public function mediaFiles()
{
return $this->hasMany(MediaFile::class);
}
}

View File

@ -4,6 +4,21 @@ namespace App;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
/**
* App\Option
*
* @property int $id
* @property string $key 選項名稱
* @property string $value 選項值
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option key($key)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option newQuery()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option query()
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option whereId($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option whereKey($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\Option whereValue($value)
* @mixin \Eloquent
*/
class Option extends Model class Option extends Model
{ {
protected $fillable = ['key', 'value']; protected $fillable = ['key', 'value'];

View File

@ -18,6 +18,14 @@ class EventServiceProvider extends ServiceProvider
Registered::class => [ Registered::class => [
SendEmailVerificationNotification::class, SendEmailVerificationNotification::class,
], ],
// MediaCategory刪除前的Event
\App\Events\MediaCategoryDeletingEvent::class => [
\App\Listeners\MediaCategoryDeletingListener::class
],
// MediaFile刪除後的Event
\App\Events\MediaFileDeletedEvent::class => [
\App\Listeners\MediaFileDeletedListener::class
],
]; ];
/** /**

View File

@ -0,0 +1,82 @@
<?php
namespace App\Repositories;
use App\MediaCategory;
/**
* Class MediaCategoriesRepository
* @package App\Repositories
* @method MediaCategory createModel()
* @method MediaCategory getModel()
* @method MediaCategory findModel($id)
*/
class MediaCategoriesRepository extends BaseRepository
{
public function __construct(MediaCategory $mediaCategory)
{
$this->setModel($mediaCategory);
}
public function getAllAppMediaCategories()
{
return $this->getModel()
->where('is_app_media_category', true)
->orderBy('seq')
->orderBy('id', 'desc')
->get();
}
public function getAllUserCategories($userId)
{
return $this->getModel()
->where('is_app_media_category', false)
->where('user_id', $userId)
->orderBy('seq')
->orderBy('id', 'desc')
->get();
}
public function getMediaFilesCount($categoryId)
{
return $this->findModel($categoryId)->mediaFiles->count();
}
/**
* @param $name
* @return \App\MediaCategory
*/
public function addCategory($name, $userId, $isAppMediaCategory)
{
$category = $this->createModel();
$category->fill([
'name' => $name,
'user_id' => $userId,
'is_app_media_category' => $isAppMediaCategory
]);
$saved = $category->save();
if($saved) {
return $category;
} else {
return false;
}
}
/**
* @param $id
* @return \App\MediaCategory
*/
public function getCategory($id)
{
return $this->findModel($id);
}
/**
* @param $id
* @return bool
*/
public function hasCategory($id)
{
return $this->hasModel($id);
}
}

View File

@ -0,0 +1,242 @@
<?php
namespace App\Repositories;
use App\MediaFile;
use Illuminate\Database\Eloquent\Builder;
/**
* Class MediaFileRepository
* @package App\Repositories
* @method MediaFile createModel()
* @method MediaFile getModel()
* @method MediaFile findModel($id)
*/
class MediaFileRepository extends BaseRepository
{
public function __construct(MediaFile $mediaFile)
{
$this->setModel($mediaFile);
}
private function queryBuilderGetLimit(Builder $queryBuilder, $limit = null)
{
if($limit) {
return $queryBuilder->limit($limit)->get();
}
return $queryBuilder->get();
}
private function queryBuilderWhereCategory(Builder $queryBuilder, $category = '')
{
if($category == 'uncategorized') {
$queryBuilder = $queryBuilder->uncategorized();
} else if(is_numeric($category)) {
$queryBuilder = $queryBuilder->category($category);
}
return $queryBuilder;
}
/**
* @param $id
* @return \App\MediaFile|null
*/
public function getMedia($id)
{
return $this->findModel($id);
}
public function hasMedia($id)
{
return $this->hasModel($id);
}
/**
* @param $fileName
* @param $ext
* @param $diskId
* @param $mediaSourceId
* @param $mimeType
* @param null $userId
* @param null $width
* @param null $height
* @param bool $isAppMedia
* @param $size
* @return MediaFile|\Illuminate\Database\Eloquent\Model|boolean
*/
public function addMedia($fileName, $ext, $diskId, $mediaSourceId, $size, $mimeType, $userId, $path = null, $width = null, $height = null, $isAppMedia = false)
{
$mediaFile = $this->createModel();
$mediaFile->fill([
'path' => $path,
'file_name' => $fileName,
'ext' => $ext,
'disk_id' => $diskId,
'media_source_id' => $mediaSourceId,
'size' => $size,
'mime_type' => $mimeType,
'width' => $width,
'height' => $height,
'is_app_media' => $isAppMedia,
'user_id' => $userId,
]);
$saved = $mediaFile->save();
return $saved ? $mediaFile : $saved;
}
/**
* @param $fileName
* @param $ext
* @param $size
* @param $mimeType
* @param $userId
* @param null $width
* @param null $height
* @return MediaFile|\Illuminate\Database\Eloquent\Model
*/
public function addAppMedia($fileName, $ext, $size, $mimeType, $userId, $path = null, $width = null, $height = null)
{
return $this->addMedia($fileName, $ext, 1, null, $size, $mimeType, $userId, $path, $width, $height, true);
}
/**
* @param $fileName
* @param $ext
* @param $size
* @param $mimeType
* @param $userId
* @param null $width
* @param null $height
* @return MediaFile|\Illuminate\Database\Eloquent\Model
*/
public function addUserMedia($fileName, $ext, $size, $mimeType, $userId, $path = null, $width = null, $height = null)
{
return $this->addMedia($fileName, $ext, 2, null, $size, $mimeType, $userId, $path, $width, $height);
}
/**
* @param null $limit
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
*/
public function getAppMedias($limit = null)
{
$queryBuilder = $this->getModel()
->isAppMedia()
->orderByDesc('id');
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
public function getAppMediasWithCategory($category, $limit = null)
{
$queryBuilder = $this->getModel()
->isAppMedia()
->orderByDesc('id');
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
/**
* @param $lastQueryId
* @param null $limit
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
*/
public function getAppMediasWithLastQueryId($lastQueryId, $limit = null)
{
$queryBuilder = $this->getModel()
->isAppMedia()
->where('id', '<', $lastQueryId)
->orderByDesc('id');
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
/**
* @param $lastQueryId
* @param null $limit
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
*/
public function getAppMediasWithLastQueryIdAndCategory($lastQueryId, $category, $limit = null)
{
$queryBuilder = $this->getModel()
->where('is_app_media', true)
->where('id', '<', $lastQueryId)
->orderByDesc('id');
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
/**
* @param $userId
* @param null $limit
* @return Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public function getUserMedias($userId, $limit = null)
{
$queryBuilder = $this->getModel()
->userMedia($userId)
->orderByDesc('id');
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
public function getUserMediasWithCategory($userId, $category, $limit = null)
{
$queryBuilder = $this->getModel()
->userMedia($userId)
->orderByDesc('id');
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
/**
* @param $userId
* @param $lastQueryId
* @param null $limit
* @return MediaFile[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
*/
public function getUserMediasWithLastQueryId($userId, $lastQueryId, $limit = null)
{
$queryBuilder = $this->getModel()
->userMedia($userId)
->where('id', '<', $lastQueryId)
->orderByDesc('id');
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
public function getUserMediasWithLastQueryIdAndCategory($userId, $lastQueryId, $category, $limit = null)
{
$queryBuilder = $this->getModel()
->userMedia($userId)
->where('id', '<', $lastQueryId)
->orderByDesc('id');
$queryBuilder = $this->queryBuilderWhereCategory($queryBuilder, $category);
return $this->queryBuilderGetLimit($queryBuilder, $limit);
}
public function getAppUncategorizedMediasCount()
{
return $this->getAppMediasWithCategory('uncategorized')->count();
}
public function getUserUncategorizedMediasCount($userId)
{
return $this->getUserMediasWithCategory($userId, 'uncategorized')->count();
}
public function getAppMediasCount()
{
return $this->getModel()->isAppMedia()->count();
}
public function getUserMediasCount($userId)
{
return $this->getModel()->userMedia($userId)->count();
}
}

View File

@ -40,8 +40,25 @@ class AppJsObjectService
} }
if($siteState->isAdminArea) { if($siteState->isAdminArea) {
$obj['admin']['ajax']['resource'] = [
'mediaFile' => [
'upload' => route('media.upload'),
'get' => route('media.get'),
'update' => route('media.update', [':id']),
'destroy' => route('media.destroy', [':id']),
'updateCategory' => route('media.updateCategory')
],
'mediaCategory' => [
'add' => route('media-category.store'),
'index' => route('media-category.index'),
'update' => route('media-category.update', [':id']),
'destroy' => route('media-category.destroy', [':id']),
'updateOrder' => route('media-category.updateOrder')
],
];
$obj['admin']['translations'] = [ $obj['admin']['translations'] = [
'dataTables' => trans('datatables'), 'dataTables' => trans('datatables'),
'mediaLibrary' => trans('mediaLibrary')
]; ];
} }
return $obj; return $obj;

View File

@ -0,0 +1,21 @@
<?php
namespace App\Traits;
use Illuminate\Http\Request;
/**
* 使得Controller可以過濾非Ajax的請求
*
* Trait PureAjaxMethodProtectable
* @package App\Traits
*/
trait PureAjaxMethodProtectable
{
// 非ajax請求則回應404
protected function protectFromNoneAjaxRequest(Request $request)
{
if(!$request->ajax()) abort(404);
return;
}
}

View File

@ -0,0 +1,94 @@
<?php
namespace App\Traits;
use App\Repositories\MediaFileRepository;
use App\Disk;
use Auth;
use Storage;
use Str;
/**
* 使得Controller可以上傳媒體檔案
*
* Trait UploadedFileProccessable
* @package App\Traits
*/
trait UploadedFileProccessable
{
/**
* 取得上傳的媒體檔案資訊
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
* @return array
*/
private function getUpadloedFileInfo(\Symfony\Component\HttpFoundation\File\UploadedFile $file)
{
$ext = $file->getClientOriginalExtension();
$baseName = str_replace(".$ext", '', $file->getClientOriginalName());
$ext = strtolower($ext);
$alteredBaseName = Str::random(26) . time() . ".$ext";
$imageDim = getimagesize($file);
if($imageDim) {
$width = $imageDim[0];
$height = $imageDim[1];
} else {
$width = null;
$height = null;
}
$fileSize = $file->getSize();
$mimeType = $file->getMimeType();
return [
'ext' => $ext,
'fileName' => $alteredBaseName,
'fileSize' => $fileSize,
'mimeType' => $mimeType,
'width' => $width,
'height' => $height,
];
}
/**
* 上傳媒體檔案
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile $file
* @param $diskId
* @param $userId
* @param string $path
* @param bool $isAppMedia
* @return \App\MediaFile|null
* @throws \Exception
*/
private function uploadMediaFile(\Symfony\Component\HttpFoundation\File\UploadedFile $file, $diskId, $userId, $path = null, $isAppMedia = false)
{
$fileInfo = $this->getUpadloedFileInfo($file);
$ext = $fileInfo['ext'];
$fileName = $fileInfo['fileName'];
$fileSize = $fileInfo['fileSize'];
$mimeType = $fileInfo['mimeType'];
$width = $fileInfo['width'];
$height = $fileInfo['height'];
$mediaFileRepo = app(MediaFileRepository::class);
$disk = Disk::find($diskId);
if($disk) {
$mediaFile = $mediaFileRepo->addMedia($fileName, $ext, $diskId, null, $fileSize, $mimeType, $userId, $path, $width, $height, $isAppMedia);
if($mediaFile) {
$putted = Storage::disk($disk->name)->putFileAs($path, $file, $fileName);
if($putted !== false) {
return $mediaFile;
} else {
$mediaFile->delete();
return null;
}
} else {
return null;
}
} else {
throw new \Exception('Disk not found with id: ' . $diskId);
}
}
}

View File

@ -30,6 +30,12 @@ use Spatie\Permission\Traits\HasRoles;
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereRememberToken($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\User whereRememberToken($value)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User whereUpdatedAt($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\User whereUpdatedAt($value)
* @mixin \Eloquent * @mixin \Eloquent
* @property-read \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Permission[] $permissions
* @property-read int|null $permissions_count
* @property-read \Illuminate\Database\Eloquent\Collection|\Spatie\Permission\Models\Role[] $roles
* @property-read int|null $roles_count
* @method static \Illuminate\Database\Eloquent\Builder|\App\User permission($permissions)
* @method static \Illuminate\Database\Eloquent\Builder|\App\User role($roles, $guard = null)
*/ */
class User extends Authenticatable class User extends Authenticatable
{ {

View File

@ -1,5 +1,14 @@
<?php <?php
return [ return [
// 自訂的disk關聯到config/fielsystems.php下定義的disks
'disks' => [
1 => 'uploadedAppMedias',
2 => 'uploadedUserMedias',
],
// 外連的media來源
'mediaSources' => [
1 => 'youtube'
],
'roles' => [ 'roles' => [
['name' => 'administrator', 'displayName' => 'administrator'], ['name' => 'administrator', 'displayName' => 'administrator'],
['name' => 'editor', 'displayName' => 'editor'], ['name' => 'editor', 'displayName' => 'editor'],
@ -43,6 +52,22 @@ return [
'displayName' => 'adminManageSystemStatus', 'displayName' => 'adminManageSystemStatus',
'assignTo' => [] 'assignTo' => []
], ],
[
//管理媒體庫
'name' => 'manage app medias',
'displayName' => 'manageAppMedias',
'assignTo' => [
'editor'
]
],
[
//管理媒體庫分類
'name' => 'manage app media categories',
'displayName' => 'manageAppMediaCategories',
'assignTo' => [
'editor'
]
],
], ],
// 預設的設定值 // 預設的設定值
'options' => [ 'options' => [

View File

@ -66,7 +66,7 @@ return [
| Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
*/ */
'capture_ajax' => true, 'capture_ajax' => false,
'add_ajax_timing' => false, 'add_ajax_timing' => false,
/* /*

View File

@ -63,7 +63,19 @@ return [
'bucket' => env('AWS_BUCKET'), 'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'), 'url' => env('AWS_URL'),
], ],
'uploadedAppMedias' => [
'driver' => 'local',
'root' => storage_path('app/public/uploads/app'),
'url' => env('APP_URL').'/storage/uploads/app',
'visibility' => 'public',
],
'uploadedUserMedias' => [
'driver' => 'local',
'root' => storage_path('app/public/uploads/user'),
'url' => env('APP_URL').'/storage/uploads/user',
'visibility' => 'public',
],
], ],
]; ];

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateDisksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('disks', function (Blueprint $table) {
$table->unsignedTinyInteger('id')->primary();
$table->string('name', 40)->unique()->comment('名稱');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
Schema::dropIfExists('disks');
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMediaCategoriesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('media_categories', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name', 100)->comment('名稱');
$table->boolean('is_app_media_category')->default(false)->comment('是否為網站媒體庫分類');
$table->unsignedInteger('seq')->nullable()->default(0)->comment('排序');
$table->unsignedBigInteger('user_id')->nullable()->comment('所屬使用者');
$table->foreign('user_id')->references('id')->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('media_categories');
}
}

View File

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMediaFilesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('media_files', function (Blueprint $table) {
$table->bigIncrements('id');
$table->text('path')->nullable()->comment('子資料夾路徑');
$table->string('file_name', 50)->unique()->comment('檔名');
$table->string('ext', 10)->nullable()->comment('副檔名');
$table->unsignedTinyInteger('disk_id')->nullable()->comment('disk名稱');
$table->unsignedInteger('size')->nullable()->comment('檔案大小');
$table->unsignedMediumInteger('width')->nullable()->default(null)->comment('圖片寬度');
$table->unsignedMediumInteger('height')->nullable()->default(null)->comment('圖片高度');
$table->string('mime_type', 40)->nullable()->comment('檔案的Mime Type');
$table->boolean('is_app_media')->default(false)->comment('是否為網站的媒體檔案');
$table->text('description')->nullable()->comment('描述');
$table->unsignedBigInteger('media_category_id')->nullable()->default(null)->comment('媒體分類');
$table->bigInteger('user_id')->unsigned()->nullable()->comment('所屬使用者');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users');
$table->foreign('disk_id')->references('id')->on('disks');
$table->foreign('media_category_id')->references('id')->on('media_categories');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('media_files');
}
}

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMediaSourcesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('media_sources', function (Blueprint $table) {
$table->unsignedTinyInteger('id')->primary();
$table->string('name')->comment('外部來源名稱');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('media_sources');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddMediaSourceColumnToMediaFilesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('media_files', function (Blueprint $table) {
$table->unsignedTinyInteger('media_source_id')->nullable()->default(null)->after('disk_id')->comment('外部來源id');
$table->foreign('media_source_id', 'source')->references('id')->on('media_sources');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('media_files', function (Blueprint $table) {
DB::statement('SET FOREIGN_KEY_CHECKS = 0');
$table->dropForeign('source'); // Drop foreign key 'user_id' from 'posts' table
$table->dropColumn('media_source_id');
DB::statement('SET FOREIGN_KEY_CHECKS = 1');
});
}
}

View File

@ -13,5 +13,7 @@ class DatabaseSeeder extends Seeder
{ {
$this->call(RolesAndPermissionsSeeder::class); $this->call(RolesAndPermissionsSeeder::class);
$this->call(OptionsSeeder::class); $this->call(OptionsSeeder::class);
$this->call(DisksSeeder::class);
$this->call(MediaSourcesSeeder::class);
} }
} }

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Seeder;
class DisksSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$disks = config('data-presets.disks');
$diskModel = app(\App\Disk::class);
foreach ($disks as $id => $disk) {
$diskModel->insert([
'id' => $id,
'name' => $disk
]);
}
}
}

View File

@ -0,0 +1,25 @@
<?php
use Illuminate\Database\Seeder;
class MediaSourcesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$mediaSources = config('data-presets.mediaSources');
$mediaSourceModel = app(\App\MediaSource::class);
foreach ($mediaSources as $id => $mediaSource) {
$mediaSourceModel->insert([
'id' => $id,
'name' => $mediaSource
]);
}
}
}

6
package-lock.json generated
View File

@ -3281,6 +3281,12 @@
"integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=", "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=",
"dev": true "dev": true
}, },
"dropzone": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/dropzone/-/dropzone-5.7.0.tgz",
"integrity": "sha512-kOltiZXH5cO/72I22JjE+w6BoT6uaVLfWdFMsi1PMKFkU6BZWpqRwjnsRm0o6ANGTBuZar5Piu7m/CbKqRPiYg==",
"dev": true
},
"duplexify": { "duplexify": {
"version": "3.7.1", "version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",

View File

@ -20,6 +20,7 @@
"cross-env": "^7.0.0", "cross-env": "^7.0.0",
"datatables.net-bs4": "^1.10.20", "datatables.net-bs4": "^1.10.20",
"datatables.net-responsive-bs4": "^2.2.3", "datatables.net-responsive-bs4": "^2.2.3",
"dropzone": "^5.7.0",
"flag-icon-css": "^3.4.6", "flag-icon-css": "^3.4.6",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"jquery": "^3.2", "jquery": "^3.2",

View File

@ -1,2 +1,109 @@
import './lib'; import './lib';
import '../app-common' import '../app-common'
import {block, unblock} from '../utils/blockui'
import mediaLibrary from "../utils/media-library";
/**
* 開啟媒體庫
*
* @param single 是否為單選模式
* @param inputElement 自動會傳值進input元素
* @param selectMode 是否出現選擇按鈕
*/
app.methods.media = function(single = true, inputElement = '', selectMode = true, callback = null) {
let media = mediaLibrary({
url: app.admin.ajax.resource.mediaFile.upload,
translations: app.admin.translations.mediaLibrary,
csrfToken: app.csrfToken,
single: single,
selectMode: selectMode,
})
let resizeMediaLibraryWindow = () => {
let windowHeight = $(window).height(),
$mediaLib = $('#media-library');
let totalHeight = windowHeight * .98,
rowHeadHeight = 50,
rowFooterHeight = 40,
padding = 15 * 2,
rowBodyHeight = totalHeight - padding - rowHeadHeight - rowFooterHeight;
$mediaLib.height(totalHeight);
$mediaLib.find('.row-head').height(rowHeadHeight);
$mediaLib.find('.row-footer').height(rowFooterHeight);
$mediaLib.find('.row-body').height(rowBodyHeight);
$mediaLib.find('.tab-page, .category-list, .media-info-wrapper').height(rowBodyHeight);
$mediaLib.find('.col-media-info').css('marginTop', this.matchMediaInPx(768) ? 0 : rowHeadHeight);
media.$emit('resize', windowHeight, $(window).width())
}
$(window).on('resize', resizeMediaLibraryWindow);
resizeMediaLibraryWindow();
let closeMediaLibrary = () => {
$('#media-library').remove();
unblock();
$(window).off('resize', resizeMediaLibraryWindow);
}
if(callback) {
media.$on('media_selected', function(data){
callback(data);
})
}
if(inputElement) {
media.$on('media_selected', function(data){
if(typeof inputElement == 'string') {
document.querySelectorAll(inputElement).forEach(function(element, index){
element.value = data.idString;
});
} else if(inputElement instanceof HTMLElement) {
inputElement.value = data.idString;
}
});
}
media.$on('close', closeMediaLibrary);
block(media.$el, {
blockMsgClass: 'media-library',
onUnblock: function(){
closeMediaLibrary()
}
})
let $dropzone = $(media.$children[0].$refs.uploadZone).dropzone({
paramName: 'media_file',
url: app.admin.ajax.resource.mediaFile.upload,
headers: {
'X-CSRF-TOKEN': app.csrfToken
},
sending: function(file, xhr, formData){
formData.append('category', media.d.currentCategory);
},
success:function(response){
let responseBody = JSON.parse(response.xhr.response);
if(responseBody.category == media.d.currentCategory) {
media.d.medias.unshift(responseBody.media)
}
if(responseBody.category == 'all') {
media.$children[0].addCategoryCount('uncategorized', 1);
} else {
media.$children[0].addCategoryCount(responseBody.category, 1);
}
}
});
media.$on('remove_all_uploaded_files', function(){
$dropzone.get(0).dropzone.removeAllFiles();
});
return media;
}
$(() => {
$('#app-header .medialibrary').on('click', function(){
app.adminMediaLibrary = app.methods.media(false, null, false);
});
})

View File

@ -25,6 +25,7 @@ import 'datatables.net-bs4';
import 'datatables.net-responsive-bs4'; import 'datatables.net-responsive-bs4';
import 'block-ui'; import 'block-ui';
window.Dropzone = require('dropzone');
try { try {
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';

View File

@ -0,0 +1,86 @@
/**
* 取得媒體分類
*
* 參數:
* category_id: 分類ID
* media_ids: 媒體的ID字串(使用逗號分隔)
*
* Response:
* mediaCategories: 分類資料
*/
export const index = (data = {}) => {
return axios.get(app.admin.ajax.resource.mediaCategory.index, {params: data});
}
/**
* 新增媒體分類
*
* 參數:
* name: 名稱
*
* Success Response:
* mediaCategory: 被新增的分類
* message: 訊息
*
* Error Response:
* message: 訊息
*/
export const add = (name, data = {}) => {
data = Object.assign({
name: name
}, data);
return axios.post(app.admin.ajax.resource.mediaCategory.add, data);
}
/**
* 更新媒體分類名稱
*
* 參數:
* id: 分類ID
* name: 名稱
*
* Success Response:
* id: 分類ID
* name: 分類名稱
* message: 訊息
*
* Error Response:
* message: 訊息
*/
export const update = (id, name, data = {}) => {
data = Object.assign({
name: name
}, data);
return axios.put(app.admin.ajax.resource.mediaCategory.update.replace(':id', id), data);
}
/**
* 更新媒體分類排序
*
* 參數:
* ids: 分類id(以逗號分隔)
*
*
* Error Response:
* message: 訊息
*/
export const updateOrder = (ids, data = {}) => {
data = Object.assign({
ids: ids
}, data);
return axios.put(app.admin.ajax.resource.mediaCategory.updateOrder, data);
}
/**
* 刪除媒體分類
*
* 參數:
* id: 分類ID
*
* Success Response:
* id: 分類ID
* count: 該媒體分類所有的媒體數量
* message: 訊息
*
* Error Response:
* message: 訊息
*/
export const destroy = (id, data = {}) => {
return axios.delete(app.admin.ajax.resource.mediaCategory.destroy.replace(':id', id), data);
}

74
resources/js/apis/admin/media-file.js vendored Normal file
View File

@ -0,0 +1,74 @@
/**
* 取得媒體
*
* 參數:
* category_id: 分類ID
* last_fetched_media_id: 最後一個取得的媒體media
* limit: 取得的數量
*
* Response:
* medias: 媒體資訊
*/
export const get = (categoryId, lastFetchedMediaId = null, limit = null, data = {}) => {
data = Object.assign({
category_id: categoryId,
last_fetched_media_id: lastFetchedMediaId,
limit: limit
}, data)
return axios.get(app.admin.ajax.resource.mediaFile.get, {params: data});
}
/**
* 更新媒體描述
*
* 參數:
* id: 媒體ID
* description: 描述
*
* Response:
* message: 訊息
*/
export const update = (id, description = '', data = {}) => {
data = Object.assign({
description: description
}, data);
return axios.put(app.admin.ajax.resource.mediaFile.update.replace(':id', id), data);
}
/**
* 刪除媒體
*
* 參數:
* id: 媒體ID
*
* Success Response:
* id: 媒體ID
* message: 訊息
*
* Error Response:
* message: 訊息
*/
export const destroy = (id, data = {}) => {
return axios.delete(app.admin.ajax.resource.mediaFile.destroy.replace(':id', id), data);
}
/**
* 更新媒體的分類
*
* 參數:
* category_id: 分類ID
* media_ids: 媒體的ID字串(使用逗號分隔)
*
* Success Response:
* mediaIds: 被移動的媒體ID
* categoryId: 被移動至的分類ID
* categorySources: 被移動的媒體的舊分類ID與數量
* message: 訊息
*
* Error Response:
* message: 訊息
*/
export const updateCategory = (mediaIds, categoryId, data = {}) => {
data = Object.assign({
media_ids: mediaIds,
category_id: categoryId
}, data)
return axios.put(app.admin.ajax.resource.mediaFile.updateCategory, data);
}

View File

@ -20,3 +20,21 @@ app.utils = {
}, },
select2: initSelect2, select2: initSelect2,
} }
app.methods = {
/**
* 檢查是否符合給定的Media Query
* 預設為max-width
* */
matchMedia: function(value, directive = 'max-width') {
return window.matchMedia('(' + directive + ': ' + value + ')').matches;
},
/**
* 檢查是否符合給定的Media Query單位為px
* 預設為max-width
* */
matchMediaInPx(value, directive = 'max-width') {
return this.matchMedia(value + 'px', directive);
},
}

View File

@ -0,0 +1,717 @@
<template>
<div id="media-library">
<div class="container-fluid">
<div class="row no-gutters">
<div class="close-window" @click="onCloseWindow"><i class="fa fa-close"></i></div>
<div class="col-md-2 col-categories" v-show="!isMobile || _d.onlyShowCategory">
<div class="row row-head no-gutters">
<div class="col">
<div class="controls">
<div v-if="_d.addingNewCategory">
<input type="text" class="form-control" v-model="_d.currentEditingCategoryText" @click.prevent.stop @keypress.enter="onAddCategory">
<button class="btn btn-sm btn-success" @click.prevent.stop="onAddCategory"><i class="fa fa-check"></i></button>
<button class="btn btn-sm btn-outline-secondary" @click.prevent.stop="onCancelCategoryAdding"><i class="fa fa-close"></i></button>
</div>
<div v-if="!_d.addingNewCategory">
<button class="btn btn-success btn-sm" @click="_d.addingNewCategory=true"><i class="fa fa-plus"></i></button>
<button class="btn btn-primary btn-sm" @click="onEditCategory(_d.currentCategory)"><i class="fa fa-edit"></i></button>
<button class="btn btn-danger btn-sm" @click="onDeleteCategory(_d.currentCategory)"><i class="fa fa-close"></i></button>
<button class="btn btn-outline-secondary btn-sm return" v-if="isMobile" @click="_d.onlyShowCategory=false"><i class="fa fa-arrow-right"></i></button>
</div>
</div>
</div>
</div>
<div class="row row-body no-gutters">
<div class="col">
<div class="category-list-wrapper">
<div class="message">
<div class="alert alert-success" v-if="_d.categorySuccessMessage">{{ _d.categorySuccessMessage }}</div>
<div class="alert alert-danger" v-if="_d.categoryErrorMessage">
{{ _d.categoryErrorMessage }}
<button type="button" class="close" aria-label="Close" @click="_d.categoryErrorMessage=null">
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div class="spinner-border" role="status" v-if="_d.categoryFetching">
<span class="sr-only">Loading...</span>
</div>
<nav class="nav flex-column category-list" :class="{dragging: this._d.currentDraggingCategoryId}" ref="categoryList">
<a class="nav-link category"
href="#"
:draggable="!isUneditableCategory(category.id)"
:data-id="category.id"
:class="{current: _d.currentCategory == category.id, editing: isCategoryEditing(category.id)}"
@dragenter.capture.prevent.stop="onDragEnterCategory($event, category.id)"
@dragleave.capture.prevent.stop="onDragLeaveCategory($event)"
@drop="onDropOnCategory($event, category.id)"
@dragstart.stop="onDragCategory(category.id)"
@dragover.prevent.stop
@dragend.capture.prevent.stop="onDropCategory"
@click.prevent="switchCategory(category.id)"
v-for="category in _d.categories">
<span v-if="!isCategoryEditing(category.id)">
<span class="name">{{ category.name }}</span>
<span class="badge badge-secondary">{{ category.count }}</span>
<button class="btn btn-sm edit-category" v-if="!isUneditableCategory(category.id)" @click.prevent.stop="onEditCategory(category.id)">
<i class="fa fa-edit"></i>
</button>
<button class="btn btn-sm remove-category" v-if="!isUneditableCategory(category.id)" @click.prevent.stop="onDeleteCategory(category.id)">
<i class="fa fa-close"></i>
</button>
</span>
<span v-if="isCategoryEditing(category.id)">
<input type="text" class="form-control" v-model="_d.currentEditingCategoryText" @click.prevent.stop>
<button class="btn btn-sm btn-primary" @click.prevent.stop="onUpdateCategoryName()"><i class="fa fa-check"></i></button>
<button class="btn btn-sm btn-outline-secondary" @click.prevent.stop="cancelCategoryEditing"><i class="fa fa-close"></i></button>
</span>
</a>
</nav>
</div>
</div>
</div>
<div class="row row-footer no-gutters">
<div class="col"></div>
</div>
</div>
<div class="col col-images" v-show="!isMobile || (isMobile && !_d.onlyShowCategory && !_d.onlyShowMediaInfo)">
<div class="row row-head no-gutters">
<div class="col">
<ul class="nav nav-tabs tabs">
<li class="nav-item" v-if="isMobile">
<a href="#" class="nav-link" @click="_d.onlyShowCategory=true">
{{ _d.text.tabCategory }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @drag.prevent @click="_d.isUploadingPage=true"
v-bind:class="{active: _d.isUploadingPage}">{{ _d.text.tabUpload }}</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" @drag.prevent @click="_d.isUploadingPage=false"
v-bind:class="{active: !_d.isUploadingPage}">{{ _d.text.tabBrowse }}</a>
</li>
</ul>
</div>
</div>
<div class="row row-body no-gutters">
<div class="col">
<div class="alert alert-success image-message" v-if="_d.imageSuccessMessage">
{{ _d.imageSuccessMessage }}
</div>
<div class="alert alert-danger image-message" v-if="_d.imageErrorMessage">
{{ _d.imageErrorMessage }}
<button type="button" class="close" aria-label="Close" @click="_d.imageErrorMessage=null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="tab-page upload" v-show="_d.isUploadingPage">
<form class="upload-zone" v-bind:action="_d.formAction" ref="uploadZone">
<input type="hidden" name="_token" v-bind:value="_d.csrfToken">
<div class="fallback">
<input name="media_file" type="file" multiple />
</div>
<div class="upload-tip"> {{ _d.text.dragOrClickToUpload }} </div>
</form>
</div>
<div class="tab-page browse" v-bind:class="{dragging : !isCurrentDraggingMediaIdsEmpty }" v-show="!_d.isUploadingPage" @scroll="onScrollBrowsing($event)">
<div v-for="media in _d.medias"
:key="media.id"
@dragend="onDropMedia"
@dragstart="onDragMedia(media.id)"
draggable="true"
class="img-wrapper"
v-bind:class="{ selected: isMediaSelected(media)}"
@click="onClickMedia($event, media)"
@click.ctrl="onCtrlClickMedia($event, media)">
<div class="img" :style="{ 'background-image': 'url(' + media.url + ')' }">
</div>
</div>
<div class="spinner-border" role="status" v-if="_d.fetchingLock">
<span class="sr-only">Loading...</span>
</div>
</div>
</div>
</div>
<div class="row row-footer no-gutters">
<div class="col"></div>
</div>
</div>
<div class="col-md-2 col-media-info" v-show="!isMobile || _d.onlyShowMediaInfo">
<div class="row row-head" v-if="isMobile">
<div class="col">
<button class="btn btn-outline-secondary btn-sm return" @click="_d.onlyShowMediaInfo=false"><i class="fa fa-arrow-left"></i></button>
</div>
</div>
<div class="row row-body no-gutters">
<div class="col">
<div class="media-info-wrapper" :class="{'select-mode': _d.selectMode}">
<div class="message">
<div class="alert alert-danger" v-if="_d.infoErrorMessage">
{{ _d.infoErrorMessage }}
<button type="button" class="close" aria-label="Close" @click="_d.infoErrorMessage=null">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="alert alert-success" v-if="_d.infoSuccessMessage">{{ _d.infoSuccessMessage }}</div>
</div>
<div class="info" :class="{hidden: !_d.currentSelectedMedia}">
<div class="url">
<span class="label font-weight-bold">{{ _d.text.url }} :</span>
<div class="content">
<a v-bind:href="_d.currentSelectedMedia.url" target="_blank">{{ _d.currentSelectedMedia.url }}</a>
</div>
</div>
<div class="size">
<span class="label font-weight-bold">{{ _d.text.fileSize }}:</span>
<span class="content">{{ fileSize }}</span>
</div>
<div class="date">
<span class="label font-weight-bold">{{ _d.text.date }}:</span>
<span class="content">{{ _d.currentSelectedMedia.date }}</span>
</div>
<div class="dimension">
<span class="label font-weight-bold">{{ _d.text.size }}:</span>
<span class="content">{{ dimension }}</span>
</div>
<div class="category">
<span class="label font-weight-bold">{{ _d.text.category }}:</span>
<select name="" id="" v-model="_d.currentSelectedMedia.category.id" v-if="_d.currentSelectedMedia.category">
<option :value="category.id" v-for="category in this.mediaSettableCategories">{{ category.name }}</option>
</select>
<button class="btn btn-success btn-sm" @click.prevent="onUpdateMediaCategory()" v-if="_d.currentSelectedMedia">{{ _d.text.update }}</button>
</div>
<div class="description">
<span class="label font-weight-bold">{{ _d.text.description }}</span>
<div class="content">
<textarea class="form-control" v-model="_d.currentSelectedMedia.description"></textarea>
</div>
<button class="btn btn-success btn-sm" @click.prevent="onUpdateMediaDescription($event)">{{ _d.text.update }}</button>
<button class="btn btn-outline-danger btn-sm" @click.prevent="onDeleteMedia($event)">{{ _d.text.delete }}</button>
</div>
<div class="img">
<img v-bind:src="_d.currentSelectedMedia.url" alt="">
</div>
</div>
</div>
</div>
</div>
<div class="row row-footer no-gutters">
<div class="col">
<div class="select-medias" v-if="_d.selectMode">
<button class="btn btn-primary" @click="onDoneSelectedMedias" :disabled="isSelectedMediasEmpty">{{ _d.text.select }}</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "media-library",
props: ['_d'],
computed: {
isMobile() {
let isMobile = this._d.windowWidth < 768;
return isMobile;
},
mediaSettableCategories() {
return this._d.categories.filter(category => !this.isAllCategory(category.id));
},
//
isSingleSelectionMode() {
return this._d.single;
},
isSelectedMediasEmpty() {
return this._d.selectedMedias.length ? false : true;
},
selectedMediaIds() {
return this._d.selectedMedias.map(_media => _media.id);
},
isCurrentDraggingMediaIdsEmpty() {
return this._d.currentDraggingMediaIds ? false : true;
},
dimension() {
if(this._d.currentSelectedMedia.width !== null && this._d.currentSelectedMedia.height !== null) {
return this._d.currentSelectedMedia.width + 'x' + this._d.currentSelectedMedia.height;
} else {
return '';
}
},
fileSize() {
if(this._d.currentSelectedMedia.sizeInBytes) {
let size = this._d.currentSelectedMedia.sizeInBytes;
if(size < 1024)
return size + ' Bytes';
let unit = 0;
while(size >= 1024) {
unit++;
size /= 1024;
}
size = parseFloat(size).toFixed(2)
switch(unit) {
case 1:
size += ' KB';
break;
case 2:
size += ' MB';
break;
case 3:
size += ' GB';
break;
}
return size;
}
return '';
}
},
methods: {
//
setSelectedMedias(selectedMedias) {
this._d.selectedMedias = selectedMedias;
},
//
clearSelectedMedia() {
this.setSelectedMedias([]);
},
//
addToSelectedMedia(media) {
this._d.selectedMedias.push(media)
},
// (by ID)
isMediaIdSelected(id) {
let foundMedias = this._d.selectedMedias.find(_media => _media.id == id);
return foundMedias ? true : false;
},
//
isMediaSelected(media) {
return this.isMediaIdSelected(media.id);
},
//
deleteFromSelectedMedia(media) {
this.setSelectedMedias(this._d.selectedMedias.filter(_media => _media.id != media.id));
},
//
addOrDeleteSelectedMedia(media) {
if(this.isMediaSelected(media)) {
this.unselectMedia(media);
} else {
this.addToSelectedMedia(media);
}
},
//
setSingleSelectedMedia(media) {
this.clearSelectedMedia();
this.addToSelectedMedia(media);
this.setCurrentSelectedMedia(media);
},
//
unselectMedia(media) {
this.deleteFromSelectedMedia(media);
this.clearCurrentSelectedMedia();
},
//
clearCurrentSelectedMedia() {
this._d.currentSelectedMedia = false
},
//
setCurrentSelectedMedia(media) {
this._d.currentSelectedMedia = media;
if(this.isMobile) {
this._d.onlyShowMediaInfo = true;
}
},
//
findMedia(id) {
return this._d.medias.find(media => media.id == id);
},
// ID""
isAllCategory(id) {
return id == 'all';
},
// ID""
isUncategorizedCategory(id) {
return id == 'uncategorized';
},
//
isUneditableCategory(id) {
return this.isUncategorizedCategory(id) || this.isAllCategory(id)
},
// ID
isCategoryEditing(id) {
return this._d.currentEditingCategory == id;
},
//
findCategory(id) {
return this._d.categories.find(category => category.id == id);
},
//
getCategoryName(id) {
let category = this.findCategory(id);
return category ? category.name : null;
},
//
removeMediaFromList(mediaId) {
this._d.medias = this._d.medias.filter(media => media.id != mediaId);
if(this._d.currentSelectedMedia.id == mediaId) {
this.clearCurrentSelectedMedia();
}
},
// click
onClickMedia(e, media, fromMulti = false) {
if(e.ctrlKey && !fromMulti) {
return;
}
if(
(this.isSingleSelectionMode && this.isMediaSelected(media)) ||
(!this.isSingleSelectionMode && this.isMediaSelected(media) && this._d.selectedMedias.length == 1)
) {
this.unselectMedia(media);
} else {
this.setSingleSelectedMedia(media);
}
},
// click
onCtrlClickMedia(e, media) {
this.setCurrentSelectedMedia(media);
if(this.isSingleSelectionMode) {
this.onClickMedia(e, media, true);
} else {
this.addOrDeleteSelectedMedia(media);
}
},
// click
onDoneSelectedMedias() {
let data = [], idString = '';
this._d.selectedMedias.forEach(_media => {
data.push({
id: _media.id,
url: _media.url
})
})
idString = data.map(media => media.id).join(',')
this.$root.$emit('media_selected', {
idString: idString,
medias: data
})
this.onCloseWindow();
},
// click
onCloseWindow() {
this.$root.$emit('close');
},
onUpdateMediaCategory() {
this.$root.$emit('update_category_for_medias', this._d.currentSelectedMedia.category.id, this._d.currentSelectedMedia.id, false);
},
// click
onUpdateMediaDescription(e) {
this.$root.$emit('update_media_description', e, this._d.currentSelectedMedia)
},
// click
onDeleteMedia(e) {
this.$root.$emit('delete_media', e, this._d.currentSelectedMedia)
},
//
updateCategoryForMedia(mediaId, categoryId) {
let media = this._d.medias.find(media => media.id == mediaId);
if(media) {
let category = this.findCategory(categoryId);
if(category) {
media.category.name = category.name;
media.category.id = categoryId;
}
}
},
// ID
setCurrentDraggingMediaIds(ids) {
this._d.currentDraggingMediaIds = ids;
},
// ID
clearCurrentDraggingMediaIds() {
this._d.currentDraggingMediaIds = null;
},
// click
deleteSelectedMedias() {
//TODO
},
// scroll
onScrollBrowsing(e) {
if(this._d.fetchingEnd) return;
let ele = e.target;
if(ele.scrollTop >= ele.scrollHeight - ele.clientHeight - 10) {
if(!this._d.fetchingLock) {
this.$root.$emit('fetch_medias', 14);
}
}
},
clearInfoMessages() {
this._d.infoSuccessMessage = '';
this._d.infoErrorMessage = '';
},
showInfoSuccessMessage(message) {
this._d.infoSuccessMessage = message;
setTimeout(() => {
this._d.infoSuccessMessage = '';
}, 5000);
},
showInfoErrorMessage(message) {
this._d.infoErrorMessage = message;
},
clearCategoryMessages() {
this._d.categorySuccessMessage = '';
this._d.categoryErrorMessage = '';
},
showCategorySuccessMessage(message) {
this._d.categorySuccessMessage = message;
setTimeout(() => {
this._d.categorySuccessMessage = '';
}, 1000);
},
showCategoryErrorMessage(message) {
this._d.categoryErrorMessage = message;
},
clearImageMessages() {
this._d.imageSuccessMessage = '';
this._d.imageErrorMessage = '';
},
showImageSuccessMessage(message) {
this._d.imageSuccessMessage = message;
setTimeout(() => {
this._d.imageSuccessMessage = '';
}, 1000);
},
showImageErrorMessage(message) {
this._d.imageErrorMessage = message;
},
// drag
onDragMedia(id) {
if(!this.isSelectedMediasEmpty && this.isMediaIdSelected(id)) {
this.setCurrentDraggingMediaIds(this.selectedMediaIds);
} else {
this.setCurrentDraggingMediaIds(id);
}
},
// dragenter
onDragEnterCategory(e, id) {
if(this._d.currentDraggingCategoryId) {
if(this.isUneditableCategory(id)) return;
if(!this.isCurrentDraggingMediaIdsEmpty) return;
let element = this.findCategoryElementById(id);
this._d.currentDraggingOverCategoryId = id;
if(this.isCategoryDragSourceBefore(this._d.currentDraggingCategoryId, id)) {
this.insertAfter(this._d.categoryDraggingIndicator, element)
} else {
this.insertBefore(this._d.categoryDraggingIndicator, element)
}
} else if(!this.isCurrentDraggingMediaIdsEmpty) {
if(!this.isAllCategory(id) && this._d.currentCategory != id) {
e.target.classList.add('on-drag');
}
}
},
// dragleave
onDragLeaveCategory(e) {
e.target.classList.remove('on-drag');
},
// dragend
onDropMedia() {
this.clearCurrentDraggingMediaIds();
},
// dropdrop
onDropOnCategory(e, id) {
e.target.classList.remove('on-drag');
if(!this.isAllCategory(id) && this._d.currentCategory != id && !this.isCurrentDraggingMediaIdsEmpty) {
this.$root.$emit('update_category_for_medias', id, this._d.currentDraggingMediaIds);
}
},
//
switchCategory(id) {
if(this._d.currentEditingCategory) {
this.cancelCategoryEditing();
}
if(!this._d.fetchingLock && !this._d.currentEditingCategory) {
this.clearSelectedMedia();
this.clearCurrentSelectedMedia();
this._d.medias = [];
this._d.currentCategory = id;
this._d.lastFetchedMediaId = null;
this._d.fetchingEnd = false;
this.$root.$emit('fetch_medias', 35);
this.$root.$emit('remove_all_uploaded_files');
}
},
//
substractCategoryCount(id, number, substractFromAll = true) {
let category = this.findCategory(id);
if(category) {
category.count -= number;
}
if(substractFromAll) {
let category = this.findCategory('all');
if(category) {
category.count -= number;
}
}
},
//
addCategoryCount(id, number, addToAll = true) {
let category = this.findCategory(id);
if(category) {
category.count += number;
}
if(addToAll) {
let category = this.findCategory('all');
if(category) {
category.count += number;
}
}
},
// click
onEditCategory(id) {
if(!this.isUneditableCategory(id)) {
this._d.currentEditingCategory = id;
this._d.currentEditingCategoryText = this.getCategoryName(id);
}
},
//
deleteCategoryFromList(id) {
this._d.categories = this._d.categories.filter(category => category.id != id);
if(id == this._d.currentCategory) {
this._d.medias = [];
this._d.currentCategory = null;
}
},
// click
onDeleteCategory(id) {
if(!this.isUneditableCategory(id) && id) {
if(confirm(this._d.text.deleteConfirmation)) {
this.$root.$emit('delete_category', id);
}
}
},
//
cancelCategoryEditing() {
this._d.currentEditingCategory = null;
this._d.currentEditingCategoryText = null;
},
// click
onUpdateCategoryName() {
this.$root.$emit('update_category_name', this._d.currentEditingCategory, this._d.currentEditingCategoryText)
},
//
updateListCategoryName(id, name) {
let category = this.findCategory(id);
if(category) {
category.name = name;
}
},
// click
onCancelCategoryAdding() {
this._d.addingNewCategory = false;
},
// click
onAddCategory() {
this.$root.$emit('add_category', this._d.currentEditingCategoryText);
},
//
addCategoryToList(id, name) {
let _categories = [];
_categories = _categories.concat(this._d.categories.filter(category => this.isUneditableCategory(category.id)))
_categories.push({
id: id,
name: name,
count: 0,
})
_categories = _categories.concat(this._d.categories.filter(category => !this.isUneditableCategory(category.id)))
this._d.categories = _categories;
},
// element
findCategoryElementById(id) {
let element = document.querySelector('.category[data-id="' + id + '"]');
return element;
},
// insert after
insertAfter(newNode, referenceNode) {
referenceNode.parentNode.appendChild(newNode);
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
},
// insert before
insertBefore(newNode, referenceNode) {
referenceNode.parentNode.appendChild(newNode);
referenceNode.parentNode.insertBefore(newNode, referenceNode);
},
//
isCategoryDragSourceBefore(sourceId, targetId) {
let dragSourceIndex = 0, dragTargetIndex = 0;
this._d.categories.forEach((category, index) => {
if(category.id == sourceId) {
dragSourceIndex = index;
}
if(category.id == targetId) {
dragTargetIndex = index;
}
});
return dragSourceIndex < dragTargetIndex;
},
// dragstart
onDragCategory(id) {
this._d.currentDraggingCategoryId = id;
let indicatorElement = document.createElement('span')
indicatorElement.className = 'dragging-indicator';
this._d.categoryDraggingIndicator = indicatorElement;
},
//
switchCategoryOrder(draggingSourceId, draggingTargetId) {
if(draggingSourceId == draggingTargetId) return;
let _categories = [];
let category = this.findCategory(draggingSourceId);
let insertBefore = this.isCategoryDragSourceBefore(draggingSourceId, draggingTargetId);
if(!insertBefore) {
this._d.categories.reverse();
}
this._d.categories.forEach((_category, index) => {
if(_category.id != category.id) {
_categories.push(_category)
}
if(_category.id == draggingTargetId) {
_categories.push(category);
}
})
if(!insertBefore) {
_categories.reverse();
}
this._d.categories = _categories
this.$root.$emit('update_category_order',
_categories
.filter(category => !this.isUneditableCategory(category.id))
.map(category => category.id)
.join(',')
);
},
// dragend
onDropCategory() {
let indicator = document.querySelector('.dragging-indicator')
indicator.parentNode.removeChild(indicator);
this.switchCategoryOrder(this._d.currentDraggingCategoryId, this._d.currentDraggingOverCategoryId);
this._d.currentDraggingOverCategoryId = null;
this._d.currentDraggingCategoryId = null;
}
}
}
</script>
<style scoped>
</style>

335
resources/js/utils/media-library.js vendored Normal file
View File

@ -0,0 +1,335 @@
import mediaLibrary from '../components/MediaLibrary';
import {
get as fetchMedia,
update as updateMediaDescription,
destroy as deleteMedia,
updateCategory as updateCategoryForMedia
} from '../apis/admin/media-file';
import {
index as fetchMediaCategories,
destroy as deleteMediaCategory,
add as addMediaCategory,
update as updateMediaCategory,
updateOrder as updateMediaCategoriesOrder
} from '../apis/admin/media-category';
export default function(param) {
// 如果有存在的媒體庫則先刪除
let previousElement = document.getElementById('media-library');
if (previousElement) {
previousElement.remove();
}
// new一個新的媒體庫結構
let mediaLibraryElement = document.createElement('media-library');
// 設定vue的子組件屬性映射
mediaLibraryElement.setAttribute(':_d', 'd')
// 媒體庫元件append進body
document.body.appendChild(mediaLibraryElement);
// 初始化Vue
let vue = new Vue({
el: mediaLibraryElement,
data: {
d: {
// 文字翻譯
text: param.translations,
// 上傳檔案的Url
formAction: param.url,
// csrf token
csrfToken: param.csrfToken,
// 是否為單選模式
single: param.single,
// 是否顯示選擇按鈕
selectMode: param.selectMode,
// 是否為上傳的頁面
isUploadingPage: false,
// 媒體列表
medias: [],
// 目前點選的媒體
currentSelectedMedia: false,
// 選中的媒體列表
selectedMedias: [],
// 最後一個取得的媒體ID
lastFetchedMediaId: null,
// 取得媒體請求的lock
fetchingLock: false,
// 已取的最後一個媒體
fetchingEnd: false,
// 正在取得分類
categoryFetching: false,
// 媒體資訊更新失敗訊息
infoErrorMessage: '',
// 媒體資訊更新成功訊息
infoSuccessMessage: '',
// 媒體分類
categories: [],
// 目前正在拖曳的媒體ID
currentDraggingMediaIds: null,
// 目前所在的媒體分類
currentCategory: 'uncategorized',
// 目前正在編輯的分類ID
currentEditingCategory: null,
// 目前正在編輯的分類輸入框文字
currentEditingCategoryText: '',
// 目前正在新增分類
addingNewCategory: false,
// 目前正在拖曳的媒體分類ID
currentDraggingCategoryId: null,
// 目前拖曳媒體分類的指示線
categoryDraggingIndicator: null,
// 目前拖曳媒體分類過的媒體分類
currentDraggingOverCategoryId: null,
// 分類更新成功訊息
categorySuccessMessage: '',
// 分類更新失敗訊息
categoryErrorMessage: '',
// 圖片更新成功訊息
imageSuccessMessage: '',
// 圖片更新失敗訊息
imageErrorMessage: '',
// 視窗高度
windowHeight: null,
// 視窗寬度
windowWidth: null,
// 手機板下用,只顯示分類
onlyShowCategory: false,
// 手機板下用,只顯示媒體資訊
onlyShowMediaInfo: false,
}
},
computed: {
component() {
return this.$children[0];
}
},
// Vue instance建立完後呼叫
created() {
this.$on('resize', (height, width) => {
this.d.windowHeight = height;
this.d.windowWidth = width;
});
// 預設先載入媒體資料
this.fetchMedia(35);
// 監聽fetch_medias事件載入媒體資料
this.$on('fetch_medias', (limit = null) => {
this.fetchMedia(limit);
});
// 媒體庫子組件觸發的事件,更新拖曳的媒體的分類
this.$on('update_category_for_medias', (mediaCategoryId, mediaIds, isUpdatingDraggingMedias = true) => {
// ajax更新媒體的分類
updateCategoryForMedia(mediaIds, mediaCategoryId)
.then(response => {
let responseBody = response.data;
// 被移動的媒體數量
let mediasMovedCount = responseBody.mediaIds.length;
// 如果不是在all的分類下
if(this.d.currentCategory != 'all' && responseBody.categoryId != this.d.currentCategory) {
// 將media從現有顯示的列表移除
responseBody.mediaIds.forEach((id, i) => {
this.component.removeMediaFromList(id);
});
} else {
responseBody.mediaIds.forEach((id, i) => {
this.component.updateCategoryForMedia(id, responseBody.categoryId);
});
}
for(let categoryId in responseBody.categorySources) {
// 從原本分類的數量減去
this.component.substractCategoryCount(categoryId, responseBody.categorySources[categoryId], false);
}
// 加上數量至目標分類
this.component.addCategoryCount(responseBody.categoryId, mediasMovedCount, false);
// 顯示訊息
if(isUpdatingDraggingMedias) {
this.showImageMessage(responseBody);
} else {
this.showInfoMessage(responseBody);
}
}).catch(error => {
// 顯示訊息
if(isUpdatingDraggingMedias) {
this.showImageErrorMessage(error);
} else {
this.showInfoErrorMessage(error)
}
});
})
// 媒體庫子組件觸發的click事件更新媒體的描述
this.$on('update_media_description', (e, media) => {
// disable按鈕
e.target.disabled = true;
// ajax更新媒體描述
updateMediaDescription(media.id, media.description)
.then(response => {
this.showInfoMessage(response.data);
})
.catch(error => {
this.showInfoErrorMessage(error);
})
.finally(() => {
// endable按鈕
e.target.disabled = false;
})
});
// 媒體庫子組件觸發的click事件刪除媒體
this.$on('delete_media', (e, media) => {
if(confirm(this.d.text.deleteConfirmation)) {
// disable按鈕
e.target.disabled = true;
// ajax刪除媒體
deleteMedia(media.id)
.then(response => {
this.showInfoMessage(response.data);
// 將媒體從列表移除
this.component.removeMediaFromList(response.data.id);
this.component.substractCategoryCount(response.data.categoryId, 1);
})
.catch(error => {
this.showInfoErrorMessage(error);
})
.finally(() => {
// endable按鈕
e.target.disabled = false;
})
}
});
// ajax載入媒體分類
this.d.categoryFetching = true;
fetchMediaCategories()
.then(response => {
this.d.categories = response.data.mediaCategories;
})
.finally(() => {
this.d.categoryFetching = false;
});
// ajax更新媒體分類名稱
this.$on('update_category_name', (categoryId, name) => {
if(name) {
updateMediaCategory(categoryId, name)
.then(response => {
this.showCategoryMessage(response.data);
// 更新列表分類名稱
this.component.updateListCategoryName(response.data.id, response.data.name);
this.component.cancelCategoryEditing();
})
.catch(error => {
this.showCategoryErrorMessage(error);
})
}
})
// ajax新增媒體分類
this.$on('add_category', name => {
if(name) {
addMediaCategory(name)
.then(response => {
this.showCategoryMessage(response.data);
this.component.addCategoryToList(response.data.mediaCategory.id, response.data.mediaCategory.name);
this.component.cancelCategoryEditing();
this.component.onCancelCategoryAdding();
})
.catch(error => {
this.showCategoryErrorMessage(error);
})
}
})
// ajax刪除媒體分類
this.$on('delete_category', id => {
deleteMediaCategory(id)
.then(response => {
this.showCategoryMessage(response.data);
this.component.addCategoryCount('uncategorized', response.data.count, false);
this.component.deleteCategoryFromList(response.data.id);
})
.catch(error => {
this.showCategoryErrorMessage(error)
})
});
// ajax更新媒體分類順序
this.$on('update_category_order', ids => {
updateMediaCategoriesOrder(ids)
.catch(error => {
this.showCategoryErrorMessage(error);
})
})
},
components: {
mediaLibrary
},
methods: {
// 載入媒體資料
fetchMedia(limit = null) {
// 載入動作的lock避免重複呼叫
this.d.fetchingLock = true;
// ajax載入媒體資料
fetchMedia(this.d.currentCategory, this.d.lastFetchedMediaId, limit)
.then(response => {
let medias = response.data.medias;
// 加入至原有列表
this.d.medias = this.d.medias.concat(medias);
if(medias.length) {
// 紀錄最後一筆媒體的id供下次使用
this.d.lastFetchedMediaId = medias[medias.length - 1].id;
} else {
// 已載入至最後的資料
this.d.fetchingEnd = true;
}
})
.finally(() => {
// 釋放載入動作的lock
this.d.fetchingLock = false;
});
},
// 根據response body顯示訊息
showInfoMessage(responseBody, success = true) {
if(responseBody.hasOwnProperty('message')) {
this.component.clearInfoMessages();
if(success) {
this.component.showInfoSuccessMessage(responseBody.message);
} else {
this.component.showInfoErrorMessage(responseBody.message);
}
}
},
// 根據error顯示訊息只顯示http error
showInfoErrorMessage(error) {
if(error.hasOwnProperty('response')) {
this.showInfoMessage(error.response.data, false);
}
},
// 根據response body顯示分類操作訊息
showCategoryMessage(responseBody, success = true) {
if(responseBody.hasOwnProperty('message')) {
this.component.clearCategoryMessages();
if(success) {
this.component.showCategorySuccessMessage(responseBody.message);
} else {
this.component.showCategoryErrorMessage(responseBody.message);
}
}
},
// 根據error顯示分類操作訊息只顯示http error
showCategoryErrorMessage(error) {
if(error.hasOwnProperty('response')) {
this.showCategoryMessage(error.response.data, false);
}
},
// 根據response body顯示圖片操作訊息
showImageMessage(responseBody, success = true) {
if(responseBody.hasOwnProperty('message')) {
this.component.clearImageMessages();
if(success) {
this.component.showImageSuccessMessage(responseBody.message);
} else {
this.component.showImageErrorMessage(responseBody.message);
}
}
},
// 根據error顯示圖片操作訊息只顯示http error
showImageErrorMessage(error) {
if(error.hasOwnProperty('response')) {
this.showImageMessage(error.response.data, false);
}
},
}
});
return vue;
}

View File

@ -0,0 +1,20 @@
<?php
return [
'tabCategory' => 'Category',
'tabUpload' => 'Upload',
'tabBrowse' => 'Browse',
'dragOrClickToUpload' => 'Drop files here or click to upload.',
'url' => 'Url',
'fileSize' => 'File Size',
'date' => 'Date',
'size' => 'Size',
'description' => 'Description',
'mimeType' => 'MIME type',
'select' => 'Select',
'update' => 'Update',
'delete' => 'Delete',
'deleteConfirmation' => 'Are you sure to delete this?',
'category' => 'Category',
'all' => 'All',
'uncategorized' => 'Uncategorized',
];

View File

@ -1,4 +1,19 @@
<?php <?php
return [ return [
'categoryNameHasBeenAdded' => 'Category :name has been added.',
'categoryNameHasBeenUpdated' => 'Category :name has been updated.',
'categoryNameHasBeenDeleted' => 'Category :name has been deleted.',
'descriptionHasBeenUpdated' => 'The description has been updated.',
'failToUpdateDescription' => 'Fail to update description.',
'failToUpdateCategoryName' => 'Fail to update category name.',
'failToAddCategory' => 'Fail to add category.',
'failToDeleteMedia' => 'Fail to delete media.',
'failToDeleteCategory' => 'Fail to delete category.',
'failToMoveMediaToCategory' => 'Fail to move media to category.',
'failToReOrderCategory' => 'Fail to reorder category.',
'givenCategoryIdDoesNotExist' => 'Given category id does not exist.',
'mediaHasBeenDeleted' => 'The media has been deleted.',
'mediaHasBeenMoveToCategory' => 'Media has been move to category :name.',
'mediaHasBeenSetToUncategorized' => 'Media has been set to uncategorized.',
'success' => 'Success' 'success' => 'Success'
]; ];

View File

@ -0,0 +1,20 @@
<?php
return [
'category' => '分類',
'date' => '日期',
'delete' => '刪除',
'deleteConfirmation' => '確定要刪除嗎?',
'description' => '描述',
'dragOrClickToUpload' => '拖曳檔案至此或點擊上傳',
'fileSize' => '檔案大小',
'mimeType' => '檔案類型',
'select' => '選擇',
'size' => '尺寸',
'tabBrowse' => '瀏覽',
'tabCategory' => '分類',
'tabUpload' => '上傳',
'update' => '更新',
'url' => '網址',
'all' => '全部',
'uncategorized' => '未分類',
];

View File

@ -3,6 +3,7 @@
//元件 //元件
@import "components/datatables"; @import "components/datatables";
@import "components/blockui"; @import "components/blockui";
@import "../components/media-library";

View File

@ -0,0 +1,770 @@
.blockUI.media-library {
z-index: 1099 !important;
width: 98% !important;
transform: translate(-50%, -50%);
top: 50% !important;
left: 50% !important;
cursor: default !important;
}
#media-library {
[draggable="true"] {
user-select: none;
}
height: 100%;
width: 100%;
.container-fluid {
padding-top: 15px;
padding-bottom: 15px;
}
.close-window {
font-size: 24px;
cursor: pointer;
position: absolute;
top: 0;
right: 8px;
}
ul.tabs {
.nav-link {
&.active {
color: white;
background: #343a40;
}
}
}
.image-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.tab-page {
overflow-y: scroll;
overflow-x: hidden;
border: 1px solid #eee;
&.browse {
&.dragging {
opacity: .5;
}
.img-wrapper {
width: calc(14.285% - 40px);
margin: 20px;
float: left;
@include media-breakpoint-down(lg) {
width: calc(20% - 30px);
margin: 15px;
}
@include media-breakpoint-down(md) {
width: calc(25% - 30px);
}
@include media-breakpoint-down(sm) {
width: calc(33.3% - 20px);
margin: 10px;
}
@include media-breakpoint-down(xs) {
width: calc(50% - 30px);
margin: 15px;
}
overflow: hidden;
cursor: pointer;
box-shadow: 0 0 1px 2px lightgrey;
&.selected {
box-shadow: 0px 0px 0 2px white, 0px 0px 0px 8px black;
}
.img {
width: 100%;
padding-top: 100%;
background-size: cover;
background-position: center center;
}
}
}
}
.col-categories {
.controls {
text-align: left;
input {
width: auto;
display: inline-block;
}
}
.category-list-wrapper {
overflow-y: scroll;
overflow-x: hidden;
.message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 75%;
text-align: left;
.alert {
word-break: break-all;
}
}
}
.category-list {
flex-wrap: nowrap;
&.dragging {
.category {
opacity: .5;
}
}
.dragging-indicator {
width: 100%;
border-top: 2px dotted;
}
.category {
padding: 4px;
margin: 1px 0;
text-align: left;
color: black;
&.on-drag {
border: 2px solid black;
}
&.current {
background: gray;
color: white;
&.editing {
background: none;
border: 1px solid gray;
}
}
.btn {
&.edit-category,
&.remove-category {
display: none;
padding: 0;
}
}
.edit-category {
}
&:hover {
.btn {
&.edit-category,
&.remove-category {
display: inline-block;
}
}
}
}
}
}
.col-media-info {
.message {
.alert {
text-align: left;
}
}
}
.media-info-wrapper {
width: 100%;
overflow: auto;
padding-left: 5px;
padding-right: 5px;
text-align: left;
.info {
&.hidden {
visibility: hidden;
}
}
section {
margin-bottom: 2px;
}
.img {
margin-top: 5px;
img {
width: 100%;
}
}
.url {
a {
word-break: break-all;
}
}
.description {
.content {
textarea {
width: 100%;
}
}
}
}
.select-medias {
height: 40px;
margin-top: 5px;
text-align: right;
}
.upload-zone {
position: relative;
min-height: 100%;
padding: 20px 20px;
width: 100%;
border: 1px dotted #aaa;
border-radius: 2px;
box-sizing: border-box;
* {
box-sizing: border-box;
}
.dz-preview {
position: relative;
display: inline-block;
width: 120px;
margin: 0.5em;
}
.dz-preview .dz-progress {
display: block;
height: 15px;
border: 1px solid #aaa;
}
.dz-preview .dz-progress .dz-upload {
display: block;
height: 100%;
width: 0;
background: green;
}
.dz-preview .dz-error-message {
color: red;
display: none;
}
.dz-preview.dz-error .dz-error-message, .dz-preview.dz-error .dz-error-mark {
display: block;
}
.dz-preview.dz-success .dz-success-mark {
display: block;
}
.dz-preview .dz-error-mark, .dz-preview .dz-success-mark {
position: absolute;
display: none;
left: 30px;
top: 30px;
width: 54px;
height: 58px;
left: 50%;
margin-left: -27px;
}
/*
* The MIT License
* Copyright (c) 2012 Matias Meno <m@tias.me>
*/
@-webkit-keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px);
}
30%, 70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px);
}
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px);
}
}
@-moz-keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px);
}
30%, 70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px);
}
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px);
}
}
@keyframes passing-through {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px);
}
30%, 70% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px);
}
100% {
opacity: 0;
-webkit-transform: translateY(-40px);
-moz-transform: translateY(-40px);
-ms-transform: translateY(-40px);
-o-transform: translateY(-40px);
transform: translateY(-40px);
}
}
@-webkit-keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px);
}
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px);
}
}
@-moz-keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px);
}
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px);
}
}
@keyframes slide-in {
0% {
opacity: 0;
-webkit-transform: translateY(40px);
-moz-transform: translateY(40px);
-ms-transform: translateY(40px);
-o-transform: translateY(40px);
transform: translateY(40px);
}
30% {
opacity: 1;
-webkit-transform: translateY(0px);
-moz-transform: translateY(0px);
-ms-transform: translateY(0px);
-o-transform: translateY(0px);
transform: translateY(0px);
}
}
@-webkit-keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1);
}
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
}
@-moz-keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1);
}
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
}
@keyframes pulse {
0% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
10% {
-webkit-transform: scale(1.1);
-moz-transform: scale(1.1);
-ms-transform: scale(1.1);
-o-transform: scale(1.1);
transform: scale(1.1);
}
20% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
-ms-transform: scale(1);
-o-transform: scale(1);
transform: scale(1);
}
}
* {
box-sizing: border-box;
}
.dz-clickable {
cursor: pointer;
}
.dz-clickable * {
cursor: default;
}
.dz-clickable .dz-message, .dz-clickable .dz-message * {
cursor: pointer;
}
.dz-started .dz-message {
display: none;
}
.dz-drag-hover {
border-style: solid;
}
.dz-drag-hover .dz-message {
opacity: 0.5;
}
.dz-message {
text-align: center;
margin: 2em 0;
}
.dz-preview {
position: relative;
display: inline-block;
vertical-align: top;
margin: 16px;
min-height: 100px;
}
.dz-preview:hover {
z-index: 1000;
}
.dz-preview:hover .dz-details {
opacity: 1;
}
.dz-preview.dz-file-preview .dz-image {
border-radius: 20px;
background: #999;
background: linear-gradient(to bottom, #eee, #ddd);
}
.dz-preview.dz-file-preview .dz-details {
opacity: 1;
}
.dz-preview.dz-image-preview {
background: white;
}
.dz-preview.dz-image-preview .dz-details {
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
-ms-transition: opacity 0.2s linear;
-o-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.dz-preview .dz-remove {
font-size: 14px;
text-align: center;
display: block;
cursor: pointer;
border: none;
}
.dz-preview .dz-remove:hover {
text-decoration: underline;
}
.dz-preview:hover .dz-details {
opacity: 1;
}
.dz-preview .dz-details {
z-index: 20;
position: absolute;
top: 0;
left: 0;
opacity: 0;
font-size: 13px;
min-width: 100%;
max-width: 100%;
padding: 2em 1em;
text-align: center;
color: rgba(0, 0, 0, 0.9);
line-height: 150%;
}
.dz-preview .dz-details .dz-size {
margin-bottom: 1em;
font-size: 16px;
}
.dz-preview .dz-details .dz-filename {
white-space: nowrap;
}
.dz-preview .dz-details .dz-filename:hover span {
border: 1px solid rgba(200, 200, 200, 0.8);
background-color: rgba(255, 255, 255, 0.8);
}
.dz-preview .dz-details .dz-filename:not(:hover) {
overflow: hidden;
text-overflow: ellipsis;
}
.dz-preview .dz-details .dz-filename:not(:hover) span {
border: 1px solid transparent;
}
.dz-preview .dz-details .dz-filename span, .dz-preview .dz-details .dz-size span {
background-color: rgba(255, 255, 255, 0.4);
padding: 0 0.4em;
border-radius: 3px;
}
.dz-preview:hover .dz-image img {
-webkit-transform: scale(1.05, 1.05);
-moz-transform: scale(1.05, 1.05);
-ms-transform: scale(1.05, 1.05);
-o-transform: scale(1.05, 1.05);
transform: scale(1.05, 1.05);
-webkit-filter: blur(8px);
filter: blur(8px);
}
.dz-preview .dz-image {
border-radius: 20px;
overflow: hidden;
width: 120px;
height: 120px;
position: relative;
display: block;
z-index: 10;
}
.dz-preview .dz-image img {
display: block;
}
.dz-preview.dz-success .dz-success-mark {
-webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
-o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1);
}
.dz-preview.dz-error .dz-error-mark {
opacity: 1;
-webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
-o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1);
}
.dz-preview .dz-success-mark, .dz-preview .dz-error-mark {
pointer-events: none;
opacity: 0;
z-index: 500;
position: absolute;
display: block;
top: 50%;
left: 50%;
margin-left: -27px;
margin-top: -27px;
}
.dz-preview .dz-success-mark svg, .dz-preview .dz-error-mark svg {
display: block;
width: 54px;
height: 54px;
}
.dz-preview.dz-processing .dz-progress {
opacity: 1;
-webkit-transition: all 0.2s linear;
-moz-transition: all 0.2s linear;
-ms-transition: all 0.2s linear;
-o-transition: all 0.2s linear;
transition: all 0.2s linear;
}
.dz-preview.dz-complete .dz-progress {
opacity: 0;
-webkit-transition: opacity 0.4s ease-in;
-moz-transition: opacity 0.4s ease-in;
-ms-transition: opacity 0.4s ease-in;
-o-transition: opacity 0.4s ease-in;
transition: opacity 0.4s ease-in;
}
.dz-preview:not(.dz-processing) .dz-progress {
-webkit-animation: pulse 6s ease infinite;
-moz-animation: pulse 6s ease infinite;
-ms-animation: pulse 6s ease infinite;
-o-animation: pulse 6s ease infinite;
animation: pulse 6s ease infinite;
}
.dz-preview .dz-progress {
opacity: 1;
z-index: 1000;
pointer-events: none;
position: absolute;
height: 16px;
left: 50%;
top: 50%;
margin-top: -8px;
width: 80px;
margin-left: -40px;
background: rgba(255, 255, 255, 0.9);
-webkit-transform: scale(1);
border-radius: 8px;
overflow: hidden;
}
.dz-preview .dz-progress .dz-upload {
background: #333;
background: linear-gradient(to bottom, #666, #444);
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 0;
-webkit-transition: width 300ms ease-in-out;
-moz-transition: width 300ms ease-in-out;
-ms-transition: width 300ms ease-in-out;
-o-transition: width 300ms ease-in-out;
transition: width 300ms ease-in-out;
}
.dz-preview.dz-error .dz-error-message {
display: block;
}
.dz-preview.dz-error:hover .dz-error-message {
opacity: 1;
pointer-events: auto;
}
.dz-preview .dz-error-message {
pointer-events: none;
z-index: 1000;
position: absolute;
display: block;
display: none;
opacity: 0;
-webkit-transition: opacity 0.3s ease;
-moz-transition: opacity 0.3s ease;
-ms-transition: opacity 0.3s ease;
-o-transition: opacity 0.3s ease;
transition: opacity 0.3s ease;
border-radius: 8px;
font-size: 13px;
top: 130px;
left: -10px;
width: 140px;
background: #be2626;
background: linear-gradient(to bottom, #be2626, #a92222);
padding: 0.5em 1.2em;
color: white;
}
.dz-preview .dz-error-message:after {
content: '';
position: absolute;
top: -6px;
left: 64px;
width: 0;
height: 0;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #be2626;
}
}
}

View File

@ -34,6 +34,9 @@
<li class="nav-item px-3"> <li class="nav-item px-3">
<a href="{{ route('index') }}" class="nav-link">Front Stage</a> <a href="{{ route('index') }}" class="nav-link">Front Stage</a>
</li> </li>
<li class="nav-item px-3">
<a href="#" class="nav-link medialibrary">Media Library</a>
</li>
</ul> </ul>
<ul class="nav navbar-nav ml-auto"> <ul class="nav navbar-nav ml-auto">
@component('components.languageDropdown')@endcomponent @component('components.languageDropdown')@endcomponent

View File

@ -30,3 +30,36 @@ Route::group(['prefix' => config('admin.route'), 'middleware' => ['admin.area'],
Route::get('/', 'AdminPageController@index')->name('index'); Route::get('/', 'AdminPageController@index')->name('index');
}); });
/**
* 媒體庫
*/
Route::group(['prefix' => 'media', 'as' => 'media.'], function(){
Route::post('/', 'MediaLibraryController@fileUpload')
->name('upload');
Route::get('/', 'MediaLibraryController@getMedias')
->name('get');
Route::match(['put', 'patch'], '/{id}', 'MediaLibraryController@updateMedia')
->where('id', '[0-9]+')
->name('update');
Route::delete('/{id}', 'MediaLibraryController@deleteMedia')
->name('destroy');
Route::match(['put', 'patch'], '/update-category', 'MediaLibraryController@updateCategory')
->name('updateCategory');
});
/**
* 媒體分類
*/
Route::group(['prefix' => 'media-category', 'as' => 'media-category.'], function(){
Route::match(['put', 'patch'], '/{id}', 'MediaCategoryController@update')
->where('id', '[0-9]+')
->name('update');
Route::match(['put', 'patch'], '/update-order', 'MediaCategoryController@updateOrder')
->name('updateOrder');
Route::get('/', 'MediaCategoryController@index')
->name('index');
Route::post('/', 'MediaCategoryController@store')
->name('store');
Route::delete('/{id}', 'MediaCategoryController@destroy')
->name('destroy');
});