22 Commits

Author SHA1 Message Date
187a7cd02f GenericTable: prevent passing NULL to strtotime 2022-07-14 16:45:32 +02:00
8414843bbf Prevent current page from being 0 if no items are present 2022-07-14 16:45:17 +02:00
474c387786 Add double-density thumbnails to albums and photo pages 2022-07-08 23:53:28 +02:00
12407d797d Address deprecation notices for certain function signatures 2022-07-08 23:52:03 +02:00
64d7433a56 Thumbnails: crop from original size if 2x is unavailable 2022-07-08 23:49:29 +02:00
58b7204fbf Do not delete thumbnail queue when replacing an asset
Thumbnails are normally created on demand, e.g. when processing the format codes in a post's body text.
Normally, the temporary URL is only used once to generate thumbnails ad-hoc. However, when cache is
enabled, a reference to the asset may be used in a cached version of a formatted body text, skipping
the normal thumbnail generation routine.

When an asset is replaced, currently, all thumbnails are removed and references to them are removed
from the database. In case the asset is still referenced in a cached formatted body text, this could lead
to an error when requesting the thumbnail, as the thumbnail request is no longer present in the system.

As we do not know what posts use particular assets at this point in the code, it is best to work around this
issue by unsetting the thumbnail filenames rather than deleting the entries outright. This effectively
generates them again on the next request.

In the future, we should aim to keep track of what posts make use of assets, so cache may be invalidated
in a more targeted way.
2022-07-08 23:49:20 +02:00
36a2779381 Don't try to generate double-density thumbs for small images 2022-07-08 23:49:13 +02:00
44bb501d13 Write new thumbnail filenames to parent Image object as well 2022-07-08 23:48:53 +02:00
9010123d18 Thumbnail class: minor refactor of generate method 2022-07-08 23:48:45 +02:00
e3b67c4022 Thumbnail class: refactor getUrl method 2022-07-08 23:48:38 +02:00
2bcdc5fe6e Split Image::getImageUrls from Image::getInlineImage 2022-07-08 23:48:30 +02:00
edfad992cc Rewrite Image::getInlineImage to support double density displays 2022-07-08 23:48:19 +02:00
357d95f6ff Add Image::getInlineImage method 2022-07-08 23:47:55 +02:00
0ec0de4414 Replace deprecated strftime calls 2022-05-07 13:25:19 +02:00
69417c36ed Merge pull request 'EXIF: prefer DateTimeOriginal over DateTimeDigitized' (#27) from exif-date-time-original into master
Reviewed-on: Public/pics#27
2022-02-16 21:56:04 +01:00
f2d8a32e67 EXIF: prefer DateTimeOriginal over DateTimeDigitized 2022-02-16 21:43:55 +01:00
4863561129 Merge pull request 'Refactor generic tables and page index classes' (#26) from refactor-tables into master
Reviewed-on: Public/pics#26
2021-05-17 20:19:18 +02:00
8474d3b2b2 Merge pull request 'Modernise autosuggest code' (#25) from autosuggest into master
Reviewed-on: Public/pics#25
2021-05-17 20:19:05 +02:00
3bf69fd21f Prevent XSS in error log viewer. 2021-03-10 17:40:06 +01:00
70e6001c85 Replace event.keyCode with event.key equivalents. 2021-02-16 15:26:57 +01:00
4402521051 Highlight matching string in autosuggest entries. 2021-02-15 12:14:24 +01:00
889302cd36 Modernise AutoSuggest and TagAutoSuggest classes. 2021-02-15 12:14:23 +01:00
10 changed files with 237 additions and 200 deletions

View File

@@ -47,9 +47,13 @@ class ManageErrors extends HTMLController
'parse' => [ 'parse' => [
'type' => 'function', 'type' => 'function',
'data' => function($row) { 'data' => function($row) {
return $row['message'] . '<br><div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' . return $row['message'] . '<br>' .
'<pre style="display: none">' . $row['debug_info'] . '</pre></div>' . '<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
'<small><a href="' . BASEURL . $row['request_uri'] . '">' . $row['request_uri'] . '</a></small>'; '<pre style="display: none">' . htmlspecialchars($row['debug_info']) .
'</pre></div>' .
'<small><a href="' . BASEURL .
htmlspecialchars($row['request_uri']) . '">' .
htmlspecialchars($row['request_uri']) . '</a></small>';
} }
], ],
'header' => 'Message / URL', 'header' => 'Message / URL',
@@ -85,7 +89,7 @@ class ManageErrors extends HTMLController
'header' => 'UID', 'header' => 'UID',
'is_sortable' => true, 'is_sortable' => true,
'parse' => [ 'parse' => [
'link' => BASEURL . '/member/?id={ID_USER}', 'link' => BASEURL . '/edituser/?id={ID_USER}',
'data' => 'id_user', 'data' => 'id_user',
], ],
], ],

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 = 'replace', $table, $columns, $data) public function insert($method, $table, $columns, $data)
{ {
// With nothing to insert, simply return. // With nothing to insert, simply return.
if (empty($data)) if (empty($data))

View File

@@ -96,7 +96,9 @@ class EXIF
elseif (!empty($exif['Make'])) elseif (!empty($exif['Make']))
$meta['camera'] = trim($exif['Make']); $meta['camera'] = trim($exif['Make']);
if (!empty($exif['DateTimeDigitized'])) if (!empty($exif['DateTimeOriginal']))
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeOriginal']);
elseif (!empty($exif['DateTimeDigitized']))
$meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']); $meta['created_timestamp'] = self::toUnixTime($exif['DateTimeDigitized']);
return new self($meta); return new self($meta);

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 = ceil($this->recordCount / $this->items_per_page); $numPages = max(1, 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...
@@ -228,13 +228,15 @@ class GenericTable
// Timestamps get custom treatment. // Timestamps get custom treatment.
case 'timestamp': case 'timestamp':
if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long') if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long')
$pattern = '%F %H:%M'; $pattern = 'Y-m-d H:i';
elseif ($options['data']['pattern'] === 'short') elseif ($options['data']['pattern'] === 'short')
$pattern = '%F'; $pattern = 'Y-m-d';
else else
$pattern = $options['data']['pattern']; $pattern = $options['data']['pattern'];
if (!is_numeric($rowData[$options['data']['timestamp']])) if (!isset($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']];
@@ -242,7 +244,7 @@ class GenericTable
if (isset($options['data']['if_null']) && $timestamp == 0) if (isset($options['data']['if_null']) && $timestamp == 0)
$value = $options['data']['if_null']; $value = $options['data']['if_null'];
else else
$value = strftime($pattern, $timestamp); $value = date($pattern, $timestamp);
break; break;
} }

View File

@@ -67,14 +67,33 @@ class Image extends Asset
return EXIF::fromFile($this->getPath()); return EXIF::fromFile($this->getPath());
} }
public function getPath() public function getImageUrls($width = null, $height = null)
{ {
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename; $image_urls = [];
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 getUrl() public function getInlineImage($width = null, $height = null, $className = 'inline-image')
{ {
return ASSETSURL . '/' . $this->subdir . '/' . $this->filename; $image_urls = $this->getImageUrls($width, $height);
return '<img class="' . $className . '" src="' . $image_urls[1] . '" alt=""' .
(isset($image_urls[2]) ? ' srcset="' . $image_urls[2] . ' 2x"' : '') . '>';
} }
/** /**
@@ -141,7 +160,8 @@ class Image extends Asset
} }
return Registry::get('db')->query(' return Registry::get('db')->query('
DELETE FROM assets_thumbs UPDATE 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 = ceil($this->recordCount / $this->items_per_page); $this->num_pages = max(1, 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 == 0) if ($this->num_pages <= 1)
{ {
$this->needsPageIndex = false; $this->needsPageIndex = false;
return; return;

View File

@@ -9,6 +9,7 @@
class Thumbnail class Thumbnail
{ {
private $image; private $image;
private $image_meta;
private $thumbnails; private $thumbnails;
private $properly_initialised; private $properly_initialised;
@@ -23,7 +24,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) public function __construct(Image $image)
{ {
$this->image = $image; $this->image = $image;
$this->image_meta = $image->getMeta(); $this->image_meta = $image->getMeta();
@@ -45,51 +46,45 @@ 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_path = '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector]; $thumb_filename = $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
if (file_exists(THUMBSDIR . $thumb_path)) if (file_exists(THUMBSDIR . '/' . $thumb_filename))
return THUMBSURL . $thumb_path; return THUMBSURL . '/' . $thumb_filename;
} }
// 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_thumb_path = '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector]; $custom_filename = $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
if (file_exists(ASSETSDIR . $custom_thumb_path)) if (file_exists(ASSETSDIR . '/' . $custom_filename))
{ {
// 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_thumb_path, THUMBSDIR . $custom_thumb_path); copy(ASSETSDIR . '/' . $custom_filename, THUMBSDIR . '/' . $custom_filename);
// 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_thumb_path; return THUMBSURL . '/' . $custom_filename;
} }
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 && array_key_exists($thumb_selector, $this->thumbnails)) if ($generate)
{ {
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.
elseif (!$generate)
{
$this->markAsQueued();
return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $this->width . 'x' . $this->height . $this->filename_suffix . '/';
}
// Still here..? What are you up to? ..Sneaking?
else 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)); $this->markAsQueued();
return BASEURL . '/thumbnail/' . $this->image->getId() . '/' . $thumb_selector . '/';
} }
} }
@@ -260,14 +255,18 @@ 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.
if (!is_dir(THUMBSDIR . '/' . $this->image->getSubdir())) $target_dir = THUMBSDIR . '/' . $this->image->getSubdir();
mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true); if (!is_dir($target_dir))
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(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $thumb_filename); $thumb->writeImage($target_dir . '/' . $thumb_filename);
// Let's remember this for future reference... // Let's remember this for future reference...
$this->markAsGenerated($thumb_filename); $this->markAsGenerated($thumb_filename);
@@ -278,7 +277,6 @@ 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());
@@ -336,11 +334,17 @@ 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 : ''; $this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : null;
}
// 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; return $success;
} }
else
throw new UnexpectedValueException('Thumbnail queuing query failed');
}
private function markAsQueued() private function markAsQueued()
{ {

View File

@@ -13,8 +13,10 @@ provided that the following conditions are met:
'use strict'; 'use strict';
function AutoSuggest(opt) { class AutoSuggest {
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") { constructor(opt) {
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" ||
typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
return; return;
} }
@@ -24,53 +26,46 @@ function AutoSuggest(opt) {
this.appendCallback = opt.appendCallback; this.appendCallback = opt.appendCallback;
this.baseurl = opt.baseUrl; this.baseurl = opt.baseUrl;
var self = this; this.input.addEventListener('keydown', event => this.doSelection(event), false);
this.input.addEventListener('keydown', function(event) { this.input.addEventListener('keyup', event => this.onType(event), false);
self.doSelection(event); }
}, false);
this.input.addEventListener('keyup', function(event) {
self.onType(this, event);
}, false);
}
AutoSuggest.prototype.doSelection = function(event) { doSelection(event) {
if (typeof this.container === "undefined" || this.container.children.length === 0) { if (typeof this.container === "undefined" || this.container.children.length === 0) {
return; return;
} }
switch (event.keyCode) { switch (event.key) {
case 13: // Enter case 'Enter':
event.preventDefault(); event.preventDefault();
this.container.children[this.selectedIndex].click(); this.container.children[this.selectedIndex].click();
break; break;
case 38: // Arrow up case 'ArrowUp':
case 40: // Arrow down case 'ArrowDown':
event.preventDefault(); event.preventDefault();
this.findSelectedElement().className = ''; this.findSelectedElement().className = '';
this.selectedIndex += event.keyCode === 38 ? -1 : 1; this.selectedIndex += event.key === 'ArrowUp' ? -1 : 1;
if (this.selectedIndex < 0) { if (this.selectedIndex < 0) {
this.selectedIndex = this.container.children.length - 1; this.selectedIndex = this.container.children.length - 1;
} else if (this.selectedIndex === this.container.children.length) { } else if (this.selectedIndex === this.container.children.length) {
this.selectedIndex = 0; this.selectedIndex = 0;
} }
var new_el = this.findSelectedElement().className = 'selected'; let new_el = this.findSelectedElement().className = 'selected';
break; break;
} }
}; };
AutoSuggest.prototype.findSelectedElement = function() { findSelectedElement() {
return this.container.children[this.selectedIndex]; return this.container.children[this.selectedIndex];
}; };
AutoSuggest.prototype.onType = function(input, event) { onType(event) {
if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) { if (['Enter', 'ArrowDown', 'ArrowUp'].indexOf(event.key) !== -1) {
return; return;
} }
var tokens = input.value.split(/\s+/).filter(function(token) { let tokens = event.target.value.split(/\s+/).filter(token => token.length >= 2);
return token.length >= 2;
});
if (tokens.length === 0) { if (tokens.length === 0) {
if (typeof this.container !== "undefined") { if (typeof this.container !== "undefined") {
@@ -79,17 +74,17 @@ AutoSuggest.prototype.onType = function(input, event) {
return false; return false;
} }
var request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" ")); let request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
var request = new HttpRequest('get', request_uri, {}, this.onReceive, this); let request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
}; };
AutoSuggest.prototype.onReceive = function(response, self) { onReceive(response, self) {
self.openContainer(); self.openContainer();
self.clearContainer(); self.clearContainer();
self.fillContainer(response); self.fillContainer(response);
}; };
AutoSuggest.prototype.openContainer = function() { openContainer() {
if (this.container) { if (this.container) {
if (!this.container.parentNode) { if (!this.container.parentNode) {
this.input.parentNode.appendChild(this.container); this.input.parentNode.appendChild(this.container);
@@ -101,76 +96,82 @@ AutoSuggest.prototype.openContainer = function() {
this.container.className = 'autosuggest'; this.container.className = 'autosuggest';
this.input.parentNode.appendChild(this.container); this.input.parentNode.appendChild(this.container);
return this.container; return this.container;
}; };
AutoSuggest.prototype.clearContainer = function() { clearContainer() {
while (this.container.children.length > 0) { while (this.container.children.length > 0) {
this.container.removeChild(this.container.children[0]); this.container.removeChild(this.container.children[0]);
} }
}; };
AutoSuggest.prototype.clearInput = function() { clearInput() {
this.input.value = ""; this.input.value = "";
this.input.focus(); this.input.focus();
}; };
AutoSuggest.prototype.closeContainer = function() { closeContainer() {
this.container.parentNode.removeChild(this.container); this.container.parentNode.removeChild(this.container);
}; };
AutoSuggest.prototype.fillContainer = function(response) { fillContainer(response) {
var self = this;
this.selectedIndex = 0; this.selectedIndex = 0;
response.items.forEach(function(item, i) {
var node = document.createElement('li'); let query = this.input.value.trim().replace(/[\-\[\]{}()*+?.,\\\/^\$|#]/g, ' ');
var text = document.createTextNode(item.label); let query_tokens = query.split(/ +/).sort((a,b) => a.length - b.length);
response.items.forEach((item, i) => {
let node = document.createElement('li');
node.innerHTML = this.highlightMatches(query_tokens, item.label);
node.jsondata = item; node.jsondata = item;
node.addEventListener('click', function(event) { node.addEventListener('click', event => {
self.appendCallback(this.jsondata); this.appendCallback(event.target.jsondata);
self.closeContainer(); this.closeContainer();
self.clearInput(); this.clearInput();
}); });
node.appendChild(text); this.container.appendChild(node);
self.container.appendChild(node); if (this.container.children.length === 1) {
if (self.container.children.length === 1) {
node.className = 'selected'; node.className = 'selected';
} }
}); });
}; };
highlightMatches(query_tokens, item) {
function TagAutoSuggest(opt) { let itemTokens = item.split(/ +/);
AutoSuggest.prototype.constructor.call(this, opt); let queryTokens = new RegExp('(' + query_tokens.join('\|') + ')', 'i');
this.type = "tags"; itemTokens.forEach((token, index) => {
item = item.replace(token, token.replace(queryTokens, ($1, match) => '<strong>' + match + '</strong>'));
});
return item;
};
} }
TagAutoSuggest.prototype = Object.create(AutoSuggest.prototype); class TagAutoSuggest extends AutoSuggest {
constructor(opt) {
super(opt);
this.type = "tags";
}
TagAutoSuggest.prototype.constructor = TagAutoSuggest; fillContainer(response) {
TagAutoSuggest.prototype.fillContainer = function(response) {
if (response.items.length > 0) { if (response.items.length > 0) {
AutoSuggest.prototype.fillContainer.call(this, response); super.fillContainer.call(this, response);
} else { } else {
var node = document.createElement('li') let node = document.createElement('li')
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>"; node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
var self = this; node.addEventListener('click', event => {
node.addEventListener('click', function(event) { this.createNewTag(response => this.appendCallback(response));
self.createNewTag(function(response) { this.closeContainer();
self.appendCallback(response); this.clearInput();
});
self.closeContainer();
self.clearInput();
}); });
self.container.appendChild(node); this.container.appendChild(node);
this.selectedIndex = 0; this.selectedIndex = 0;
node.className = 'selected'; node.className = 'selected';
} }
}; };
TagAutoSuggest.prototype.createNewTag = function(callback) { createNewTag(callback) {
var request_uri = this.baseurl + '/suggest/?type=createtag'; let request_uri = this.baseurl + '/suggest/?type=createtag';
var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this); let request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
}
} }

View File

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

View File

@@ -90,8 +90,14 @@ 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, '"> <a href="', $image->getPageUrl(), $this->url_suffix, '#photo_frame">
<img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '" alt="" title="', $image->getTitle(), '">'; <img src="', $image->getThumbnailUrl($width, $height, $crop, $fit), '"';
// 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 '