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() public function getAlbumSource()
{ {
// TODO allow albums to specify different storage locations - e.g. Amazon S3, SFTP/FTP, OpenStack // 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 interface IAlbumSource
{ {
function getOriginalsFolder();
/** /**
* Gets the absolute path or URL to the given photo file. * Gets the absolute path to the given photo file.
* @param Album $album * @param Album $album Album containing the photo.
* @param Photo $photo * @param Photo $photo Photo to get the path to.
* @param string $thumbnail Thumbnail to get the image to.
* @return string * @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. * 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 App\Photo;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
/**
* Driver for managing files on the local filesystem.
* @package App\AlbumSources
*/
class LocalFilesystemSource implements IAlbumSource class LocalFilesystemSource implements IAlbumSource
{ {
private $parentFolder; private $parentFolder;
@ -16,18 +20,42 @@ class LocalFilesystemSource implements IAlbumSource
$this->parentFolder = $parentFolder; $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) 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(); $extension = $uploadedFile->guessExtension();
if (!is_null($extension)) if (!is_null($extension))
@ -41,6 +69,6 @@ class LocalFilesystemSource implements IAlbumSource
private function getPathToAlbum(Album $album) 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->camera_software = $this->metadataCameraSoftware($exifData);
$photo->save(); $photo->save();
// Generate thumbnails // Generate and save thumbnails
$this->output->writeln('Generating thumbnails'); $this->output->writeln('Generating thumbnails');
$themeInfo = $this->themeHelper->info(); $themeInfo = $this->themeHelper->info();
$thumbnailsRequired = $themeInfo['thumbnails']; $thumbnailsRequired = $themeInfo['thumbnails'];
/** @var mixed $thumbnail */
foreach ($thumbnailsRequired as $thumbnail) foreach ($thumbnailsRequired as $thumbnail)
{ {
$this->imageHelper->generateThumbnail( $generatedThumbnailPath = $this->imageHelper->generateThumbnail($originalPhotoResource, $photo, $thumbnail);
$originalPhotoResource, $albumSource->saveThumbnail($album, $photo, $thumbnail, $generatedThumbnailPath);
$photo,
$albumSource,
$thumbnail
);
$this->output->writeln(sprintf('Thumbnail \'%s\' (%dx%d) created', $thumbnail['name'], $thumbnail['width'], $thumbnail['height'])); $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 class ImageHelper
{ {
public function generateThumbnail( public function generateThumbnail($gdImageResource, Photo $photo, $thumbnailInfo)
$gdImageResource,
Photo $photo,
IAlbumSource $storage,
$thumbnailInfo
)
{ {
$thumbnailWidth = intval($thumbnailInfo['width']); $thumbnailWidth = intval($thumbnailInfo['width']);
$thumbnailHeight = intval($thumbnailInfo['height']); $thumbnailHeight = intval($thumbnailInfo['height']);
@ -30,11 +25,16 @@ class ImageHelper
$photo->width, $photo->width,
$photo->height $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 // TODO make thumbnail quality configurable
imagejpeg($thumbnailImageResource, $tempName . '.jpg', 90); imagejpeg($thumbnailImageResource, $tempNameWithExtension, 90);
return $tempNameWithExtension;
} }
public function openImage($imagePath, &$imageInfo) public function openImage($imagePath, &$imageInfo)

View File

@ -4,6 +4,12 @@ namespace App\Helpers;
class MiscHelper 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) public static function randomString($length = 10)
{ {
$seed = 'abcdefghijklmnopqrstuvwxyz01234567890'; $seed = 'abcdefghijklmnopqrstuvwxyz01234567890';

View File

@ -2,6 +2,7 @@
namespace App\Http\Controllers\Gallery; namespace App\Http\Controllers\Gallery;
use App\Album;
use App\Facade\Theme; use App\Facade\Theme;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
@ -9,6 +10,10 @@ class DefaultController extends Controller
{ {
public function index() 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; namespace App\Providers;
use App\Album;
use App\Facade\Theme; use App\Facade\Theme;
use App\Helpers\ImageHelper; use App\Helpers\ImageHelper;
use App\Helpers\ThemeHelper; use App\Helpers\ThemeHelper;
@ -27,13 +28,11 @@ class AppServiceProvider extends ServiceProvider
return new ThemeHelper(); 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(); $this->addThemeInfoToView();
} $this->addAlbumsToView();
catch (QueryException $ex)
{
// When running migrations, the configuration table may not exist - so ignore and continue
} }
} }
@ -47,6 +46,12 @@ class AppServiceProvider extends ServiceProvider
// //
} }
private function addAlbumsToView()
{
$albums = Album::all()->sortBy('name');
View::share('albums', $albums);
}
private function addThemeInfoToView() private function addThemeInfoToView()
{ {
$themeInfo = Theme::info(); $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') @extends('themes.base.layout')
@section('title', 'Welcome') @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 --> <!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">
<ul class="nav navbar-nav"> <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"> <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"> <ul class="dropdown-menu">
<li><a href="#">Action</a></li> @foreach ($albums as $album)
<li><a href="#">Another action</a></li> <li><a href="{{ $album->url() }}">{{ $album->name }}</a></li>
<li><a href="#">Something else here</a></li> @endforeach
<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>
</ul> </ul>
</li> </li>
@ -40,12 +34,7 @@
</li> </li>
@endcan @endcan
</ul> </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"> <ul class="nav navbar-nav navbar-right">
{{-- Authentication Links --}} {{-- Authentication Links --}}
@if (Auth::guest()) @if (Auth::guest())
@ -54,16 +43,13 @@
@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">
<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> {{ Auth::user()->name }} <span class="caret"></span>
</a> </a>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li> <li>
<a href="{{ url('/logout') }}" <a href="{{ url('/logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">Logout</a>
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
Logout
</a>
<form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;"> <form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">
{{ csrf_field() }} {{ csrf_field() }}

View File

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