Implemented a progress bar for uploading photos, and allowed multiple uploads using the single upload file control
This commit is contained in:
parent
69f9b0fb41
commit
80dd1e4a40
@ -62,24 +62,46 @@ class ProcessUploadCommand extends Command
|
|||||||
{
|
{
|
||||||
$uploadsToProcess = Upload::where([
|
$uploadsToProcess = Upload::where([
|
||||||
['is_completed', false],
|
['is_completed', false],
|
||||||
['is_processing', false]
|
['is_processing', false],
|
||||||
|
['is_ready', true]
|
||||||
])
|
])
|
||||||
->orderBy('created_at')
|
->orderBy('created_at')
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
/** @var Upload $upload */
|
||||||
foreach ($uploadsToProcess as $upload)
|
foreach ($uploadsToProcess as $upload)
|
||||||
{
|
{
|
||||||
|
$upload->is_processing = 1;
|
||||||
|
$upload->save();
|
||||||
|
|
||||||
$this->output->writeln(sprintf('Processing upload #%d', $upload->id));
|
$this->output->writeln(sprintf('Processing upload #%d', $upload->id));
|
||||||
$this->handleUpload($upload);
|
$this->handleUpload($upload);
|
||||||
|
|
||||||
|
$upload->is_completed = 1;
|
||||||
|
$upload->is_processing = 0;
|
||||||
|
$upload->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function handleUpload(Upload $upload)
|
private function handleUpload(Upload $upload)
|
||||||
{
|
{
|
||||||
$photos = $upload->uploadPhotos;
|
$photos = $upload->uploadPhotos;
|
||||||
|
|
||||||
|
/** @var UploadPhoto $photo */
|
||||||
foreach ($photos as $photo)
|
foreach ($photos as $photo)
|
||||||
{
|
{
|
||||||
$this->handlePhoto($photo);
|
try
|
||||||
|
{
|
||||||
|
$this->handlePhoto($photo);
|
||||||
|
$upload->number_successful++;
|
||||||
|
}
|
||||||
|
catch (\Exception $ex)
|
||||||
|
{
|
||||||
|
$upload->number_failed++;
|
||||||
|
$photo->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
$upload->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,11 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command('twilight:process-uploads')
|
$schedule->command('twilight:process-uploads')
|
||||||
->everyMinute()
|
->everyMinute()
|
||||||
->when(function () {
|
->when(function () {
|
||||||
return (Upload::where('is_completed', 0)->count() > 0);
|
return (Upload::where([
|
||||||
|
'is_completed' => 0,
|
||||||
|
'is_processing' => 0,
|
||||||
|
'is_ready' => 1
|
||||||
|
])->count() > 0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ use App\Album;
|
|||||||
use App\Facade\Theme;
|
use App\Facade\Theme;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests;
|
use App\Http\Requests;
|
||||||
|
use App\Upload;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
|
|
||||||
@ -94,6 +95,22 @@ class AlbumController extends Controller
|
|||||||
return Theme::render('admin.edit_album', ['album' => $album]);
|
return Theme::render('admin.edit_album', ['album' => $album]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function monitorUpload($id, $upload_id)
|
||||||
|
{
|
||||||
|
$this->authorize('admin-access');
|
||||||
|
|
||||||
|
$upload = AlbumController::loadUpload($upload_id, $id);
|
||||||
|
|
||||||
|
return Theme::render('admin.album_upload_progress', ['upload' => $upload, 'album' => $upload->album]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function monitorUploadJson($id, $upload_id)
|
||||||
|
{
|
||||||
|
$this->authorize('admin-access');
|
||||||
|
|
||||||
|
return response()->json(AlbumController::loadUpload($upload_id, $id)->toArray());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified resource in storage.
|
* Update the specified resource in storage.
|
||||||
*
|
*
|
||||||
@ -142,4 +159,25 @@ class AlbumController extends Controller
|
|||||||
|
|
||||||
return $album;
|
return $album;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $id
|
||||||
|
* @param $albumId
|
||||||
|
* @return Upload|null
|
||||||
|
*/
|
||||||
|
private static function loadUpload($id, $albumId)
|
||||||
|
{
|
||||||
|
$upload = Upload::where([
|
||||||
|
'id' => intval($id),
|
||||||
|
'album_id' => intval($albumId)
|
||||||
|
])->first();
|
||||||
|
|
||||||
|
if (is_null($upload))
|
||||||
|
{
|
||||||
|
App::abort(404);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $upload;
|
||||||
|
}
|
||||||
}
|
}
|
@ -42,35 +42,49 @@ class PhotoController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('admin-access');
|
$this->authorize('admin-access');
|
||||||
|
|
||||||
/** @var UploadedFile $photoFile */
|
$photoFiles = $request->files->get('photo');
|
||||||
$photoFile = UploadedFile::createFromBase($request->files->get('photo'));
|
|
||||||
|
|
||||||
// Load the linked album
|
// Load the linked album
|
||||||
$album = AlbumController::loadAlbum($request->get('album_id'));
|
$album = AlbumController::loadAlbum($request->get('album_id'));
|
||||||
|
|
||||||
/** @var File $savedFile */
|
|
||||||
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($album, $photoFile);
|
|
||||||
|
|
||||||
$photo = new Photo();
|
|
||||||
$photo->album_id = $album->id;
|
|
||||||
$photo->name = $photoFile->getClientOriginalName();
|
|
||||||
$photo->file_name = $savedFile->getFilename();
|
|
||||||
$photo->mime_type = $savedFile->getMimeType();
|
|
||||||
$photo->file_size = $savedFile->getSize();
|
|
||||||
$photo->save();
|
|
||||||
|
|
||||||
$upload = new Upload();
|
$upload = new Upload();
|
||||||
|
$upload->album_id = $album->id;
|
||||||
$upload->is_completed = false;
|
$upload->is_completed = false;
|
||||||
$upload->is_processing = false;
|
$upload->is_processing = false;
|
||||||
$upload->number_photos = 1;
|
$upload->is_ready = false;
|
||||||
|
$upload->number_photos = 0;
|
||||||
$upload->save();
|
$upload->save();
|
||||||
|
|
||||||
$uploadPhoto = new UploadPhoto();
|
foreach ($photoFiles as $photoFile)
|
||||||
$uploadPhoto->upload_id = $upload->id;
|
{
|
||||||
$uploadPhoto->photo_id = $photo->id;
|
$photoFile = UploadedFile::createFromBase($photoFile);
|
||||||
$uploadPhoto->save();
|
|
||||||
|
|
||||||
exit();
|
/** @var File $savedFile */
|
||||||
|
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($album, $photoFile);
|
||||||
|
|
||||||
|
$photo = new Photo();
|
||||||
|
$photo->album_id = $album->id;
|
||||||
|
$photo->name = $photoFile->getClientOriginalName();
|
||||||
|
$photo->file_name = $savedFile->getFilename();
|
||||||
|
$photo->mime_type = $savedFile->getMimeType();
|
||||||
|
$photo->file_size = $savedFile->getSize();
|
||||||
|
$photo->save();
|
||||||
|
|
||||||
|
$upload->number_photos++;
|
||||||
|
|
||||||
|
$uploadPhoto = new UploadPhoto();
|
||||||
|
$uploadPhoto->upload_id = $upload->id;
|
||||||
|
$uploadPhoto->photo_id = $photo->id;
|
||||||
|
$uploadPhoto->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$upload->is_ready = true;
|
||||||
|
$upload->save();
|
||||||
|
|
||||||
|
return redirect(route('albums.monitorUpload', [
|
||||||
|
'id' => $album->id,
|
||||||
|
'upload_id' => $upload->id
|
||||||
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,6 +26,11 @@ class Upload extends Model
|
|||||||
protected $hidden = [
|
protected $hidden = [
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public function album()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Album::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function uploadPhotos()
|
public function uploadPhotos()
|
||||||
{
|
{
|
||||||
return $this->hasMany(UploadPhoto::class);
|
return $this->hasMany(UploadPhoto::class);
|
||||||
|
@ -15,12 +15,17 @@ class CreateUploadsTable extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('uploads', function (Blueprint $table) {
|
Schema::create('uploads', function (Blueprint $table) {
|
||||||
$table->increments('id');
|
$table->increments('id');
|
||||||
|
$table->unsignedInteger('album_id');
|
||||||
$table->boolean('is_completed');
|
$table->boolean('is_completed');
|
||||||
$table->boolean('is_processing');
|
$table->boolean('is_processing');
|
||||||
$table->integer('number_photos')->default(0);
|
$table->integer('number_photos')->default(0);
|
||||||
$table->integer('number_successful')->default(0);
|
$table->integer('number_successful')->default(0);
|
||||||
$table->integer('number_failed')->default(0);
|
$table->integer('number_failed')->default(0);
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('album_id')
|
||||||
|
->references('id')->on('albums')
|
||||||
|
->onDelete('cascade');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddUploadIsReadyColumn extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('uploads', function (Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->boolean('is_ready');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('uploads', function (Blueprint $table)
|
||||||
|
{
|
||||||
|
$table->dropColumn('is_ready');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
11
public/themes/bootstrap3/theme.css
vendored
11
public/themes/bootstrap3/theme.css
vendored
@ -13,9 +13,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navbar .avatar {
|
.navbar .avatar {
|
||||||
left: -28px;
|
border-radius: 20px;
|
||||||
position: absolute;
|
margin-left: 5px;
|
||||||
top: 9px;
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel .progress {
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-content {
|
.tab-content {
|
||||||
|
@ -0,0 +1,84 @@
|
|||||||
|
@extends('themes.base.layout')
|
||||||
|
@section('title', 'Processing...')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div class="container" style="margin-top: 40px;">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12 col-sm-8 col-sm-offset-2">
|
||||||
|
<div class="panel panel-default" id="status-panel">
|
||||||
|
<div class="panel-heading">Processing...</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Your upload is now being processed.</p>
|
||||||
|
<div id="progress-bar-container">
|
||||||
|
<div class="progress"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel panel-default" id="complete-panel" style="display: none;">
|
||||||
|
<div class="panel-heading">Upload completed</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>Your upload has completed.</p>
|
||||||
|
<div class="btn-toolbar btn-group-sm pull-right">
|
||||||
|
<a class="btn btn-default" href="{{ $album->url() }}">View album</a>
|
||||||
|
<a class="btn btn-primary" href="{{ route('albums.show', ['id' => $album->id]) }}">Back to album settings</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('scripts')
|
||||||
|
<script type="text/javascript">
|
||||||
|
var refreshInterval = null;
|
||||||
|
|
||||||
|
function refreshStatus()
|
||||||
|
{
|
||||||
|
$.get('{{ route('albums.monitorUploadJson', ['id' => $album->id, 'upload_id' => $upload->id]) }}', function(data)
|
||||||
|
{
|
||||||
|
var total = data.number_photos;
|
||||||
|
|
||||||
|
if (data.is_completed)
|
||||||
|
{
|
||||||
|
// Stop the refresh
|
||||||
|
window.clearInterval(refreshInterval);
|
||||||
|
|
||||||
|
// Display the complete box
|
||||||
|
$('#status-panel').hide();
|
||||||
|
$('#complete-panel').show();
|
||||||
|
}
|
||||||
|
|
||||||
|
var successPercentage = (data.number_successful / data.number_photos) * 100;
|
||||||
|
var failedPercentage = (data.number_failed / data.number_photos) * 100;
|
||||||
|
|
||||||
|
{{-- Render a Bootstrap-3 compatible progress bar --}}
|
||||||
|
var progressBar = $('<div/>').addClass('progress');
|
||||||
|
|
||||||
|
{{-- Successful --}}
|
||||||
|
var progressBarSuccess = $('<div/>')
|
||||||
|
.addClass('progress-bar')
|
||||||
|
.addClass('progress-bar-success')
|
||||||
|
.css('width', parseInt(successPercentage) + '%')
|
||||||
|
.appendTo(progressBar);
|
||||||
|
var progressBarSuccessSpan = $('<span/>').addClass('sr-only').html(parseInt(successPercentage) + '% successful').appendTo(progressBarSuccess);
|
||||||
|
|
||||||
|
{{-- Failed --}}
|
||||||
|
var progressBarError = $('<div/>')
|
||||||
|
.addClass('progress-bar')
|
||||||
|
.addClass('progress-bar-warning')
|
||||||
|
.css('width', parseInt(failedPercentage) + '%')
|
||||||
|
.appendTo(progressBar);
|
||||||
|
var progressBarErrorSpan = $('<span/>').addClass('sr-only').html(parseInt(failedPercentage) + '% failed').appendTo(progressBarError);
|
||||||
|
|
||||||
|
{{-- Add to DOM --}}
|
||||||
|
$('#progress-bar-container').html(progressBar[0].outerHTML);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
refreshInterval = window.setInterval(refreshStatus, 1000);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@endpush
|
@ -19,13 +19,13 @@
|
|||||||
{{-- Tab panes --}}
|
{{-- Tab panes --}}
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<div role="tabpanel" class="tab-pane active" id="upload-tab">
|
<div role="tabpanel" class="tab-pane active" id="upload-tab">
|
||||||
<h4>Upload a single image</h4>
|
<h4>Upload single images</h4>
|
||||||
|
|
||||||
{!! Form::open(['route' => 'photos.store', 'method' => 'POST', 'files' => true]) !!}
|
{!! Form::open(['route' => 'photos.store', 'method' => 'POST', 'files' => true]) !!}
|
||||||
{!! Form::hidden('album_id', $album->id) !!}
|
{!! Form::hidden('album_id', $album->id) !!}
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
{!! Form::file('photo', ['class' => 'control-label']) !!}
|
{!! Form::file('photo[]', ['class' => 'control-label', 'multiple' => 'multiple']) !!}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -42,9 +42,10 @@
|
|||||||
<li><a href="{{ url('/register') }}">Register</a></li>
|
<li><a href="{{ url('/register') }}">Register</a></li>
|
||||||
@else
|
@else
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" style="padding-top: 4px; padding-bottom: 4px;">
|
||||||
<img class="avatar" src="{{ \App\Helpers\MiscHelper::gravatarUrl(Auth::user()->email, 32) }}" alt="{{ Auth::user()->name }}" title="{{ Auth::user()->name }}" />
|
{{ Auth::user()->name }}
|
||||||
{{ Auth::user()->name }} <span class="caret"></span>
|
<img class="avatar" src="{{ \App\Helpers\MiscHelper::gravatarUrl(Auth::user()->email, 42) }}" alt="{{ Auth::user()->name }}" title="{{ Auth::user()->name }}" />
|
||||||
|
<span class="caret"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu" role="menu">
|
||||||
|
@ -17,8 +17,13 @@ Auth::routes();
|
|||||||
Route::group(['prefix' => 'admin'], function () {
|
Route::group(['prefix' => 'admin'], function () {
|
||||||
Route::get('/', 'Admin\DefaultController@index')->name('admin');
|
Route::get('/', 'Admin\DefaultController@index')->name('admin');
|
||||||
|
|
||||||
|
// Album management
|
||||||
Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete');
|
Route::get('albums/{id}/delete', 'Admin\AlbumController@delete')->name('albums.delete');
|
||||||
|
Route::get('albums/{id}/monitor/{uploadId}.json', 'Admin\AlbumController@monitorUploadJson')->name('albums.monitorUploadJson');
|
||||||
|
Route::get('albums/{id}/monitor/{uploadId}', 'Admin\AlbumController@monitorUpload')->name('albums.monitorUpload');
|
||||||
Route::resource('albums', 'Admin\AlbumController');
|
Route::resource('albums', 'Admin\AlbumController');
|
||||||
|
|
||||||
|
// Photo management
|
||||||
Route::resource('photos', 'Admin\PhotoController');
|
Route::resource('photos', 'Admin\PhotoController');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user