From f86a3ce358a60cada4ca1e1eb9dc0b610d6ceb1c Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sat, 3 Sep 2016 21:32:55 +0200 Subject: [PATCH] Add single photo pages, navigateable by arrow keys and touch gestures. --- TODO.md | 1 - controllers/ViewPhoto.php | 32 +++++++++ models/Asset.php | 39 ++++++++--- models/Dispatcher.php | 6 ++ models/Tag.php | 2 +- public/css/default.css | 97 +++++++++++++++++++++++++++ public/js/photonav.js | 72 ++++++++++++++++++++ templates/PhotoPage.php | 138 ++++++++++++++++++++++++++++++++++++++ templates/PhotosIndex.php | 2 +- 9 files changed, 378 insertions(+), 11 deletions(-) create mode 100644 controllers/ViewPhoto.php create mode 100644 public/js/photonav.js create mode 100644 templates/PhotoPage.php diff --git a/TODO.md b/TODO.md index 68957b22..f3691d60 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,5 @@ TODO: -* Pagina om één foto te bekijken * Taggen door gebruikers * Uploaden door gebruikers * Album management diff --git a/controllers/ViewPhoto.php b/controllers/ViewPhoto.php new file mode 100644 index 00000000..eafaf31a --- /dev/null +++ b/controllers/ViewPhoto.php @@ -0,0 +1,32 @@ +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'); + } +} diff --git a/models/Asset.php b/models/Asset.php index dfc12c32..f0050209 100644 --- a/models/Asset.php +++ b/models/Asset.php @@ -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, '/')); diff --git a/models/Dispatcher.php b/models/Dispatcher.php index 790e1fca..c47e9dff 100644 --- a/models/Dispatcher.php +++ b/models/Dispatcher.php @@ -53,6 +53,12 @@ class Dispatcher $_GET = array_merge($_GET, $path); return new ViewPhotoAlbum(); } + // A photo for sure, then, right? + elseif (preg_match('~^/(?.+?)/?$~', $_SERVER['PATH_INFO'], $path)) + { + $_GET = array_merge($_GET, $path); + return new ViewPhoto(); + } // No idea, then? else throw new NotFoundException(); diff --git a/models/Tag.php b/models/Tag.php index ea9bd7c3..5c1b2a69 100644 --- a/models/Tag.php +++ b/models/Tag.php @@ -246,7 +246,7 @@ class Tag public function getUrl() { - return BASEURL . '/tag/' . $this->slug . '/'; + return BASEURL . '/' . $this->slug . '/'; } public function save() diff --git a/public/css/default.css b/public/css/default.css index dda95c1a..769d424e 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -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) { diff --git a/public/js/photonav.js b/public/js/photonav.js new file mode 100644 index 00000000..44e4fd33 --- /dev/null +++ b/public/js/photonav.js @@ -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")); diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php new file mode 100644 index 00000000..9ee2a097 --- /dev/null +++ b/templates/PhotoPage.php @@ -0,0 +1,138 @@ +photo = $photo; + } + + protected function html_content() + { + $this->photoNav(); + $this->photo(); + + echo ' +
+

', $this->photo->getTitle(), '

'; + + $this->taggedPeople(); + + echo ' +
'; + + $this->photoMeta(); + + echo ' + '; + } + + private function photo() + { + echo ' + '; + } + + private function photoNav() + { + if (false) // $previous_post = $this->post->getPreviousPostUrl()) + echo ' + Previous photo'; + else + echo ' + Previous photo'; + + if (false) //$this->post->getNextPostUrl()) + echo ' + Next photo'; + else + echo ' + Next photo'; + } + + private function photoMeta() + { + echo ' +
+

EXIF

+
'; + + if (!empty($this->exif->created_timestamp)) + echo ' +
Date Taken
+
', date("j M Y, H:i:s", $this->exif->created_timestamp), '
'; + + if (!empty($this->exif->camera)) + echo ' +
Model
+
', $this->exif->camera, '
'; + + if (!empty($this->exif->shutter_speed)) + echo ' +
Shutter Speed
+
', $this->exif->shutterSpeedFraction(), '
'; + + if (!empty($this->exif->aperture)) + echo ' +
Aperture
+
f/', number_format($this->exif->aperture, 1), '
'; + + if (!empty($this->exif->focal_length)) + echo ' +
Focal Length
+
', $this->exif->focal_length, ' mm
'; + + if (!empty($this->exif->iso)) + echo ' +
ISO Speed
+
', $this->exif->iso, '
'; + + echo ' +
+
'; + } + + private function taggedPeople() + { + echo ' +

Tags

+ '; + } + + public function setExif(EXIF $exif) + { + $this->exif = $exif; + } +} diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index 47daf87e..5f353b15 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -94,7 +94,7 @@ class PhotosIndex extends SubTemplate Edit'; echo ' - + '; if ($this->show_labels)