Add single photo pages, navigateable by arrow keys and touch gestures.
This commit is contained in:
parent
e6b0c13354
commit
f86a3ce358
1
TODO.md
1
TODO.md
@ -1,6 +1,5 @@
|
|||||||
TODO:
|
TODO:
|
||||||
|
|
||||||
* Pagina om één foto te bekijken
|
|
||||||
* Taggen door gebruikers
|
* Taggen door gebruikers
|
||||||
* Uploaden door gebruikers
|
* Uploaden door gebruikers
|
||||||
* Album management
|
* Album management
|
||||||
|
32
controllers/ViewPhoto.php
Normal file
32
controllers/ViewPhoto.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
@ -32,9 +32,7 @@ class Asset
|
|||||||
|
|
||||||
public static function fromId($id_asset, $return_format = 'object')
|
public static function fromId($id_asset, $return_format = 'object')
|
||||||
{
|
{
|
||||||
$db = Registry::get('db');
|
$row = Registry::get('db')->queryAssoc('
|
||||||
|
|
||||||
$row = $db->queryAssoc('
|
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM assets
|
FROM assets
|
||||||
WHERE id_asset = {int:id_asset}',
|
WHERE id_asset = {int:id_asset}',
|
||||||
@ -42,16 +40,31 @@ class Asset
|
|||||||
'id_asset' => $id_asset,
|
'id_asset' => $id_asset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Asset not found?
|
return empty($row) ? false : self::byRow($row, $return_format);
|
||||||
if (empty($row))
|
}
|
||||||
return false;
|
|
||||||
|
|
||||||
$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
|
SELECT variable, value
|
||||||
FROM assets_meta
|
FROM assets_meta
|
||||||
WHERE id_asset = {int:id_asset}',
|
WHERE id_asset = {int:id_asset}',
|
||||||
[
|
[
|
||||||
'id_asset' => $id_asset,
|
'id_asset' => $row['id_asset'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $return_format == 'object' ? new Asset($row) : $row;
|
return $return_format == 'object' ? new Asset($row) : $row;
|
||||||
@ -254,6 +267,11 @@ class Asset
|
|||||||
return $this->meta;
|
return $this->meta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFullPath()
|
||||||
|
{
|
||||||
|
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||||
|
}
|
||||||
|
|
||||||
public function getPath()
|
public function getPath()
|
||||||
{
|
{
|
||||||
return $this->subdir;
|
return $this->subdir;
|
||||||
@ -282,6 +300,11 @@ class Asset
|
|||||||
return BASEURL . '/assets/' . $this->subdir . '/' . $this->filename;
|
return BASEURL . '/assets/' . $this->subdir . '/' . $this->filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getPageUrl()
|
||||||
|
{
|
||||||
|
return BASEURL . '/' . $this->slug . '/';
|
||||||
|
}
|
||||||
|
|
||||||
public function getType()
|
public function getType()
|
||||||
{
|
{
|
||||||
return substr($this->mimetype, 0, strpos($this->mimetype, '/'));
|
return substr($this->mimetype, 0, strpos($this->mimetype, '/'));
|
||||||
|
@ -53,6 +53,12 @@ class Dispatcher
|
|||||||
$_GET = array_merge($_GET, $path);
|
$_GET = array_merge($_GET, $path);
|
||||||
return new ViewPhotoAlbum();
|
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?
|
// No idea, then?
|
||||||
else
|
else
|
||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
|
@ -246,7 +246,7 @@ class Tag
|
|||||||
|
|
||||||
public function getUrl()
|
public function getUrl()
|
||||||
{
|
{
|
||||||
return BASEURL . '/tag/' . $this->slug . '/';
|
return BASEURL . '/' . $this->slug . '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save()
|
public function save()
|
||||||
|
@ -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
|
/* Responsive: smartphone in portrait
|
||||||
---------------------------------------*/
|
---------------------------------------*/
|
||||||
@media only screen and (max-width: 895px) {
|
@media only screen and (max-width: 895px) {
|
||||||
|
72
public/js/photonav.js
Normal file
72
public/js/photonav.js
Normal 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
138
templates/PhotoPage.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -94,7 +94,7 @@ class PhotosIndex extends SubTemplate
|
|||||||
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
|
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
<a href="', $image->getUrl(), '">
|
<a href="', $image->getPageUrl(), '">
|
||||||
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
|
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
|
||||||
|
|
||||||
if ($this->show_labels)
|
if ($this->show_labels)
|
||||||
|
Loading…
Reference in New Issue
Block a user