Albums are now displayed on the index page. Logged in users now get a gravatar displayed. Thumbnails are now also served and displayed on the index page.

This commit is contained in:
Andy Heathershaw 2016-09-03 22:13:05 +01:00
parent c2a65accdf
commit b08a0e4710
14 changed files with 237 additions and 74 deletions

View File

@ -49,6 +49,28 @@ 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');
return new LocalFilesystemSource(dirname(__DIR__) . '/storage/app/albums');
}
public function photos()
{
return $this->hasMany(Photo::class);
}
public function thumbnailUrl($thumbnailName)
{
$photo = Photo::where('album_id', $this->id)->first();
if (!is_null($photo))
{
return $this->getAlbumSource()->getUrlToPhoto($this, $photo, $thumbnailName);
}
return '';
}
public function url()
{
return sprintf('/%s', urlencode($this->url_alias));
}
}

View File

@ -8,15 +8,35 @@ use Symfony\Component\HttpFoundation\File\File;
interface IAlbumSource
{
function getOriginalsFolder();
/**
* Gets the absolute path or URL to the given photo file.
* @param Album $album
* @param Photo $photo
* 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);
function getPathToPhoto(Album $album, Photo $photo, $thumbnail);
function saveThumbnail(Album $album, Photo $photo, $thumbnailImageResource, $thumbnailInfo);
/**
* 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);
/**
* 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);
/**
* Saves an uploaded file to the container and returns the filename.

View File

@ -7,6 +7,10 @@ use App\Helpers\MiscHelper;
use App\Photo;
use Symfony\Component\HttpFoundation\File\File;
/**
* Driver for managing files on the local filesystem.
* @package App\AlbumSources
*/
class LocalFilesystemSource implements IAlbumSource
{
private $parentFolder;
@ -16,18 +20,42 @@ class LocalFilesystemSource implements IAlbumSource
$this->parentFolder = $parentFolder;
}
public function getPathToPhoto(Album $album, Photo $photo)
public function getOriginalsFolder()
{
return sprintf('%s/%s', $this->getPathToAlbum($album), $photo->file_name);
return '_originals';
}
public function saveThumbnail(Album $album, Photo $photo, $thumbnailImageResource, $thumbnailInfo)
public function getPathToPhoto(Album $album, Photo $photo, $thumbnail = null)
{
if (is_null($thumbnail))
{
$thumbnail = $this->getOriginalsFolder();
}
return sprintf('%s/%s/%s', $this->getPathToAlbum($album), $thumbnail, $photo->file_name);
}
public function getUrlToPhoto(Album $album, Photo $photo, $thumbnail = null)
{
$photoUrl = sprintf('%s/%s', urlencode($album->url_alias), urlencode($photo->file_name));
if (!is_null($thumbnail))
{
$photoUrl .= sprintf('?t=%s', urlencode($thumbnail));
}
return url($photoUrl);
}
public function saveThumbnail(Album $album, Photo $photo, $thumbnailInfo, $tempFilename)
{
$fileInfo = new File($tempFilename);
$fileInfo->move(sprintf('%s/%s', $this->getPathToAlbum($album), $thumbnailInfo['name']), $photo->file_name);
}
public function saveUploadedPhoto(Album $album, File $uploadedFile)
{
$tempFilename = sprintf('%s/photo_%s', $this->getPathToAlbum($album), MiscHelper::randomString(20));
$tempFilename = sprintf('%s/%s/%s', $this->getPathToAlbum($album), $this->getOriginalsFolder(), MiscHelper::randomString(20));
$extension = $uploadedFile->guessExtension();
if (!is_null($extension))
@ -41,6 +69,6 @@ class LocalFilesystemSource implements IAlbumSource
private function getPathToAlbum(Album $album)
{
return sprintf('%s/albums/%s', $this->parentFolder, $album->url_alias);
return sprintf('%s/%s', $this->parentFolder, $album->url_alias);
}
}

View File

@ -117,19 +117,16 @@ class ProcessUploadCommand extends Command
$photo->camera_software = $this->metadataCameraSoftware($exifData);
$photo->save();
// Generate thumbnails
// Generate and save thumbnails
$this->output->writeln('Generating thumbnails');
$themeInfo = $this->themeHelper->info();
$thumbnailsRequired = $themeInfo['thumbnails'];
/** @var mixed $thumbnail */
foreach ($thumbnailsRequired as $thumbnail)
{
$this->imageHelper->generateThumbnail(
$originalPhotoResource,
$photo,
$albumSource,
$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']));
}

View File

@ -7,12 +7,7 @@ use App\Photo;
class ImageHelper
{
public function generateThumbnail(
$gdImageResource,
Photo $photo,
IAlbumSource $storage,
$thumbnailInfo
)
public function generateThumbnail($gdImageResource, Photo $photo, $thumbnailInfo)
{
$thumbnailWidth = intval($thumbnailInfo['width']);
$thumbnailHeight = intval($thumbnailInfo['height']);
@ -30,11 +25,16 @@ class ImageHelper
$photo->width,
$photo->height
);
$tempName = tempnam('/tmp', 'blue_twilight_thumbnail_');
rename($tempName, $tempName . '.jpg');
// TODO make the /tmp folder configurable
$tempName = tempnam('/tmp', 'btw_thumb_');
$tempNameWithExtension = ($tempName . '.jpg');
rename($tempName, $tempNameWithExtension);
// TODO make thumbnail quality configurable
imagejpeg($thumbnailImageResource, $tempName . '.jpg', 90);
imagejpeg($thumbnailImageResource, $tempNameWithExtension, 90);
return $tempNameWithExtension;
}
public function openImage($imagePath, &$imageInfo)

View File

@ -4,6 +4,12 @@ namespace App\Helpers;
class MiscHelper
{
public static function gravatarUrl($emailAddress, $size = 48, $default = 'identicon')
{
$hash = md5(strtolower(trim($emailAddress)));
return sprintf('https://www.gravatar.com/avatar/%s?s=%d&d=%s', $hash, $size, $default);
}
public static function randomString($length = 10)
{
$seed = 'abcdefghijklmnopqrstuvwxyz01234567890';

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Gallery;
use App\Album;
use App\Facade\Theme;
use App\Http\Controllers\Controller;
@ -9,6 +10,10 @@ class DefaultController extends Controller
{
public function index()
{
return Theme::render('gallery.index');
$albums = Album::all()->sortBy('name');
return Theme::render('gallery.index', [
'albums' => $albums
]);
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Gallery;
use App\Album;
use app\Http\Controllers\Admin\AlbumController;
use App\Http\Controllers\Controller;
use App\Photo;
use Illuminate\Support\Facades\App;
use Symfony\Component\HttpFoundation\Request;
class PhotoController extends Controller
{
public function view(Request $request, $albumUrlAlias, $photoFilename)
{
$album = PhotoController::loadAlbumByAlias($albumUrlAlias);
$albumSource = $album->getAlbumSource();
$thumbnail = $request->get('t', $albumSource->getOriginalsFolder());
$photo = PhotoController::loadPhotoByAlbumAndFilename($album, $photoFilename);
return response()->file($albumSource->getPathToPhoto($album, $photo, $thumbnail));
}
/**
* @param $id
* @return Album
*/
public static function loadAlbumByAlias($alias)
{
$album = Album::where('url_alias', $alias)->first();
if (is_null($album))
{
App::abort(404);
return null;
}
return $album;
}
/**
* @param $id
* @return Photo
*/
public static function loadPhotoByAlbumAndFilename(Album $album, $filename)
{
$photo = Photo::where([
['album_id', $album->id],
['file_name', $filename]
])->first();
if (is_null($photo))
{
App::abort(404);
return null;
}
return $photo;
}
}

View File

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Album;
use App\Facade\Theme;
use App\Helpers\ImageHelper;
use App\Helpers\ThemeHelper;
@ -27,13 +28,11 @@ class AppServiceProvider extends ServiceProvider
return new ThemeHelper();
});
try
// When running migrations or CLI tasks, don't need to add things to the view
if (php_sapi_name() != 'cli')
{
$this->addThemeInfoToView();
}
catch (QueryException $ex)
{
// When running migrations, the configuration table may not exist - so ignore and continue
$this->addAlbumsToView();
}
}
@ -47,6 +46,12 @@ class AppServiceProvider extends ServiceProvider
//
}
private function addAlbumsToView()
{
$albums = Album::all()->sortBy('name');
View::share('albums', $albums);
}
private function addThemeInfoToView()
{
$themeInfo = Theme::info();

View File

@ -1,15 +0,0 @@
@import url(https://fonts.googleapis.com/css?family=Raleway:300,400,600);
* {
font-family: Raleway, sans-serif;
}
.form-actions {
text-align: right;
}
.tab-content {
border: solid 1px #ddd;
border-top: 0;
padding: 10px;
}

25
public/themes/bootstrap3/theme.css vendored Normal file
View File

@ -0,0 +1,25 @@
@import url(https://fonts.googleapis.com/css?family=Raleway:300,400,600);
* {
font-family: Raleway, sans-serif;
}
.album-index img {
max-width: 100%;
}
.form-actions {
text-align: right;
}
.navbar .avatar {
left: -28px;
position: absolute;
top: 9px;
}
.tab-content {
border: solid 1px #ddd;
border-top: 0;
padding: 10px;
}

View File

@ -1,2 +1,25 @@
@extends('themes.base.layout')
@section('title', 'Welcome')
@section('content')
<div class="container">
<div class="row">
@foreach ($albums as $album)
<div class="col-xs-12 col-sm-4">
<div class="panel panel-default album-index">
<div class="panel-heading"><a href="{{ $album->url() }}">{{ $album->name }}</a></div>
<div class="panel-body">
<p class="text-center">
<img src="{{ $album->thumbnailUrl('preview') }}"/>
</p>
<p>{{ $album->description }}</p>
</div>
<div class="panel-footer text-right">
<a href="{{ $album->url() }}" class="btn btn-sm btn-default">Open album</a>
</div>
</div>
</div>
@endforeach
</div>
</div>
@endsection

View File

@ -14,18 +14,12 @@
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav">
<li class="active"><a href="#">Link <span class="sr-only">(current)</span></a></li>
<li><a href="#">Link</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Dropdown <span class="caret"></span></a>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Albums <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="#">Action</a></li>
<li><a href="#">Another action</a></li>
<li><a href="#">Something else here</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">Separated link</a></li>
<li role="separator" class="divider"></li>
<li><a href="#">One more separated link</a></li>
@foreach ($albums as $album)
<li><a href="{{ $album->url() }}">{{ $album->name }}</a></li>
@endforeach
</ul>
</li>
@ -40,12 +34,7 @@
</li>
@endcan
</ul>
<form class="navbar-form navbar-left">
<div class="form-group">
<input type="text" class="form-control" placeholder="Search">
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>
<ul class="nav navbar-nav navbar-right">
{{-- Authentication Links --}}
@if (Auth::guest())
@ -54,16 +43,13 @@
@else
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<img class="avatar" src="{{ \App\Helpers\MiscHelper::gravatarUrl(Auth::user()->email, 32) }}" alt="{{ Auth::user()->name }}" title="{{ Auth::user()->name }}" />
{{ Auth::user()->name }} <span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<li>
<a href="{{ url('/logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
Logout
</a>
<a href="{{ url('/logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
<form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">
{{ csrf_field() }}

View File

@ -13,9 +13,6 @@
Auth::routes();
// Gallery
Route::get('/', 'Gallery\DefaultController@index')->name('home');
// Administration
Route::group(['prefix' => 'admin'], function () {
Route::get('/', 'Admin\DefaultController@index')->name('admin');
@ -24,3 +21,7 @@ Route::group(['prefix' => 'admin'], function () {
Route::resource('albums', 'Admin\AlbumController');
Route::resource('photos', 'Admin\PhotoController');
});
// Gallery
Route::get('/', 'Gallery\DefaultController@index')->name('home');
Route::get('/{albumUrlAlias}/{photoFilename}', 'Gallery\PhotoController@view')->name('viewPhoto');