Add single photo pages, navigateable by arrow keys and touch gestures.

This commit is contained in:
Aaron van Geffen 2016-09-03 21:32:55 +02:00
parent e6b0c13354
commit f86a3ce358
9 changed files with 378 additions and 11 deletions

View File

@ -1,6 +1,5 @@
TODO:
* Pagina om één foto te bekijken
* Taggen door gebruikers
* Uploaden door gebruikers
* Album management

32
controllers/ViewPhoto.php Normal file
View File

@ -0,0 +1,32 @@
<?php
/*****************************************************************************
* ViewPhoto.php
* Contains the view photo controller
*
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
*****************************************************************************/
class ViewPhoto extends HTMLController
{
public function __construct()
{
$photo = Asset::fromSlug($_GET['slug']);
if (empty($photo))
throw new NotFoundException();
parent::__construct($photo->getTitle() . ' - ' . SITE_TITLE);
$page = new PhotoPage($photo->getImage());
// Exif data?
$exif = EXIF::fromFile($photo->getFullPath());
if ($exif)
$page->setExif($exif);
$this->page->adopt($page);
$this->page->setCanonicalUrl($photo->getPageUrl());
// Add an edit button to the admin bar.
if (Registry::get('user')->isAdmin())
$this->admin_bar->appendItem(BASEURL . '/editasset/?id=' . $photo->getId(), 'Edit this photo');
}
}

View File

@ -32,9 +32,7 @@ class Asset
public static function fromId($id_asset, $return_format = 'object')
{
$db = Registry::get('db');
$row = $db->queryAssoc('
$row = Registry::get('db')->queryAssoc('
SELECT *
FROM assets
WHERE id_asset = {int:id_asset}',
@ -42,16 +40,31 @@ class Asset
'id_asset' => $id_asset,
]);
// Asset not found?
if (empty($row))
return false;
return empty($row) ? false : self::byRow($row, $return_format);
}
$row['meta'] = $db->queryPair('
public static function fromSlug($slug, $return_format = 'object')
{
$row = Registry::get('db')->queryAssoc('
SELECT *
FROM assets
WHERE slug = {string:slug}',
[
'slug' => $slug,
]);
return empty($row) ? false : self::byRow($row, $return_format);
}
public static function byRow(array $row, $return_format = 'object')
{
// Supplement with metadata.
$row['meta'] = Registry::get('db')->queryPair('
SELECT variable, value
FROM assets_meta
WHERE id_asset = {int:id_asset}',
[
'id_asset' => $id_asset,
'id_asset' => $row['id_asset'],
]);
return $return_format == 'object' ? new Asset($row) : $row;
@ -254,6 +267,11 @@ class Asset
return $this->meta;
}
public function getFullPath()
{
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
}
public function getPath()
{
return $this->subdir;
@ -282,6 +300,11 @@ class Asset
return BASEURL . '/assets/' . $this->subdir . '/' . $this->filename;
}
public function getPageUrl()
{
return BASEURL . '/' . $this->slug . '/';
}
public function getType()
{
return substr($this->mimetype, 0, strpos($this->mimetype, '/'));

View File

@ -53,6 +53,12 @@ class Dispatcher
$_GET = array_merge($_GET, $path);
return new ViewPhotoAlbum();
}
// A photo for sure, then, right?
elseif (preg_match('~^/(?<slug>.+?)/?$~', $_SERVER['PATH_INFO'], $path))
{
$_GET = array_merge($_GET, $path);
return new ViewPhoto();
}
// No idea, then?
else
throw new NotFoundException();

View File

@ -246,7 +246,7 @@ class Tag
public function getUrl()
{
return BASEURL . '/tag/' . $this->slug . '/';
return BASEURL . '/' . $this->slug . '/';
}
public function save()

View File

@ -474,6 +474,103 @@ textarea {
}
/* Styling for the photo pages
--------------------------------*/
#photo_frame {
overflow: hidden;
text-align: center;
}
#photo_frame a {
background: #fff;
border: 0.9em solid #fff;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
cursor: -moz-zoom-in;
display: inline-block;
}
#photo_frame a img {
border: none;
display: block;
height: auto;
width: 100%;
}
#previous_photo, #next_photo {
background: #fff;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
color: #678FA4;
font-size: 3em;
line-height: 0.5;
padding: 32px 8px;
position: fixed;
text-decoration: none;
top: 45%;
}
#previous_photo em, #next_photo em {
position: absolute;
top: -1000em;
left: -1000em;
}
span#previous_photo, span#next_photo {
opacity: 0.25;
}
a#previous_photo:hover, a#next_photo:hover {
background: #eee;
color: #000;
}
#previous_photo {
left: 0;
}
#previous_photo:before {
content: '←';
}
#next_photo {
right: 0;
}
#next_photo:before {
content: '→';
}
#sub_photo h2, #sub_photo h3, #photo_exif_box h3 {
font: 600 20px/30px "Open Sans", sans-serif;
margin: 0 0 10px;
}
#sub_photo h3 {
font-size: 16px;
}
#sub_photo {
background: #fff;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
float: left;
padding: 2%;
margin: 25px 3.5% 25px 0;
width: 68.5%;
}
#photo_exif_box {
background: #fff;
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
margin: 25px 0 25px 0;
overflow: auto;
padding: 2%;
float: right;
width: 20%;
}
#photo_exif_box dt {
font-weight: bold;
float: left;
clear: left;
width: 120px;
}
#photo_exif_box dt:after {
content: ':';
}
#photo_exif_box dd {
float: left;
margin: 0;
}
/* Responsive: smartphone in portrait
---------------------------------------*/
@media only screen and (max-width: 895px) {

72
public/js/photonav.js Normal file
View File

@ -0,0 +1,72 @@
function enableKeyDownNavigation() {
document.addEventListener("keydown", function (event) {
if (event.keyCode == 37) {
var target = document.getElementById("previous_photo").href;
if (target) {
event.preventDefault();
document.location.href = target;
}
}
else if (event.keyCode == 39) {
var target = document.getElementById("next_photo").href;
if (target) {
event.preventDefault();
document.location.href = target;
}
}
}, false);
}
function disableKeyDownPropagation(obj) {
for (var x = 0; x < obj.length; x++) {
obj[x].addEventListener("keydown", function (event) {
if (event.keyCode == 37 || event.keyCode == 39) {
event.stopPropagation();
}
});
}
}
function enableTouchNavigation() {
var x_down = null;
var y_down = null;
document.addEventListener('touchstart', function(event) {
x_down = event.touches[0].clientX;
y_down = event.touches[0].clientY;
}, false);
document.addEventListener('touchmove', function(event) {
if (!x_down || !y_down) {
return;
}
var x_diff = x_down - event.touches[0].clientX;
var y_diff = y_down - event.touches[0].clientY;
if (Math.abs(y_diff) > 50) {
return;
}
if (Math.abs(x_diff) > Math.abs(y_diff)) {
if (x_diff > 0) {
var target = document.getElementById("previous_photo").href;
if (target) {
event.preventDefault();
document.location.href = target;
}
} else {
var target = document.getElementById("next_photo").href;
if (target) {
event.preventDefault();
document.location.href = target;
}
}
}
}, false);
}
enableKeyDownNavigation();
enableTouchNavigation();
disableKeyDownPropagation(document.getElementsByTagName("textarea"));
disableKeyDownPropagation(document.getElementsByTagName("input"));

138
templates/PhotoPage.php Normal file
View File

@ -0,0 +1,138 @@
<?php
/*****************************************************************************
* PhotoPage.php
* Contains the photo page template.
*
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
*****************************************************************************/
class PhotoPage extends SubTemplate
{
private $photo;
private $exif;
public function __construct(Image $photo)
{
$this->photo = $photo;
}
protected function html_content()
{
$this->photoNav();
$this->photo();
echo '
<div id="sub_photo">
<h2 class="entry-title">', $this->photo->getTitle(), '</h2>';
$this->taggedPeople();
echo '
</div>';
$this->photoMeta();
echo '
<script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>';
}
private function photo()
{
echo '
<div id="photo_frame">
<a href="', $this->photo->getUrl(), '">';
if ($this->photo->isPortrait())
echo '
<img src="', $this->photo->getThumbnailUrl(null, 960), '" alt="">';
else
echo '
<img src="', $this->photo->getThumbnailUrl(1280, null), '" alt="">';
echo '
</a>
</div>';
}
private function photoNav()
{
if (false) // $previous_post = $this->post->getPreviousPostUrl())
echo '
<a href="', $previous_post, '" id="previous_photo"><em>Previous photo</em></a>';
else
echo '
<span id="previous_photo"><em>Previous photo</em></span>';
if (false) //$this->post->getNextPostUrl())
echo '
<a href="', $next_post, '" id="next_photo"><em>Next photo</em></a>';
else
echo '
<span id="next_photo"><em>Next photo</em></span>';
}
private function photoMeta()
{
echo '
<div id="photo_exif_box">
<h3>EXIF</h3>
<dl class="photo_meta">';
if (!empty($this->exif->created_timestamp))
echo '
<dt>Date Taken</dt>
<dd>', date("j M Y, H:i:s", $this->exif->created_timestamp), '</dd>';
if (!empty($this->exif->camera))
echo '
<dt>Model</dt>
<dd>', $this->exif->camera, '</dd>';
if (!empty($this->exif->shutter_speed))
echo '
<dt>Shutter Speed</dt>
<dd>', $this->exif->shutterSpeedFraction(), '</dd>';
if (!empty($this->exif->aperture))
echo '
<dt>Aperture</dt>
<dd>f/', number_format($this->exif->aperture, 1), '</dd>';
if (!empty($this->exif->focal_length))
echo '
<dt>Focal Length</dt>
<dd>', $this->exif->focal_length, ' mm</dd>';
if (!empty($this->exif->iso))
echo '
<dt>ISO Speed</dt>
<dd>', $this->exif->iso, '</dd>';
echo '
</dl>
</div>';
}
private function taggedPeople()
{
echo '
<h3>Tags</h3>
<ul>';
foreach ($this->photo->getTags() as $tag)
{
echo '
<li>
<a rel="tag" title="View all posts tagged ', $tag->tag, '" href="', $tag->getUrl(), '" class="entry-tag">', $tag->tag, '</a>
</li>';
}
echo '
</ul>';
}
public function setExif(EXIF $exif)
{
$this->exif = $exif;
}
}

View File

@ -94,7 +94,7 @@ class PhotosIndex extends SubTemplate
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
echo '
<a href="', $image->getUrl(), '">
<a href="', $image->getPageUrl(), '">
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
if ($this->show_labels)