Massive refactoring of the image processing, so it's now driven by the front-end and we can completely remove the command-line tasks - which will allow the app to work completely encoded using SourceGuardian and domain-locking.
This commit is contained in:
parent
821bfceb09
commit
56cfade23c
@ -30,15 +30,6 @@ class Album extends Model
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
public function fromRequest(Request $request)
|
||||
{
|
||||
$this->name = $request->get('name');
|
||||
$this->description = $request->get('description');
|
||||
$this->generateAlias();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function generateAlias()
|
||||
{
|
||||
$this->url_alias = ucfirst(preg_replace('/[^a-z0-9\-]/', '-', strtolower($this->name)));
|
||||
@ -50,7 +41,7 @@ class Album extends Model
|
||||
public function getAlbumSource()
|
||||
{
|
||||
// TODO allow albums to specify different storage locations - e.g. Amazon S3, SFTP/FTP, OpenStack
|
||||
return new LocalFilesystemSource(dirname(__DIR__) . '/storage/app/albums');
|
||||
return new LocalFilesystemSource($this, dirname(__DIR__) . '/storage/app/albums');
|
||||
}
|
||||
|
||||
public function photos()
|
||||
@ -66,7 +57,7 @@ class Album extends Model
|
||||
|
||||
if (!is_null($photo))
|
||||
{
|
||||
return $this->getAlbumSource()->getUrlToPhoto($this, $photo, $thumbnailName);
|
||||
return $this->getAlbumSource()->getUrlToPhoto($photo, $thumbnailName);
|
||||
}
|
||||
|
||||
return '';
|
||||
|
@ -12,37 +12,33 @@ interface IAlbumSource
|
||||
|
||||
/**
|
||||
* Gets the absolute path to the given photo file.
|
||||
* @param Album $album Album containing the photo.
|
||||
* @param Photo $photo Photo to get the path to.
|
||||
* @param string $thumbnail Thumbnail to get the image to.
|
||||
* @return string
|
||||
*/
|
||||
function getPathToPhoto(Album $album, Photo $photo, $thumbnail = null);
|
||||
function getPathToPhoto(Photo $photo, $thumbnail = null);
|
||||
|
||||
/**
|
||||
* Gets the absolute URL to the given photo file.
|
||||
* @param Album $album Album containing the photo.
|
||||
* @param Photo $photo Photo to get the URL to.
|
||||
* @param string $thumbnail Thumbnail to get the image to.
|
||||
* @return string
|
||||
*/
|
||||
function getUrlToPhoto(Album $album, Photo $photo, $thumbnail = null);
|
||||
function getUrlToPhoto(Photo $photo, $thumbnail = null);
|
||||
|
||||
/**
|
||||
* Saves a generated thumbnail to its permanent location.
|
||||
* @param Album $album Album containing the photo.
|
||||
* @param Photo $photo Photo the thumbnail relates to.
|
||||
* @param string $thumbnailInfo Information about the thumbnail.
|
||||
* @param string $tempFilename Filename containing the thumbnail.
|
||||
* @return mixed
|
||||
*/
|
||||
function saveThumbnail(Album $album, Photo $photo, $thumbnailInfo, $tempFilename);
|
||||
function saveThumbnail(Photo $photo, $thumbnailInfo, $tempFilename);
|
||||
|
||||
/**
|
||||
* Saves an uploaded file to the container and returns the filename.
|
||||
* @param Album $album The album containing the photo
|
||||
* @param File $uploadedFile The photo uploaded
|
||||
* @return File
|
||||
*/
|
||||
function saveUploadedPhoto(Album $album, File $uploadedFile);
|
||||
function saveUploadedPhoto(File $uploadedFile);
|
||||
}
|
@ -13,10 +13,19 @@ use Symfony\Component\HttpFoundation\File\File;
|
||||
*/
|
||||
class LocalFilesystemSource implements IAlbumSource
|
||||
{
|
||||
/**
|
||||
* @var Album
|
||||
*/
|
||||
private $album;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $parentFolder;
|
||||
|
||||
public function __construct($parentFolder)
|
||||
public function __construct(Album $album, $parentFolder)
|
||||
{
|
||||
$this->album = $album;
|
||||
$this->parentFolder = $parentFolder;
|
||||
}
|
||||
|
||||
@ -25,20 +34,20 @@ class LocalFilesystemSource implements IAlbumSource
|
||||
return '_originals';
|
||||
}
|
||||
|
||||
public function getPathToPhoto(Album $album, Photo $photo, $thumbnail = null)
|
||||
public function getPathToPhoto(Photo $photo, $thumbnail = null)
|
||||
{
|
||||
if (is_null($thumbnail))
|
||||
{
|
||||
$thumbnail = $this->getOriginalsFolder();
|
||||
}
|
||||
|
||||
return sprintf('%s/%s/%s', $this->getPathToAlbum($album), $thumbnail, $photo->storage_file_name);
|
||||
return sprintf('%s/%s/%s', $this->getPathToAlbum(), $thumbnail, $photo->storage_file_name);
|
||||
}
|
||||
|
||||
public function getUrlToPhoto(Album $album, Photo $photo, $thumbnail = null)
|
||||
public function getUrlToPhoto(Photo $photo, $thumbnail = null)
|
||||
{
|
||||
$photoUrl = route('downloadPhoto', [
|
||||
'albumUrlAlias' => $album->url_alias,
|
||||
'albumUrlAlias' => $this->album->url_alias,
|
||||
'photoFilename' => $photo->storage_file_name
|
||||
]);
|
||||
|
||||
@ -50,15 +59,15 @@ class LocalFilesystemSource implements IAlbumSource
|
||||
return $photoUrl;
|
||||
}
|
||||
|
||||
public function saveThumbnail(Album $album, Photo $photo, $thumbnailInfo, $tempFilename)
|
||||
public function saveThumbnail(Photo $photo, $thumbnailInfo, $tempFilename)
|
||||
{
|
||||
$fileInfo = new File($tempFilename);
|
||||
$fileInfo->move(sprintf('%s/%s', $this->getPathToAlbum($album), $thumbnailInfo['name']), $photo->storage_file_name);
|
||||
$fileInfo->move(sprintf('%s/%s', $this->getPathToAlbum(), $thumbnailInfo['name']), $photo->storage_file_name);
|
||||
}
|
||||
|
||||
public function saveUploadedPhoto(Album $album, File $uploadedFile)
|
||||
public function saveUploadedPhoto(File $uploadedFile)
|
||||
{
|
||||
$tempFilename = sprintf('%s/%s/%s', $this->getPathToAlbum($album), $this->getOriginalsFolder(), MiscHelper::randomString(20));
|
||||
$tempFilename = sprintf('%s/%s/%s', $this->getPathToAlbum(), $this->getOriginalsFolder(), MiscHelper::randomString(20));
|
||||
|
||||
$extension = $uploadedFile->guessExtension();
|
||||
if (!is_null($extension))
|
||||
@ -70,8 +79,8 @@ class LocalFilesystemSource implements IAlbumSource
|
||||
return new File($tempFilename);
|
||||
}
|
||||
|
||||
private function getPathToAlbum(Album $album)
|
||||
private function getPathToAlbum()
|
||||
{
|
||||
return sprintf('%s/%s', $this->parentFolder, $album->url_alias);
|
||||
return sprintf('%s/%s', $this->parentFolder, $this->album->url_alias);
|
||||
}
|
||||
}
|
@ -1,234 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Album;
|
||||
use App\Helpers\ImageHelper;
|
||||
use App\Helpers\ThemeHelper;
|
||||
use App\Photo;
|
||||
use App\Upload;
|
||||
use App\UploadPhoto;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ProcessUploadCommand extends Command
|
||||
{
|
||||
const METADATA_VERSION = 1;
|
||||
|
||||
/**
|
||||
* @var ImageHelper
|
||||
*/
|
||||
private $imageHelper;
|
||||
|
||||
/**
|
||||
* @var ThemeHelper
|
||||
*/
|
||||
private $themeHelper;
|
||||
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'twilight:process-uploads';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Processes uploads made through the web application.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ImageHelper $imageHelper, ThemeHelper $themeHelper)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->imageHelper = $imageHelper;
|
||||
$this->themeHelper = $themeHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$uploadsToProcess = Upload::where([
|
||||
['is_completed', false],
|
||||
['is_processing', false],
|
||||
['is_ready', true]
|
||||
])
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
/** @var Upload $upload */
|
||||
foreach ($uploadsToProcess as $upload)
|
||||
{
|
||||
$upload->is_processing = 1;
|
||||
$upload->save();
|
||||
|
||||
$this->output->writeln(sprintf('Processing upload #%d', $upload->id));
|
||||
$this->handleUpload($upload);
|
||||
|
||||
$upload->is_completed = 1;
|
||||
$upload->is_processing = 0;
|
||||
$upload->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function handleUpload(Upload $upload)
|
||||
{
|
||||
$photos = $upload->uploadPhotos;
|
||||
|
||||
/** @var UploadPhoto $photo */
|
||||
foreach ($photos as $photo)
|
||||
{
|
||||
try
|
||||
{
|
||||
$this->handlePhoto($photo);
|
||||
$upload->number_successful++;
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
$upload->number_failed++;
|
||||
$photo->photo->delete();
|
||||
$photo->delete();
|
||||
}
|
||||
|
||||
$upload->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function handlePhoto(UploadPhoto $uploadPhoto)
|
||||
{
|
||||
/** @var Photo $photo */
|
||||
$photo = $uploadPhoto->photo;
|
||||
|
||||
/** @var Album $album */
|
||||
$album = $photo->album;
|
||||
$albumSource = $album->getAlbumSource();
|
||||
|
||||
$photoFile = $albumSource->getPathToPhoto($album, $photo);
|
||||
|
||||
// Read and analyse Exif data
|
||||
$this->output->writeln(sprintf('Analysing photo #%d: %s', $photo->id, $photo->name));
|
||||
|
||||
// Open the photo
|
||||
$imageInfo = null;
|
||||
$originalPhotoResource = $this->imageHelper->openImage($photoFile, $imageInfo);
|
||||
$photo->width = $imageInfo[0];
|
||||
$photo->height = $imageInfo[1];
|
||||
$photo->mime_type = $imageInfo['mime'];
|
||||
|
||||
// Read the Exif data
|
||||
$exifData = @exif_read_data($photoFile);
|
||||
$isExifDataFound = ($exifData !== false && is_array($exifData));
|
||||
$angleToRotate = 0;
|
||||
|
||||
if ($isExifDataFound && isset($exifData['Orientation']))
|
||||
{
|
||||
switch ($exifData['Orientation'])
|
||||
{
|
||||
case 3:
|
||||
$angleToRotate = 180;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
$angleToRotate = 270;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
$angleToRotate = 90;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($angleToRotate > 0)
|
||||
{
|
||||
$originalPhotoResource = $this->imageHelper->rotateImage($originalPhotoResource, $angleToRotate);
|
||||
|
||||
if ($angleToRotate == 90 || $angleToRotate == 270)
|
||||
{
|
||||
$photo->width = $imageInfo[1];
|
||||
$photo->height = $imageInfo[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isExifDataFound)
|
||||
{
|
||||
$photo->metadata_version = ProcessUploadCommand::METADATA_VERSION;
|
||||
$photo->taken_at = $this->metadataDateTime($exifData);
|
||||
$photo->camera_make = $this->metadataCameraMake($exifData);
|
||||
$photo->camera_model = $this->metadataCameraModel($exifData);
|
||||
$photo->camera_software = $this->metadataCameraSoftware($exifData);
|
||||
$photo->rotation = $angleToRotate;
|
||||
}
|
||||
|
||||
$photo->save();
|
||||
|
||||
// Generate and save thumbnails
|
||||
$this->output->writeln('Generating thumbnails');
|
||||
$themeInfo = $this->themeHelper->info();
|
||||
$thumbnailsRequired = $themeInfo['thumbnails'];
|
||||
|
||||
/** @var mixed $thumbnail */
|
||||
foreach ($thumbnailsRequired as $thumbnail)
|
||||
{
|
||||
$generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $photo, $thumbnail);
|
||||
$albumSource->saveThumbnail($album, $photo, $thumbnail, $generatedThumbnailPath);
|
||||
|
||||
$this->output->writeln(sprintf('Thumbnail \'%s\' (%dx%d) created', $thumbnail['name'], $thumbnail['width'], $thumbnail['height']));
|
||||
}
|
||||
}
|
||||
|
||||
private function metadataCameraMake(array $exifData)
|
||||
{
|
||||
if (isset($exifData['Make']))
|
||||
{
|
||||
return $exifData['Make'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function metadataCameraModel(array $exifData)
|
||||
{
|
||||
if (isset($exifData['Model']))
|
||||
{
|
||||
return $exifData['Model'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function metadataCameraSoftware(array $exifData)
|
||||
{
|
||||
if (isset($exifData['Software']))
|
||||
{
|
||||
return $exifData['Software'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function metadataDateTime(array $exifData)
|
||||
{
|
||||
$dateTime = null;
|
||||
if (isset($exifData['DateTime']))
|
||||
{
|
||||
$dateTime = $exifData['DateTime'];
|
||||
}
|
||||
|
||||
if (!is_null($dateTime))
|
||||
{
|
||||
$dateTime = preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime);
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Album;
|
||||
use App\Helpers\ImageHelper;
|
||||
use App\Helpers\ThemeHelper;
|
||||
use App\Photo;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateThumbnailsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'twilight:regenerate-thumbnails {--album} {id}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Recreates thumbnails for an image or an album.';
|
||||
|
||||
/**
|
||||
* @var ImageHelper
|
||||
*/
|
||||
private $imageHelper;
|
||||
|
||||
/**
|
||||
* @var ThemeHelper
|
||||
*/
|
||||
private $themeHelper;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(ImageHelper $imageHelper, ThemeHelper $themeHelper)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->imageHelper = $imageHelper;
|
||||
$this->themeHelper = $themeHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = intval($this->argument('id'));
|
||||
if ($this->option('album'))
|
||||
{
|
||||
$this->handleAlbum($id);
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->handlePhoto($id);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleAlbum($id)
|
||||
{
|
||||
$album = Album::where('id', $id)->first();
|
||||
if (is_null($album))
|
||||
{
|
||||
throw new \Exception(sprintf('The album with ID %d could not be found', $id));
|
||||
}
|
||||
|
||||
/** @var Photo $photo */
|
||||
foreach ($album->photos as $photo)
|
||||
{
|
||||
$this->regenerateThumbnailsForPhoto($album, $photo);
|
||||
}
|
||||
}
|
||||
|
||||
private function handlePhoto($id)
|
||||
{
|
||||
$photo = Photo::where('id', $id)->first();
|
||||
if (is_null($photo))
|
||||
{
|
||||
throw new \Exception(sprintf('The photo with ID %d could not be found', $id));
|
||||
}
|
||||
|
||||
/** @var Album $album */
|
||||
$album = $photo->album;
|
||||
$this->regenerateThumbnailsForPhoto($album, $photo);
|
||||
}
|
||||
|
||||
private function regenerateThumbnailsForPhoto(Album $album, Photo $photo)
|
||||
{
|
||||
$albumSource = $album->getAlbumSource();
|
||||
|
||||
$originalPhotoResource = $this->imageHelper->openImage($albumSource->getPathToPhoto($album, $photo), $imageInfo);
|
||||
if (!is_resource($originalPhotoResource))
|
||||
{
|
||||
throw new \Exception(sprintf('The original image for photo ID %d could not be found', $id));
|
||||
}
|
||||
|
||||
$this->output->writeln(sprintf('Generating thumbnails for "%s"...', $photo->file_name));
|
||||
|
||||
$themeInfo = $this->themeHelper->info();
|
||||
$thumbnailsRequired = $themeInfo['thumbnails'];
|
||||
|
||||
/** @var mixed $thumbnail */
|
||||
foreach ($thumbnailsRequired as $thumbnail)
|
||||
{
|
||||
$generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $photo, $thumbnail);
|
||||
$albumSource->saveThumbnail($album, $photo, $thumbnail, $generatedThumbnailPath);
|
||||
|
||||
$this->output->writeln(sprintf('Thumbnail \'%s\' (%dx%d) created', $thumbnail['name'], $thumbnail['width'], $thumbnail['height']));
|
||||
}
|
||||
}
|
||||
}
|
@ -16,8 +16,6 @@ class Kernel extends ConsoleKernel
|
||||
* @var array
|
||||
*/
|
||||
protected $commands = [
|
||||
ProcessUploadCommand::class,
|
||||
RegenerateThumbnailsCommand::class
|
||||
];
|
||||
|
||||
/**
|
||||
@ -28,15 +26,6 @@ class Kernel extends ConsoleKernel
|
||||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('twilight:process-uploads')
|
||||
->everyMinute()
|
||||
->when(function () {
|
||||
return (Upload::where([
|
||||
'is_completed' => 0,
|
||||
'is_processing' => 0,
|
||||
'is_ready' => 1
|
||||
])->count() > 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
18
app/Facade/Image.php
Normal file
18
app/Facade/Image.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Facade;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Image extends Facade
|
||||
{
|
||||
/**
|
||||
* Get the registered name of the component.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return 'image';
|
||||
}
|
||||
}
|
@ -49,7 +49,6 @@ class ImageHelper
|
||||
public function openImage($imagePath, &$imageInfo)
|
||||
{
|
||||
$imageInfo = getimagesize($imagePath);
|
||||
|
||||
if ($imageInfo === false)
|
||||
{
|
||||
throw new \Exception(sprintf('The image "%s" does not appear to be a valid image, or cannot be read', pathinfo($imagePath, PATHINFO_FILENAME)));
|
||||
@ -97,4 +96,26 @@ class ImageHelper
|
||||
|
||||
return imagerotate($imageResource, $angle, 0);
|
||||
}
|
||||
|
||||
public function saveImage($imageResource, $imagePath, $imageInfo)
|
||||
{
|
||||
switch ($imageInfo[2])
|
||||
{
|
||||
case IMG_GIF:
|
||||
imagegif($imageResource, $imagePath);
|
||||
break;
|
||||
|
||||
case IMG_JPEG:
|
||||
imagejpeg($imageResource, $imagePath);
|
||||
break;
|
||||
|
||||
case IMG_PNG:
|
||||
imagepng($imageResource, $imagePath);
|
||||
break;
|
||||
|
||||
case IMG_WBMP:
|
||||
imagewbmp($imageResource, $imagePath);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,71 @@ use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
public function analyse($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = $this->loadAlbum($id);
|
||||
$photos = $album->photos()
|
||||
->where('is_analysed', false)
|
||||
->orderBy('created_at')
|
||||
->get();
|
||||
|
||||
return Theme::render('admin.album_analyse_progress', ['album' => $album, 'photos' => $photos]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
return Theme::render('admin.create_album');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = $this->loadAlbum($id);
|
||||
|
||||
return Theme::render('admin.delete_album', ['album' => $album]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = $this->loadAlbum($id);
|
||||
$album->delete();
|
||||
|
||||
return redirect(route('albums.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = $this->loadAlbum($id);
|
||||
|
||||
return Theme::render('admin.edit_album', ['album' => $album]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@ -32,27 +97,6 @@ class AlbumController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new resource.
|
||||
*
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
return Theme::render('admin.create_album');
|
||||
}
|
||||
|
||||
public function delete($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = AlbumController::loadAlbum($id);
|
||||
|
||||
return Theme::render('admin.delete_album', ['album' => $album]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified resource.
|
||||
*
|
||||
@ -63,7 +107,7 @@ class AlbumController extends Controller
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = AlbumController::loadAlbum($id);
|
||||
$album = $this->loadAlbum($id);
|
||||
$photos = $album->photos()
|
||||
->orderBy(DB::raw('COALESCE(taken_at, created_at)'))
|
||||
->paginate(UserConfig::get('items_per_page_admin'));
|
||||
@ -86,42 +130,13 @@ class AlbumController extends Controller
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = new Album();
|
||||
$album->fromRequest($request)->save();
|
||||
$album->fill($request->only(['name', 'description']));
|
||||
$album->generateAlias();
|
||||
$album->save();
|
||||
|
||||
return redirect(route('albums.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified resource.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function edit($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = AlbumController::loadAlbum($id);
|
||||
|
||||
return Theme::render('admin.edit_album', ['album' => $album]);
|
||||
}
|
||||
|
||||
public function monitorUpload($id, $uploadId)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$upload = AlbumController::loadUpload($uploadId, $id);
|
||||
|
||||
return Theme::render('admin.album_upload_progress', ['upload' => $upload, 'album' => $upload->album]);
|
||||
}
|
||||
|
||||
public function monitorUploadJson($id, $uploadId)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
return response()->json(AlbumController::loadUpload($uploadId, $id)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified resource in storage.
|
||||
*
|
||||
@ -133,33 +148,18 @@ class AlbumController extends Controller
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = AlbumController::loadAlbum($id);
|
||||
$album->fromRequest($request)->save();
|
||||
$album = $this->loadAlbum($id);
|
||||
$album->fill($request->only(['name', 'description']));
|
||||
$album->save();
|
||||
|
||||
return Theme::render('admin.show_album', ['album' => $album]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified resource from storage.
|
||||
*
|
||||
* @param int $id
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$album = $this->loadAlbum($id);
|
||||
$album->delete();
|
||||
|
||||
return redirect(route('albums.index'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return Album
|
||||
*/
|
||||
public static function loadAlbum($id)
|
||||
private function loadAlbum($id)
|
||||
{
|
||||
$album = Album::where('id', intval($id))->first();
|
||||
if (is_null($album))
|
||||
@ -170,25 +170,4 @@ class AlbumController extends Controller
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -3,8 +3,13 @@
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Album;
|
||||
use App\AlbumSources\IAlbumSource;
|
||||
use App\Facade\Image;
|
||||
use App\Facade\Theme;
|
||||
use App\Helpers\ImageHelper;
|
||||
use App\Helpers\MiscHelper;
|
||||
use App\Photo;
|
||||
use App\Services\PhotoService;
|
||||
use App\Upload;
|
||||
use App\UploadPhoto;
|
||||
use Illuminate\Http\Request;
|
||||
@ -16,6 +21,38 @@ use Symfony\Component\HttpFoundation\File\File;
|
||||
|
||||
class PhotoController extends Controller
|
||||
{
|
||||
public function analyse($photoId)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
/** @var Photo $photo */
|
||||
$photo = Photo::where('id', intval($photoId))->first();
|
||||
if (is_null($photo))
|
||||
{
|
||||
App::abort(404);
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = ['is_successful' => false, 'message' => ''];
|
||||
|
||||
try
|
||||
{
|
||||
$photoService = new PhotoService($photo);
|
||||
$photoService->analyse();
|
||||
|
||||
$result['is_successful'] = true;
|
||||
}
|
||||
catch (\Exception $ex)
|
||||
{
|
||||
$result['is_successful'] = false;
|
||||
$result['message'] = $ex->getMessage();
|
||||
|
||||
$photo->delete();
|
||||
}
|
||||
|
||||
return response()->json($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*
|
||||
@ -36,6 +73,27 @@ class PhotoController extends Controller
|
||||
//
|
||||
}
|
||||
|
||||
public function rotate($photoId, $angle)
|
||||
{
|
||||
$this->authorize('admin-access');
|
||||
|
||||
$photo = Photo::where('id', intval($photoId))->first();
|
||||
if (is_null($photo))
|
||||
{
|
||||
App::abort(404);
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($angle != 90 && $angle != 180 && $angle != 270)
|
||||
{
|
||||
App::aport(400);
|
||||
return null;
|
||||
}
|
||||
|
||||
$photoService = new PhotoService($photo);
|
||||
$photoService->rotate($angle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created resource in storage.
|
||||
*
|
||||
@ -49,22 +107,14 @@ class PhotoController extends Controller
|
||||
$photoFiles = $request->files->get('photo');
|
||||
|
||||
// Load the linked album
|
||||
$album = AlbumController::loadAlbum($request->get('album_id'));
|
||||
|
||||
$upload = new Upload();
|
||||
$upload->album_id = $album->id;
|
||||
$upload->is_completed = false;
|
||||
$upload->is_processing = false;
|
||||
$upload->is_ready = false;
|
||||
$upload->number_photos = 0;
|
||||
$upload->save();
|
||||
$album = $this->loadAlbum($request->get('album_id'));
|
||||
|
||||
foreach ($photoFiles as $photoFile)
|
||||
{
|
||||
$photoFile = UploadedFile::createFromBase($photoFile);
|
||||
|
||||
/** @var File $savedFile */
|
||||
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($album, $photoFile);
|
||||
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($photoFile);
|
||||
|
||||
$photo = new Photo();
|
||||
$photo->album_id = $album->id;
|
||||
@ -73,22 +123,12 @@ class PhotoController extends Controller
|
||||
$photo->storage_file_name = $savedFile->getFilename();
|
||||
$photo->mime_type = $savedFile->getMimeType();
|
||||
$photo->file_size = $savedFile->getSize();
|
||||
$photo->is_analysed = false;
|
||||
$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
|
||||
return redirect(route('albums.analyse', [
|
||||
'id' => $album->id
|
||||
]));
|
||||
}
|
||||
|
||||
@ -99,7 +139,7 @@ class PhotoController extends Controller
|
||||
$archiveFile = UploadedFile::createFromBase($request->files->get('archive'));
|
||||
|
||||
// Load the linked album
|
||||
$album = AlbumController::loadAlbum($request->get('album_id'));
|
||||
$album = $this->loadAlbum($request->get('album_id'));
|
||||
|
||||
// Create a temporary folder to hold the extracted files
|
||||
$tempFolder = sprintf('%s/btw_upload_%s', env('TEMP_FOLDER', '/tmp'), MiscHelper::randomString());
|
||||
@ -120,14 +160,6 @@ class PhotoController extends Controller
|
||||
return redirect(route('albums.show', ['id' => $album->id]));
|
||||
}
|
||||
|
||||
$upload = new Upload();
|
||||
$upload->album_id = $album->id;
|
||||
$upload->is_completed = false;
|
||||
$upload->is_processing = false;
|
||||
$upload->is_ready = false;
|
||||
$upload->number_photos = 0;
|
||||
$upload->save();
|
||||
|
||||
$di = new \RecursiveDirectoryIterator($tempFolder, \RecursiveDirectoryIterator::SKIP_DOTS);
|
||||
$recursive = new \RecursiveIteratorIterator($di);
|
||||
|
||||
@ -136,13 +168,26 @@ class PhotoController extends Controller
|
||||
{
|
||||
if ($fileInfo->isDir())
|
||||
{
|
||||
if ($fileInfo->getFilename() == '__MACOSX')
|
||||
{
|
||||
@rmdir($fileInfo->getPathname());
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = getimagesize($fileInfo->getPathname());
|
||||
if ($result === false)
|
||||
{
|
||||
// Not an image file - skip
|
||||
@unlink($fileInfo->getPathname());
|
||||
continue;
|
||||
}
|
||||
|
||||
$photoFile = new File($fileInfo->getPathname());
|
||||
|
||||
/** @var File $savedFile */
|
||||
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($album, $photoFile);
|
||||
$savedFile = $album->getAlbumSource()->saveUploadedPhoto($photoFile);
|
||||
|
||||
$photo = new Photo();
|
||||
$photo->album_id = $album->id;
|
||||
@ -151,22 +196,14 @@ class PhotoController extends Controller
|
||||
$photo->storage_file_name = $savedFile->getFilename();
|
||||
$photo->mime_type = $savedFile->getMimeType();
|
||||
$photo->file_size = $savedFile->getSize();
|
||||
$photo->is_analysed = false;
|
||||
$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();
|
||||
@rmdir($tempFolder);
|
||||
|
||||
return redirect(route('albums.monitorUpload', [
|
||||
'id' => $album->id,
|
||||
'upload_id' => $upload->id
|
||||
return redirect(route('albums.analyse', [
|
||||
'id' => $album->id
|
||||
]));
|
||||
}
|
||||
|
||||
@ -243,4 +280,20 @@ class PhotoController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return Album
|
||||
*/
|
||||
private function loadAlbum($id)
|
||||
{
|
||||
$album = Album::where('id', intval($id))->first();
|
||||
if (is_null($album))
|
||||
{
|
||||
App::abort(404);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $album;
|
||||
}
|
||||
}
|
||||
|
@ -8,13 +8,16 @@ use App\Facade\UserConfig;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AlbumController extends Controller
|
||||
{
|
||||
public function index($albumUrlAlias)
|
||||
{
|
||||
$album = AlbumController::loadAlbum($albumUrlAlias);
|
||||
$photos = $album->photos()->paginate(UserConfig::get('items_per_page'));
|
||||
$photos = $album->photos()
|
||||
->orderBy(DB::raw('COALESCE(taken_at, created_at)'))
|
||||
->paginate(UserConfig::get('items_per_page_admin'));
|
||||
|
||||
return Theme::render('gallery.album', [
|
||||
'album' => $album,
|
||||
|
@ -20,7 +20,7 @@ class PhotoController extends Controller
|
||||
$thumbnail = $request->get('t', $albumSource->getOriginalsFolder());
|
||||
$photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename);
|
||||
|
||||
return response()->file($albumSource->getPathToPhoto($album, $photo, $thumbnail));
|
||||
return response()->file($albumSource->getPathToPhoto($photo, $thumbnail));
|
||||
}
|
||||
|
||||
public function show($albumUrlAlias, $photoFilename)
|
||||
|
@ -28,7 +28,8 @@ class Photo extends Model
|
||||
'camera_model',
|
||||
'camera_software',
|
||||
'width',
|
||||
'height'
|
||||
'height',
|
||||
'is_analysed'
|
||||
];
|
||||
|
||||
/**
|
||||
@ -46,10 +47,7 @@ class Photo extends Model
|
||||
|
||||
public function thumbnailUrl($thumbnailName = null)
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = $this->album;
|
||||
|
||||
return $album->getAlbumSource()->getUrlToPhoto($album, $this, $thumbnailName);
|
||||
return $this->album->getAlbumSource()->getUrlToPhoto($this, $thumbnailName);
|
||||
}
|
||||
|
||||
public function url()
|
||||
|
206
app/Services/PhotoService.php
Normal file
206
app/Services/PhotoService.php
Normal file
@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Album;
|
||||
use App\AlbumSources\IAlbumSource;
|
||||
use App\Helpers\ImageHelper;
|
||||
use App\Helpers\ThemeHelper;
|
||||
use App\Photo;
|
||||
|
||||
class PhotoService
|
||||
{
|
||||
const METADATA_VERSION = 1;
|
||||
|
||||
/**
|
||||
* @var Album
|
||||
*/
|
||||
private $album;
|
||||
|
||||
/**
|
||||
* @var IAlbumSource
|
||||
*/
|
||||
private $albumSource;
|
||||
|
||||
/**
|
||||
* @var ImageHelper
|
||||
*/
|
||||
private $imageHelper;
|
||||
|
||||
/**
|
||||
* @var Photo
|
||||
*/
|
||||
private $photo;
|
||||
|
||||
/**
|
||||
* @var ThemeHelper
|
||||
*/
|
||||
private $themeHelper;
|
||||
|
||||
public function __construct(Photo $photo)
|
||||
{
|
||||
$this->photo = $photo;
|
||||
$this->album = $photo->album;
|
||||
$this->albumSource = $this->album->getAlbumSource();
|
||||
|
||||
$this->imageHelper = new ImageHelper();
|
||||
$this->themeHelper = new ThemeHelper();
|
||||
}
|
||||
|
||||
public function analyse()
|
||||
{
|
||||
/** @var Album $album */
|
||||
$album = $this->photo->album;
|
||||
$albumSource = $album->getAlbumSource();
|
||||
|
||||
$photoFile = $albumSource->getPathToPhoto($this->photo);
|
||||
|
||||
$imageInfo = null;
|
||||
$originalPhotoResource = $this->imageHelper->openImage($photoFile, $imageInfo);
|
||||
if ($originalPhotoResource === false)
|
||||
{
|
||||
throw new \Exception(sprintf('The image "%s" does not appear to be a valid image, or cannot be read', pathinfo($photoFile, PATHINFO_FILENAME)));
|
||||
}
|
||||
|
||||
$this->photo->width = $imageInfo[0];
|
||||
$this->photo->height = $imageInfo[1];
|
||||
$this->photo->mime_type = $imageInfo['mime'];
|
||||
|
||||
// Read the Exif data
|
||||
$exifData = @exif_read_data($photoFile);
|
||||
$isExifDataFound = ($exifData !== false && is_array($exifData));
|
||||
$angleToRotate = 0;
|
||||
|
||||
// If Exif data contains an Orientation, ensure we rotate the original image as such
|
||||
if ($isExifDataFound && isset($exifData['Orientation']))
|
||||
{
|
||||
switch ($exifData['Orientation'])
|
||||
{
|
||||
case 3:
|
||||
$angleToRotate = 180;
|
||||
break;
|
||||
|
||||
case 6:
|
||||
$angleToRotate = 270;
|
||||
break;
|
||||
|
||||
case 8:
|
||||
$angleToRotate = 90;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($angleToRotate > 0)
|
||||
{
|
||||
$originalPhotoResource = $this->imageHelper->rotateImage($originalPhotoResource, $angleToRotate);
|
||||
|
||||
if ($angleToRotate == 90 || $angleToRotate == 270)
|
||||
{
|
||||
$this->photo->width = $imageInfo[1];
|
||||
$this->photo->height = $imageInfo[0];
|
||||
}
|
||||
|
||||
$this->imageHelper->saveImage($originalPhotoResource, $photoFile, $imageInfo);
|
||||
}
|
||||
}
|
||||
|
||||
if ($isExifDataFound)
|
||||
{
|
||||
$this->photo->metadata_version = self::METADATA_VERSION;
|
||||
$this->photo->taken_at = $this->metadataDateTime($exifData);
|
||||
$this->photo->camera_make = $this->metadataCameraMake($exifData);
|
||||
$this->photo->camera_model = $this->metadataCameraModel($exifData);
|
||||
$this->photo->camera_software = $this->metadataCameraSoftware($exifData);
|
||||
}
|
||||
|
||||
$this->photo->is_analysed = true;
|
||||
$this->photo->save();
|
||||
|
||||
$this->regenerateThumbnails($originalPhotoResource);
|
||||
}
|
||||
|
||||
public function regenerateThumbnails($originalPhotoResource = null)
|
||||
{
|
||||
if (is_null($originalPhotoResource))
|
||||
{
|
||||
$imageInfo = null;
|
||||
$originalPhotoResource = $this->imageHelper->openImage($this->albumSource->getPathToPhoto($this->photo), $imageInfo);
|
||||
}
|
||||
|
||||
// Generate and save thumbnails
|
||||
$themeInfo = $this->themeHelper->info();
|
||||
$thumbnailsRequired = $themeInfo['thumbnails'];
|
||||
|
||||
/** @var mixed $thumbnail */
|
||||
foreach ($thumbnailsRequired as $thumbnail)
|
||||
{
|
||||
$generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $this->photo, $thumbnail);
|
||||
$this->albumSource->saveThumbnail($this->photo, $thumbnail, $generatedThumbnailPath);
|
||||
}
|
||||
}
|
||||
|
||||
public function rotate($angle)
|
||||
{
|
||||
$imageInfo = array();
|
||||
$photoPath = $this->albumSource->getPathToPhoto($this->photo);
|
||||
$originalPhotoImage = $this->imageHelper->openImage($photoPath, $imageInfo);
|
||||
$originalPhotoImage = $this->imageHelper->rotateImage($originalPhotoImage, intval($angle));
|
||||
$this->imageHelper->saveImage($originalPhotoImage, $photoPath, $imageInfo);
|
||||
|
||||
if ($angle == 90 || $angle == 270)
|
||||
{
|
||||
$width = $this->photo->width;
|
||||
$this->photo->width = $this->photo->height;
|
||||
$this->photo->height = $width;
|
||||
}
|
||||
|
||||
$this->regenerateThumbnails($originalPhotoImage);
|
||||
|
||||
$this->photo->save();
|
||||
}
|
||||
|
||||
private function metadataCameraMake(array $exifData)
|
||||
{
|
||||
if (isset($exifData['Make']))
|
||||
{
|
||||
return $exifData['Make'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function metadataCameraModel(array $exifData)
|
||||
{
|
||||
if (isset($exifData['Model']))
|
||||
{
|
||||
return $exifData['Model'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function metadataCameraSoftware(array $exifData)
|
||||
{
|
||||
if (isset($exifData['Software']))
|
||||
{
|
||||
return $exifData['Software'];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function metadataDateTime(array $exifData)
|
||||
{
|
||||
$dateTime = null;
|
||||
if (isset($exifData['DateTime']))
|
||||
{
|
||||
$dateTime = $exifData['DateTime'];
|
||||
}
|
||||
|
||||
if (!is_null($dateTime))
|
||||
{
|
||||
$dateTime = preg_replace('/^([\d]{4}):([\d]{2}):([\d]{2})/', '$1-$2-$3', $dateTime);
|
||||
}
|
||||
|
||||
return $dateTime;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class Upload extends Model
|
||||
{
|
||||
use Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'is_completed', 'is_processing', 'number_photos', 'number_successful', 'number_failed'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
public function album()
|
||||
{
|
||||
return $this->belongsTo(Album::class);
|
||||
}
|
||||
|
||||
public function uploadPhotos()
|
||||
{
|
||||
return $this->hasMany(UploadPhoto::class);
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class UploadPhoto extends Model
|
||||
{
|
||||
use Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = [
|
||||
'upload_id', 'photo_id'
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for arrays.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hidden = [
|
||||
];
|
||||
|
||||
public function photo()
|
||||
{
|
||||
return $this->belongsTo(Photo::class);
|
||||
}
|
||||
}
|
@ -229,8 +229,9 @@ return [
|
||||
// Additional aliases added by AH
|
||||
'Form' => Collective\Html\FormFacade::class,
|
||||
'Html' => Collective\Html\HtmlFacade::class,
|
||||
'Theme' => App\Facade\Theme::class,
|
||||
'UserConfig' => App\Facade\UserConfig::class
|
||||
'Image' => \App\Facade\Image::class,
|
||||
'Theme' => \App\Facade\Theme::class,
|
||||
'UserConfig' => \App\Facade\UserConfig::class
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class RemovePhotoRotation extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
$table->dropColumn('rotation');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
$table->integer('rotation')->nullable();
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class RemoveUploadTables extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::dropIfExists('upload_photos');
|
||||
Schema::dropIfExists('uploads');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::create('uploads', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->unsignedInteger('album_id');
|
||||
$table->boolean('is_completed');
|
||||
$table->boolean('is_processing');
|
||||
$table->integer('number_photos')->default(0);
|
||||
$table->integer('number_successful')->default(0);
|
||||
$table->integer('number_failed')->default(0);
|
||||
$table->boolean('is_ready');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('album_id')
|
||||
->references('id')->on('albums')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
|
||||
Schema::create('upload_photos', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->unsignedInteger('upload_id');
|
||||
$table->unsignedBigInteger('photo_id');
|
||||
$table->boolean('is_ready');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('upload_id')
|
||||
->references('id')->on('uploads')
|
||||
->onDelete('cascade');
|
||||
$table->foreign('photo_id')
|
||||
->references('id')->on('photos')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddPhotoIsAnalysedColumn extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
$table->boolean('is_analysed');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('photos', function (Blueprint $table) {
|
||||
$table->dropColumn('is_analysed');
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,126 @@
|
||||
@extends('themes.base.layout')
|
||||
@section('title', 'Analysing...')
|
||||
|
||||
@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">Analysing...</div>
|
||||
<div class="panel-body">
|
||||
<p>Your uploaded photos are now being analysed.</p>
|
||||
<div id="progress-bar-container">
|
||||
<div class="progress"></div>
|
||||
</div>
|
||||
|
||||
<div id="file-list" style="margin-top: 20px;">
|
||||
@foreach ($photos as $photo)
|
||||
<p data-photo-id="{{ $photo->id }}">{{ $photo->name }} ... <i class="fa fa-fw"></i></p>
|
||||
@endforeach
|
||||
</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 number_successful = 0;
|
||||
var number_error = 0;
|
||||
var number_total = 0;
|
||||
|
||||
function redrawProgressBar()
|
||||
{
|
||||
var successPercentage = (number_successful / number_total) * 100;
|
||||
var failedPercentage = (number_error / number_total) * 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() {
|
||||
number_total = $('#file-list p').length;
|
||||
|
||||
if (number_total == 0) {
|
||||
$('#status-panel').hide();
|
||||
$('#complete-panel').show();
|
||||
}
|
||||
else
|
||||
{
|
||||
$('#file-list p').each(function (index, element) {
|
||||
var photo_id = $(element).data('photo-id');
|
||||
var url = '{{ route('photos.analyse', ['id' => 0]) }}';
|
||||
url = url.replace(/0$/, photo_id);
|
||||
|
||||
$.ajax(
|
||||
url,
|
||||
{
|
||||
complete: function () {
|
||||
redrawProgressBar();
|
||||
|
||||
if (number_successful + number_error >= number_total) {
|
||||
$('#status-panel').hide();
|
||||
$('#complete-panel').show();
|
||||
}
|
||||
},
|
||||
dataType: 'json',
|
||||
error: function (xhr, textStatus, errorThrown) {
|
||||
$('i', '#file-list p[data-photo-id=' + photo_id + ']')
|
||||
.addClass('text-danger')
|
||||
.addClass('fa-times');
|
||||
number_error++;
|
||||
},
|
||||
method: 'POST',
|
||||
success: function (data) {
|
||||
if (data.is_successful) {
|
||||
$('i', '#file-list p[data-photo-id=' + photo_id + ']')
|
||||
.addClass('text-success')
|
||||
.addClass('fa-check');
|
||||
number_successful++;
|
||||
}
|
||||
else {
|
||||
$('i', '#file-list p[data-photo-id=' + photo_id + ']')
|
||||
.addClass('text-danger')
|
||||
.addClass('fa-times');
|
||||
number_error++;
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@endpush
|
@ -1,84 +0,0 @@
|
||||
@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
|
@ -93,10 +93,41 @@
|
||||
|
||||
@push('scripts')
|
||||
<script type="text/javascript">
|
||||
function rotatePhoto(photo_id, angle, parent)
|
||||
{
|
||||
var url = '{{ route('photos.rotate', ['id' => 0, 'angle' => 1]) }}';
|
||||
url = url.replace('/0/', '/' + photo_id + '/');
|
||||
url = url.replace(/\/1$/, '/' + angle);
|
||||
|
||||
$.post(url, function()
|
||||
{
|
||||
var image = $('img.photo-thumbnail', parent);
|
||||
var originalUrl = image.data('original-src');
|
||||
image.attr('src', originalUrl + "&_=" + new Date().getTime());
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#upload-button').click(function() {
|
||||
$('.nav-tabs a[href="#upload-tab"]').tab('show');
|
||||
});
|
||||
|
||||
$('a.rotate-photo-left').click(function() {
|
||||
var parent = $(this).parents('.photo');
|
||||
var photo_id = $(parent).data('photo-id');
|
||||
|
||||
rotatePhoto(photo_id, 90, parent);
|
||||
$(this).dropdown('toggle');
|
||||
return false;
|
||||
});
|
||||
$('a.rotate-photo-right').click(function() {
|
||||
var parent = $(this).parents('.photo');
|
||||
var photo_id = $(parent).data('photo-id');
|
||||
|
||||
rotatePhoto(photo_id, 270, parent);
|
||||
$(this).dropdown('toggle');
|
||||
return false;
|
||||
});
|
||||
})
|
||||
</script>
|
||||
@endpush
|
@ -9,6 +9,8 @@
|
||||
<title>@yield('title') | {{ UserConfig::get('app_name') }}</title>
|
||||
<base href="{{ url('/') }}">
|
||||
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
|
||||
{{-- Cannot use $theme_url here: if a theme uses the base layout, it would also have to provide all these dependencies! --}}
|
||||
{{-- As these files are shipped with core (not a theme) use the main app.version instead of the current theme's version --}}
|
||||
<link href="themes/base/bootstrap/css/bootstrap.min.css?v={{ urlencode(config('app.version')) }}" rel="stylesheet">
|
||||
@ -59,6 +61,13 @@
|
||||
<script src="themes/base/js/jquery.min.js?v={{ urlencode(config('app.version')) }}"></script>
|
||||
<script src="themes/base/bootstrap/js/bootstrap.min.js?v={{ urlencode(config('app.version')) }}"></script>
|
||||
<script src="themes/base/js/app.js?v={{ urlencode(config('app.version')) }}"></script>
|
||||
<script type="text/javascript">
|
||||
$.ajaxSetup({
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,10 +1,21 @@
|
||||
@php ($field_prefix = sprintf('photo[%d]', $photo->id))
|
||||
<hr/>
|
||||
<div class="photo row">
|
||||
<div class="photo row" data-photo-id="{{ $photo->id }}" style="position: relative;">
|
||||
<div class="col-xs-12 col-sm-2 text-center">
|
||||
<a href="{{ $photo->thumbnailUrl() }}" target="_blank">
|
||||
<img src="{{ $photo->thumbnailUrl('admin-preview') }}" style="max-width: 100%;"/>
|
||||
</a>
|
||||
<img class="photo-thumbnail" src="{{ $photo->thumbnailUrl('admin-preview') }}" data-original-src="{{ $photo->thumbnailUrl('admin-preview') }}" style="max-width: 100%;"/>
|
||||
</a><br/>
|
||||
<div class="btn-toolbar" role="toolbar" style="margin-top: 5px;">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fa fa-fw fa-rotate-right"></i> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="#" class="rotate-photo-left"><i class="fa fa-fw fa-rotate-left"></i> Rotate left</a></li>
|
||||
<li><a href="#" class="rotate-photo-right"><i class="fa fa-fw fa-rotate-right"></i> Rotate right</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-12 col-sm-10">
|
||||
<div class="form-group">
|
||||
|
@ -21,12 +21,13 @@ Route::group(['prefix' => 'admin'], function () {
|
||||
Route::get('settings', 'Admin\DefaultController@settings')->name('admin.settings');
|
||||
|
||||
// Album management
|
||||
Route::get('albums/{id}/analyse', 'Admin\AlbumController@analyse')->name('albums.analyse');
|
||||
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');
|
||||
|
||||
// Photo management
|
||||
Route::post('photos/analyse/{id}', 'Admin\PhotoController@analyse')->name('photos.analyse');
|
||||
Route::post('photos/rotate/{photoId}/{angle}', 'Admin\PhotoController@rotate')->name('photos.rotate');
|
||||
Route::post('photos/store-bulk', 'Admin\PhotoController@storeBulk')->name('photos.storeBulk');
|
||||
Route::put('photos/update-bulk/{albumId}', 'Admin\PhotoController@updateBulk')->name('photos.updateBulk');
|
||||
Route::resource('photos', 'Admin\PhotoController');
|
||||
|
Loading…
Reference in New Issue
Block a user