refs #3: implemented multiple independent views for an album and created a slideshow view

This commit is contained in:
Andy Heathershaw 2016-10-05 11:49:39 +01:00
parent 8990c31a5f
commit 58b055e9cd
12 changed files with 241 additions and 7 deletions

View File

@ -43,6 +43,19 @@ class MiscHelper
return sprintf('https://www.gravatar.com/avatar/%s?s=%d&d=%s', $hash, $size, $default); return sprintf('https://www.gravatar.com/avatar/%s?s=%d&d=%s', $hash, $size, $default);
} }
/**
* Tests whether the provided URL belongs to the current application (i.e. both scheme and hostname match.)
* @param $url
* @return bool
*/
public static function isSafeUrl($url)
{
$parts = parse_url($url);
$validParts = parse_url(url('/'));
return ($parts['scheme'] == $validParts['scheme'] && $parts['host'] == $validParts['host']);
}
public static function randomString($length = 10) public static function randomString($length = 10)
{ {
$seed = 'abcdefghijklmnopqrstuvwxyz01234567890'; $seed = 'abcdefghijklmnopqrstuvwxyz01234567890';

View File

@ -14,7 +14,7 @@ use Illuminate\Support\Facades\DB;
class AlbumController extends Controller class AlbumController extends Controller
{ {
public function index($albumUrlAlias) public function index(Request $request, $albumUrlAlias)
{ {
$album = DbHelper::loadAlbumByUrlAlias($albumUrlAlias); $album = DbHelper::loadAlbumByUrlAlias($albumUrlAlias);
if (is_null($album)) if (is_null($album))
@ -23,14 +23,31 @@ class AlbumController extends Controller
return null; return null;
} }
$validViews = ['default', 'slideshow'];
$requestedView = strtolower($request->get('view'));
if (!in_array($requestedView, $validViews))
{
$requestedView = $validViews[0];
}
$this->authorizeForUser($this->getUser(), 'album.view', $album); $this->authorizeForUser($this->getUser(), 'album.view', $album);
// The slideshow view needs access to all photos, not paged
$photos = $album->photos() $photos = $album->photos()
->orderBy(DB::raw('COALESCE(taken_at, created_at)')) ->orderBy(DB::raw('COALESCE(taken_at, created_at)'));
->paginate(UserConfig::get('items_per_page'));
return Theme::render('gallery.album', [ if ($requestedView != 'slideshow')
{
$photos = $photos->paginate(UserConfig::get('items_per_page'));
}
else
{
$photos = $photos->get();
}
return Theme::render(sprintf('gallery.album_%s', $requestedView), [
'album' => $album, 'album' => $album,
'current_view' => $requestedView,
'photos' => $photos 'photos' => $photos
]); ]);
} }

View File

@ -6,6 +6,7 @@ use App\Album;
use App\Facade\Theme; use App\Facade\Theme;
use App\Facade\UserConfig; use App\Facade\UserConfig;
use App\Helpers\DbHelper; use App\Helpers\DbHelper;
use App\Helpers\MiscHelper;
use app\Http\Controllers\Admin\AlbumController; use app\Http\Controllers\Admin\AlbumController;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Middleware\VerifyCsrfToken; use App\Http\Middleware\VerifyCsrfToken;
@ -58,7 +59,7 @@ class PhotoController extends Controller
return response()->file($album->getAlbumSource()->getPathToPhoto($photo, $thumbnail)); return response()->file($album->getAlbumSource()->getPathToPhoto($photo, $thumbnail));
} }
public function show($albumUrlAlias, $photoFilename) public function show(Request $request, $albumUrlAlias, $photoFilename)
{ {
$album = DbHelper::loadAlbumByUrlAlias($albumUrlAlias); $album = DbHelper::loadAlbumByUrlAlias($albumUrlAlias);
if (is_null($album)) if (is_null($album))
@ -73,10 +74,18 @@ class PhotoController extends Controller
$isOriginalAllowed = Gate::forUser($this->getUser())->allows('photo.download_original', $photo); $isOriginalAllowed = Gate::forUser($this->getUser())->allows('photo.download_original', $photo);
$returnAlbumUrl = $album->url();
$referer = $request->headers->get('Referer');
if (strlen($referer) > 0 && MiscHelper::isSafeUrl($referer))
{
$returnAlbumUrl = $referer;
}
return Theme::render('gallery.photo', [ return Theme::render('gallery.photo', [
'album' => $album, 'album' => $album,
'is_original_allowed' => $isOriginalAllowed, 'is_original_allowed' => $isOriginalAllowed,
'photo' => $photo 'photo' => $photo,
'return_album_url' => $returnAlbumUrl
]); ]);
} }

View File

@ -1,3 +1,16 @@
.album-slideshow-container .image-preview {
height: 600px;
max-width: 100%;
width: 800px;
}
.album-slideshow-container .thumbnails {
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
width: auto;
}
.no-margin-bottom { .no-margin-bottom {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -364,6 +364,67 @@ function EditPhotosViewModel(album_id, language, urls) {
}; };
} }
function SlideShowViewModel(required_timeout_ms) {
var self = this;
self.current = ko.observable();
self.currentIndex = ko.observable(0);
self.images = ko.observableArray();
self.interval = null;
self.isRunning = ko.observable(false);
self.isPaused = ko.observable(false);
self.changeCurrentImage = function(photo_id)
{
for (var i = 0; i < self.images().length; i++)
{
var this_image = self.images()[i];
if (this_image.id == photo_id)
{
self.current(this_image);
self.currentIndex(i);
window.clearInterval(self.interval);
self.interval = window.setInterval(self.rotateNextImage, required_timeout_ms);
return;
}
}
};
self.continueSlideshow = function() {
self.isPaused(false);
self.interval = window.setInterval(self.rotateNextImage, required_timeout_ms);
};
self.pauseSlideshow = function() {
self.isPaused(true);
window.clearInterval(self.interval);
};
self.rotateNextImage = function() {
var next_index = self.currentIndex() + 1;
if (next_index >= self.images().length)
{
next_index = 0;
}
self.current(self.images()[next_index]);
self.currentIndex(next_index);
};
self.startSlideshow = function() {
if (self.images().length <= 0)
{
return;
}
self.interval = window.setInterval(self.rotateNextImage, required_timeout_ms);
self.current(self.images()[0]);
self.isRunning(true);
};
}
function StorageLocationsViewModel() { function StorageLocationsViewModel() {
var self = this; var self = this;

View File

@ -3,6 +3,7 @@ return [
'activate_user_label' => 'Manually activate this account', 'activate_user_label' => 'Manually activate this account',
'admin_user_label' => 'User is an administrator', 'admin_user_label' => 'User is an administrator',
'album_source_label' => 'Storage location:', 'album_source_label' => 'Storage location:',
'album_view_label' => 'View as:',
'apply_action' => 'Apply', 'apply_action' => 'Apply',
'bulk_edit_photos_label' => 'Bulk edit selected photos:', 'bulk_edit_photos_label' => 'Bulk edit selected photos:',
'bulk_edit_photos_placeholder' => 'Select an action', 'bulk_edit_photos_placeholder' => 'Select an action',

View File

@ -1,5 +1,10 @@
<?php <?php
return [ return [
'album_views' => [
'default' => 'Default',
'slideshow' => 'Slideshow'
],
'back_to_album' => 'Back to :name',
'index_no_results_heading' => 'This gallery is empty!', 'index_no_results_heading' => 'This gallery is empty!',
'index_no_results_text' => 'If you are the owner of this gallery, you can create new albums and upload photos using the :admin_link.' 'index_no_results_text' => 'If you are the owner of this gallery, you can create new albums and upload photos using the :admin_link.'
]; ];

View File

@ -16,6 +16,10 @@
<div class="container album-container"> <div class="container album-container">
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="pull-right">
@include(\App\Facade\Theme::viewName('partials.album_view_selector'))
</div>
<h1 class="page-title">{{ $album->name }}</h1> <h1 class="page-title">{{ $album->name }}</h1>
<p>{{ $album->description }}</p> <p>{{ $album->description }}</p>
<hr/> <hr/>

View File

@ -0,0 +1,81 @@
@extends('themes.base.layout')
@section('title', $album->name)
@section('breadcrumb')
<div class="breadcrumb">
<div class="container">
<ol class="breadcrumb">
<li><a href="{{ route('home') }}">Gallery</a></li>
<li class="active">{{ $album->name }}</li>
</ol>
</div>
</div>
@endsection
@section('content')
<div class="container album-container album-slideshow-container">
<div class="row">
<div class="col-xs-12">
<div class="pull-right">
@include(\App\Facade\Theme::viewName('partials.album_view_selector'))
</div>
<h1 class="page-title">{{ $album->name }}</h1>
<p>{{ $album->description }}</p>
<hr/>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-center">
<div class="pull-right">
<div class="btn btn-group">
<button class="btn btn-default" data-bind="click: pauseSlideshow, disable: isPaused, visible: isRunning"><i class="fa fa-fw fa-pause"></i></button>
<button class="btn btn-default" data-bind="click: continueSlideshow, enable: isPaused, visible: isRunning"><i class="fa fa-fw fa-play"></i></button>
</div>
</div>
<div id="image-preview" data-bind="with: current()">
<a data-bind="attr: { href: info_url, title: description }">
<img class="img-rounded thumbnail img-responsive" data-bind="attr: { src: original_url, alt: name, title: description }"/>
</a>
</div>
</div>
</div>
<div class="row" style="margin-top: 15px;">
<div class="col-xs-12">
<div class="thumbnails">
@foreach ($photos as $photo)
<a href="{{ $photo->url() }}" data-bind="click: function() { viewModel.changeCurrentImage({{ $photo->id }}); }">
<img class="ss-thumbnail" src="{{ $photo->thumbnailUrl('slideshow-tiny') }}" alt="{{ $photo->name }}" title="{{ $photo->description }}" data-photo-id="{{ $photo->id }}" data-original-url="{{ $photo->thumbnailUrl('fullsize') }}" data-info-url="{{ $photo->url() }}" />
</a>
@endforeach
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
// TODO make this timeout configurable
var viewModel = new SlideShowViewModel(5000);
$(document).ready(function() {
$('img.ss-thumbnail').each(function(index, element)
{
viewModel.images.push({
'id': $(element).data('photo-id'),
'name': $(element).attr('alt'),
'description': $(element).attr('title'),
'original_url': $(element).data('original-url'),
'info_url': $(element).data('info-url')
});
});
ko.applyBindings(viewModel);
viewModel.startSlideshow();
});
</script>
@endpush

View File

@ -6,7 +6,7 @@
<div class="container"> <div class="container">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{{ route('home') }}">Gallery</a></li> <li><a href="{{ route('home') }}">Gallery</a></li>
<li><a href="{{ $photo->album->url() }}">{{ $photo->album->name }}</a></li> <li><a href="{{ $return_album_url }}">{{ $photo->album->name }}</a></li>
<li class="active">{{ $photo->name }}</li> <li class="active">{{ $photo->name }}</li>
</ol> </ol>
</div> </div>
@ -85,5 +85,11 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row">
<div class="col-xs-12">
<a href="{{ $return_album_url }}" class="btn btn-default">@lang('gallery.back_to_album', ['name' => $photo->album->name])</a>
</div>
</div>
</div> </div>
@endsection @endsection

View File

@ -0,0 +1,19 @@
<form method="get" action="{{ \Request::url() }}">
<div class="field-group">
<label class="control-label">@lang('forms.album_view_label')</label>
<select name="view" class="form-control" id="album-view-selector">
<option value="default"@if (isset($current_view) && $current_view == 'default') selected="selected"@endif>@lang('gallery.album_views.default')</option>
<option value="slideshow"@if (isset($current_view) && $current_view == 'slideshow') selected="selected"@endif>@lang('gallery.album_views.slideshow')</option>
</select>
</div>
</form>
@push('scripts')
<script type="text/javascript">
$(document).ready(function() {
$('#album-view-selector').change(function() {
$(this).parents('form').submit();
});
});
</script>
@endpush

View File

@ -19,6 +19,11 @@
"name": "admin-preview", "name": "admin-preview",
"height": 112, "height": 112,
"width": 150 "width": 150
},
{
"name": "slideshow-tiny",
"height": 75,
"width": 100
} }
] ]
} }