3 Commits

Author SHA1 Message Date
Joost Rijneveld
a211e3ae4a Elaborate on how to set up dev env 2022-11-27 14:03:14 +01:00
7d82a4a924 Merge pull request 'Complete date-ordered orderings' (#29) from electricdusk/pics:assets-complete-ordering into master
Reviewed-on: Public/pics#29
2022-11-22 21:09:10 +01:00
b7a37c85f6 Complete date-ordered orderings
Bug as reported by Yorick: When two Assets have the same capture
date, a bug occurs in the interface where the user gets stuck in
a loop when moving to the next image.

This patch uses the primary key as a fallback when ordering the
images by capture date.  This way, the asset ordering is complete
and it should resolve the bug.
2022-11-22 12:00:53 +01:00
13 changed files with 115 additions and 95 deletions

7
.gitignore vendored
View File

@@ -1,5 +1,2 @@
.DS_Store composer-setup.php
composer.lock composer.phar
config.php
hashru.sublime-project
hashru.sublime-workspace

View File

@@ -13,12 +13,22 @@ The Kabuki codebase requires the following PHP extensions to be enabled for full
## Setup ## Setup
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file. Copy `config.php.dist` to `config.php` and set-up the constants contained in the file. For development, consider starting from `config-dev.php.dist`.
Ensure you have a MySQL database running with credentials matching your `config.php`. For development, consider the /dev/docker-compose.yml file.
Run `composer install`. If you do not have composer installed globally, run it from the project directory as follows:
```
wget -O composer-setup.php https://getcomposer.org/installer
php composer-setup.php --install-dir=.
php ./composer.phar install
```
## Running ## Running
For development purposes, simply run the `server` script provided in the root of this repository. For development purposes, simply run the `server` script provided in the root of this repository.
This will start a PHP development server on `hashru.local:8080`. This will start a PHP development server on `127.0.0.1:8080`.
For a production environment, please set up a proper PHP-FPM environment instead. For a production environment, please set up a proper PHP-FPM environment instead.

36
config-dev.php.dist Normal file
View File

@@ -0,0 +1,36 @@
<?php
/*****************************************************************************
* config.php
* Contains general settings for the project.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
const DEBUG = true;
const CACHE_ENABLED = true;
const CACHE_KEY_PREFIX = 'hashru_';
// Basedir and base URL of the project.
const BASEDIR = __DIR__;
const BASEURL = 'http://127.0.0.1:8080'; // no trailing /
// Reply-To e-mail header address
const REPLY_TO_ADDRESS = 'no-reply@my.domain.tld';
// Assets dir and url, where assets are plentiful. (In wwwroot!)
const ASSETSDIR = BASEDIR . '/public/assets';
const ASSETSURL = BASEURL . '/assets';
// Thumbs dir and url, where thumbnails for assets reside.
const THUMBSDIR = BASEDIR . '/public/thumbs';
const THUMBSURL = BASEURL . '/thumbs';
// Database server, username, password, name
const DB_SERVER = '127.0.0.1';
const DB_USER = 'hashru';
const DB_PASS = 'hashru';
const DB_NAME = 'hashru_pics';
const DB_LOG_QUERIES = false;
const SITE_TITLE = 'HashRU Pics';
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';

11
dev/docker-compose.yml Normal file
View File

@@ -0,0 +1,11 @@
version: '3'
services:
mysql:
image: mysql:latest
ports:
- 3306:3306
environment:
MYSQL_USER: 'hashru'
MYSQL_PASSWORD: 'hashru'
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
MYSQL_DATABASE: 'hashru_pics'

View File

@@ -606,14 +606,12 @@ class Asset
FROM assets_tags AS t FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND WHERE t.id_tag = {int:id_tag} AND
a.date_captured <= {datetime:date_captured} AND (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY a.date_captured DESC, a.id_asset DESC'
ORDER BY a.date_captured DESC'
: ' : '
FROM assets AS a FROM assets AS a
WHERE date_captured >= {datetime:date_captured} AND WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY date_captured ASC, a.id_asset ASC')
ORDER BY date_captured ASC')
. ' . '
LIMIT 1', LIMIT 1',
[ [
@@ -639,14 +637,12 @@ class Asset
FROM assets_tags AS t FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND WHERE t.id_tag = {int:id_tag} AND
a.date_captured >= {datetime:date_captured} AND (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY a.date_captured ASC, a.id_asset ASC'
ORDER BY a.date_captured ASC'
: ' : '
FROM assets AS a FROM assets AS a
WHERE date_captured <= {datetime:date_captured} AND WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
a.id_asset != {int:id_asset} ORDER BY date_captured DESC, a.id_asset DESC')
ORDER BY date_captured DESC')
. ' . '
LIMIT 1', LIMIT 1',
[ [

View File

@@ -488,7 +488,7 @@ class Database
/** /**
* This function can be used to insert data into the database in a secure way. * This function can be used to insert data into the database in a secure way.
*/ */
public function insert($method, $table, $columns, $data) public function insert($method = 'replace', $table, $columns, $data)
{ {
// With nothing to insert, simply return. // With nothing to insert, simply return.
if (empty($data)) if (empty($data))

View File

@@ -43,7 +43,7 @@ class GenericTable
$this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start']; $this->start = empty($options['start']) || !is_numeric($options['start']) || $options['start'] < 0 || $options['start'] > $this->recordCount ? 0 : $options['start'];
// Figure out where we are on the whole, too. // Figure out where we are on the whole, too.
$numPages = max(1, ceil($this->recordCount / $this->items_per_page)); $numPages = ceil($this->recordCount / $this->items_per_page);
$this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages); $this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages);
// Let's bear a few things in mind... // Let's bear a few things in mind...
@@ -234,9 +234,7 @@ class GenericTable
else else
$pattern = $options['data']['pattern']; $pattern = $options['data']['pattern'];
if (!isset($rowData[$options['data']['timestamp']])) if (!is_numeric($rowData[$options['data']['timestamp']]))
$timestamp = 0;
elseif (!is_numeric($rowData[$options['data']['timestamp']]))
$timestamp = strtotime($rowData[$options['data']['timestamp']]); $timestamp = strtotime($rowData[$options['data']['timestamp']]);
else else
$timestamp = (int) $rowData[$options['data']['timestamp']]; $timestamp = (int) $rowData[$options['data']['timestamp']];

View File

@@ -67,33 +67,14 @@ class Image extends Asset
return EXIF::fromFile($this->getPath()); return EXIF::fromFile($this->getPath());
} }
public function getImageUrls($width = null, $height = null) public function getPath()
{ {
$image_urls = []; return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
if (isset($width) || isset($height))
{
$thumbnail = new Thumbnail($this);
$image_urls[1] = $this->getThumbnailUrl($width, $height, false);
// Can we afford to generate double-density thumbnails as well?
if ((!isset($width) || $this->image_width >= $width * 2) &&
(!isset($height) || $this->image_height >= $height * 2))
$image_urls[2] = $this->getThumbnailUrl($width * 2, $height * 2, false);
else
$image_urls[2] = $this->getThumbnailUrl($this->image_width, $this->image_height, true);
}
else
$image_urls[1] = $this->getUrl();
return $image_urls;
} }
public function getInlineImage($width = null, $height = null, $className = 'inline-image') public function getUrl()
{ {
$image_urls = $this->getImageUrls($width, $height); return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
return '<img class="' . $className . '" src="' . $image_urls[1] . '" alt=""' .
(isset($image_urls[2]) ? ' srcset="' . $image_urls[2] . ' 2x"' : '') . '>';
} }
/** /**
@@ -160,8 +141,7 @@ class Image extends Asset
} }
return Registry::get('db')->query(' return Registry::get('db')->query('
UPDATE assets_thumbs DELETE FROM assets_thumbs
SET filename = NULL
WHERE id_asset = {int:id_asset}', WHERE id_asset = {int:id_asset}',
['id_asset' => $this->id_asset]); ['id_asset' => $this->id_asset]);
} }

View File

@@ -63,9 +63,9 @@ class PageIndex
lower current/cont. pgs. center upper lower current/cont. pgs. center upper
*/ */
$this->num_pages = max(1, ceil($this->recordCount / $this->items_per_page)); $this->num_pages = ceil($this->recordCount / $this->items_per_page);
$this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages); $this->current_page = min(ceil($this->start / $this->items_per_page) + 1, $this->num_pages);
if ($this->num_pages <= 1) if ($this->num_pages == 0)
{ {
$this->needsPageIndex = false; $this->needsPageIndex = false;
return; return;

View File

@@ -9,7 +9,6 @@
class Thumbnail class Thumbnail
{ {
private $image; private $image;
private $image_meta;
private $thumbnails; private $thumbnails;
private $properly_initialised; private $properly_initialised;
@@ -24,7 +23,7 @@ class Thumbnail
const CROP_MODE_SLICE_CENTRE = 4; const CROP_MODE_SLICE_CENTRE = 4;
const CROP_MODE_SLICE_BOTTOM = 5; const CROP_MODE_SLICE_BOTTOM = 5;
public function __construct(Image $image) public function __construct($image)
{ {
$this->image = $image; $this->image = $image;
$this->image_meta = $image->getMeta(); $this->image_meta = $image->getMeta();
@@ -46,45 +45,51 @@ class Thumbnail
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix; $thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
if (!empty($this->thumbnails[$thumb_selector])) if (!empty($this->thumbnails[$thumb_selector]))
{ {
$thumb_filename = $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector]; $thumb_path = '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
if (file_exists(THUMBSDIR . '/' . $thumb_filename)) if (file_exists(THUMBSDIR . $thumb_path))
return THUMBSURL . '/' . $thumb_filename; return THUMBSURL . $thumb_path;
} }
// Do we have a custom thumbnail on file? // Do we have a custom thumbnail on file?
$custom_selector = 'custom_' . $this->width . 'x' . $this->height; $custom_selector = 'custom_' . $this->width . 'x' . $this->height;
if (isset($this->image_meta[$custom_selector])) if (isset($this->image_meta[$custom_selector]))
{ {
$custom_filename = $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector]; $custom_thumb_path = '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
if (file_exists(ASSETSDIR . '/' . $custom_filename)) if (file_exists(ASSETSDIR . $custom_thumb_path))
{ {
// Ensure destination thumbnail directory exists.
if (!file_exists($this->image->getSubdir()))
@mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
// Copy the custom thumbail to the general thumbnail directory. // Copy the custom thumbail to the general thumbnail directory.
copy(ASSETSDIR . '/' . $custom_filename, THUMBSDIR . '/' . $custom_filename); copy(ASSETSDIR . $custom_thumb_path, THUMBSDIR . $custom_thumb_path);
// Let's remember this for future reference. // Let's remember this for future reference.
$this->markAsGenerated($this->image_meta[$custom_selector]); $this->markAsGenerated($this->image_meta[$custom_selector]);
return THUMBSURL . '/' . $custom_filename; return THUMBSURL . $custom_thumb_path;
} }
else else
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!'); throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
} }
// Is this the right moment to generate a thumbnail, then? // Is this the right moment to generate a thumbnail, then?
if ($generate) if ($generate && array_key_exists($thumb_selector, $this->thumbnails))
{ {
if (array_key_exists($thumb_selector, $this->thumbnails)) return $this->generate();
return $this->generate();
else
throw new Exception("Trying to generate a thumbnail not previously queued by the system\n" .
print_r(func_get_args(), true));
} }
// If not, queue it for generation at another time, and return a URL to generate it with. // If not, queue it for generation at another time, and return a URL to generate it with.
else elseif (!$generate)
{ {
$this->markAsQueued(); $this->markAsQueued();
return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $thumb_selector . '/'; return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $this->width . 'x' . $this->height . $this->filename_suffix . '/';
}
// Still here..? What are you up to? ..Sneaking?
else
{
throw new Exception("Trying to generate a thumbnail for selector " . $thumb_selector . ", which does not appear to have been requested by the system.\n" . print_r(func_get_args(), true));
} }
} }
@@ -255,18 +260,14 @@ class Thumbnail
'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext; '_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
// Ensure the thumbnail subdirectory exists. // Ensure the thumbnail subdirectory exists.
$target_dir = THUMBSDIR . '/' . $this->image->getSubdir(); if (!is_dir(THUMBSDIR . '/' . $this->image->getSubdir()))
if (!is_dir($target_dir)) mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
mkdir($target_dir, 0755, true);
if (!is_writable($target_dir))
throw new Exception('Thumbnail directory is not writable!');
// No need to preserve every detail. // No need to preserve every detail.
$thumb->setImageCompressionQuality(80); $thumb->setImageCompressionQuality(80);
// Save it in a public spot. // Save it in a public spot.
$thumb->writeImage($target_dir . '/' . $thumb_filename); $thumb->writeImage(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $thumb_filename);
// Let's remember this for future reference... // Let's remember this for future reference...
$this->markAsGenerated($thumb_filename); $this->markAsGenerated($thumb_filename);
@@ -277,6 +278,7 @@ class Thumbnail
// Finally, return the URL for the generated thumbnail image. // Finally, return the URL for the generated thumbnail image.
return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename; return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
} }
// Blast! Curse your sudden but inevitable betrayal!
catch (ImagickException $e) catch (ImagickException $e)
{ {
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage()); throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
@@ -334,16 +336,10 @@ class Thumbnail
if ($success) if ($success)
{ {
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix; $thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : null; $this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : '';
// For consistency, write new thumbnail filename to parent Image object.
// TODO: there could still be an inconsistency if multiple objects exists for the same image asset.
$this->image->getThumbnails()[$thumb_selector] = $this->thumbnails[$thumb_selector];
return $success;
} }
else
throw new UnexpectedValueException('Thumbnail queuing query failed'); return $success;
} }
private function markAsQueued() private function markAsQueued()

2
server
View File

@@ -1,2 +1,2 @@
#!/bin/bash #!/bin/bash
php -S hashru.local:8080 -t public php -S 127.0.0.1:8080 -t public

View File

@@ -65,9 +65,11 @@ class PhotoPage extends SubTemplate
<a href="', $this->photo->getUrl(), '">'; <a href="', $this->photo->getUrl(), '">';
if ($this->photo->isPortrait()) if ($this->photo->isPortrait())
echo $this->photo->getInlineImage(null, 960); echo '
<img src="', $this->photo->getThumbnailUrl(null, 960), '" alt="">';
else else
echo $this->photo->getInlineImage(1280, null); echo '
<img src="', $this->photo->getThumbnailUrl(1280, null), '" alt="">';
echo ' echo '
</a> </a>

View File

@@ -90,14 +90,8 @@ 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->getPageUrl(), $this->url_suffix, '#photo_frame"> <a href="', $image->getPageUrl(), $this->url_suffix, '">
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"'; <img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">';
// Can we offer double-density thumbs?
if ($image->width() >= $width * 2 && $image->height() >= $height * 2)
echo ' srcset="', $image->getThumbnailUrl($width * 2, $height * 2, $crop, $fit), ' 2x"';
echo ' alt="" title="', $image->getTitle(), '">';
if ($this->show_labels) if ($this->show_labels)
echo ' echo '