forked from Public/pics
Compare commits
106 Commits
version-1.
...
master
Author | SHA1 | Date |
---|---|---|
Aaron van Geffen | 0ec0de4414 | |
Roflin | 69417c36ed | |
Bart Schuurmans | f2d8a32e67 | |
Roflin | 4863561129 | |
Roflin | 8474d3b2b2 | |
Aaron van Geffen | 3bf69fd21f | |
Aaron van Geffen | 237f4005bd | |
Aaron van Geffen | 4bf4641428 | |
Aaron van Geffen | ff808ba18d | |
Aaron van Geffen | 6c662481bc | |
Aaron van Geffen | af73f00701 | |
Aaron van Geffen | 681af07985 | |
Aaron van Geffen | cba42a9129 | |
Aaron van Geffen | 96937b6952 | |
Aaron van Geffen | 5c55e45c3c | |
Aaron van Geffen | 70e6001c85 | |
Aaron van Geffen | 4402521051 | |
Aaron van Geffen | 889302cd36 | |
Aaron van Geffen | cae5c6e5cf | |
Aaron van Geffen | 162d14b35f | |
Aaron van Geffen | 555c61937b | |
Aaron van Geffen | d069ddca18 | |
Aaron van Geffen | 71b71f8a03 | |
Aaron van Geffen | 2885e24456 | |
Aaron van Geffen | c72e24c0c7 | |
Aaron van Geffen | b8191bf554 | |
Aaron van Geffen | 3594b3d021 | |
Aaron van Geffen | 936d3d20db | |
Aaron van Geffen | 5c4a075231 | |
Aaron van Geffen | 6ddf518294 | |
Aaron van Geffen | 66a411973a | |
Aaron van Geffen | a83b938f8a | |
Aaron van Geffen | 5344378333 | |
Aaron van Geffen | 8147e2b97d | |
Aaron van Geffen | d562c70667 | |
Aaron van Geffen | 5599ff8d9b | |
Aaron van Geffen | e7490e40dd | |
Aaron van Geffen | 6fcc2eb59f | |
Aaron van Geffen | b793e05980 | |
Aaron van Geffen | 340ed84272 | |
Aaron van Geffen | 93884e2e93 | |
Aaron van Geffen | 2a740d8cef | |
Aaron van Geffen | 5e0d4df2f7 | |
Aaron van Geffen | e84c4f2b43 | |
Aaron van Geffen | 893d31af52 | |
Aaron van Geffen | 5895f4faa6 | |
Aaron van Geffen | 8e7a09f3f3 | |
Aaron van Geffen | 837c92db44 | |
Aaron van Geffen | c392105814 | |
Aaron van Geffen | 9d95df81fe | |
Aaron van Geffen | d4cc72304e | |
Aaron van Geffen | 2c68b6a798 | |
Aaron van Geffen | fd84e1c9f8 | |
Aaron van Geffen | 8d02662eb3 | |
Aaron van Geffen | 31f4edc996 | |
Aaron van Geffen | a208c0482f | |
Roflin | 909d50efa8 | |
Aaron van Geffen | bd1ca8d18c | |
Aaron van Geffen | c7d3b9c3d1 | |
Aaron van Geffen | 5a51778a6a | |
Aaron van Geffen | 2bb29d7224 | |
Aaron van Geffen | 1b7e83e11e | |
Aaron van Geffen | 354e54a0af | |
Aaron van Geffen | 17859b70e9 | |
Aaron van Geffen | 6a7defcdc9 | |
Aaron van Geffen | f193b614b7 | |
Aaron van Geffen | 12ea378b02 | |
Aaron van Geffen | 62900e7f81 | |
Aaron van Geffen | c48ba786c1 | |
Aaron van Geffen | 3694819d13 | |
Aaron van Geffen | d7b68995e8 | |
Aaron van Geffen | 5df7ea8371 | |
Aaron van Geffen | 7d3ab166c7 | |
Aaron van Geffen | ed6054e6b6 | |
Roflin | 3fc8ccf550 | |
Roflin | 6a7c7af7b8 | |
Aaron van Geffen | 8ec6c227d5 | |
Aaron van Geffen | 42e5c7fe37 | |
Aaron van Geffen | 05c48be785 | |
Aaron van Geffen | d3cb750874 | |
Dennis Brentjes | 20db3561cf | |
Dennis Brentjes | 768f5ee529 | |
Dennis Brentjes | 16ec547064 | |
Dennis Brentjes | e40c05c1f8 | |
Dennis Brentjes | 344db6e4c5 | |
Dennis Brentjes | fcbbc7106d | |
Dennis Brentjes | 331193019c | |
Aaron van Geffen | bcbb74a680 | |
Aaron van Geffen | c6c249787f | |
Aaron van Geffen | 068d1dad3e | |
Aaron van Geffen | f1408ad2ee | |
Dennis Brentjes | 8b73420936 | |
Aaron van Geffen | ee304dd7b9 | |
Aaron van Geffen | 1def1484cb | |
Aaron van Geffen | 981b652e25 | |
Aaron van Geffen | cda7f3115c | |
Aaron van Geffen | e439a074a6 | |
Aaron van Geffen | ee9bdd45c0 | |
Aaron van Geffen | 9fe8acc747 | |
Aaron van Geffen | 096cea078c | |
Aaron van Geffen | 2a25434862 | |
Aaron van Geffen | 943297900c | |
Aaron van Geffen | 95e289d82d | |
Aaron van Geffen | 1a15e347f2 | |
Aaron van Geffen | 31e1357b47 | |
Aaron van Geffen | 08cc6b1c77 |
|
@ -0,0 +1,11 @@
|
|||
Copyright 2016-2021 Stichting HashRU
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,31 @@
|
|||
# HashRU Pics
|
||||
|
||||
This is the development repository for the HashRU photo website.
|
||||
The CMS and its modules originate in [Kabuki CMS](https://aaronweb.net/projects/kabuki/), but have been extended and are maintained separately in this repository.
|
||||
|
||||
## Requirements
|
||||
|
||||
The Kabuki codebase requires the following PHP extensions to be enabled for full operation:
|
||||
|
||||
* exif
|
||||
* imagick (PECL)
|
||||
* mysqli
|
||||
|
||||
## Setup
|
||||
|
||||
Copy `config.php.dist` to `config.php` and set-up the constants contained in the file.
|
||||
|
||||
## Running
|
||||
|
||||
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`.
|
||||
|
||||
For a production environment, please set up a proper PHP-FPM environment instead.
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome over at the [HashRU Gitea](https://gitea.hashru.nl/Public/pics/pulls).
|
||||
|
||||
## License
|
||||
|
||||
The HashRU Pics repository is licensed with a BSD 3-clause license, as is Kabuki CMS.
|
10
TODO.md
10
TODO.md
|
@ -1,11 +1,11 @@
|
|||
TODO:
|
||||
|
||||
* Gebruikers *persoons*tags laten verwijderen
|
||||
* Alleen persoonstags tonen bij foto's.
|
||||
|
||||
* Gebruikers persoonstags laten verwijderen vanaf fotopagina.
|
||||
|
||||
* 'Return to album' knop toevoegen die naar juiste pagina leidt.
|
||||
|
||||
* Bij taggen van user: thumbnail setten
|
||||
|
||||
* Grid herberekenen; captions weglaten.
|
||||
|
||||
* Sortering: alleen *albums* ASC, personen DESC!
|
||||
|
||||
* Album/tag management
|
||||
|
|
7
app.php
7
app.php
|
@ -16,16 +16,15 @@ require_once 'vendor/autoload.php';
|
|||
Registry::set('start', microtime(true));
|
||||
Registry::set('db', new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME));
|
||||
|
||||
// Handle errors our own way.
|
||||
ErrorHandler::enable();
|
||||
|
||||
// Do some authentication checks.
|
||||
Session::start();
|
||||
$user = Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest();
|
||||
$user->updateAccessTime();
|
||||
Registry::set('user', $user);
|
||||
|
||||
// Handle errors our own way.
|
||||
set_error_handler('ErrorHandler::handleError');
|
||||
ini_set("display_errors", DEBUG ? "On" : "Off");
|
||||
|
||||
// The real magic starts here!
|
||||
ob_start();
|
||||
Dispatcher::dispatch();
|
||||
|
|
|
@ -14,6 +14,9 @@ const CACHE_KEY_PREFIX = 'hashru_';
|
|||
const BASEDIR = __DIR__;
|
||||
const BASEURL = 'https://pics.hashru.nl'; // 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';
|
||||
|
@ -29,5 +32,5 @@ const DB_PASS = '';
|
|||
const DB_NAME = 'hashru_pics';
|
||||
const DB_LOG_QUERIES = false;
|
||||
|
||||
const SITE_TITLE = 'HashRU';
|
||||
const SITE_TITLE = 'HashRU Pics';
|
||||
const SITE_SLOGAN = 'Nijmeegs Nerdclubje';
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* Download.php
|
||||
* Contains the code to download an album.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2019, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Download
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure we're logged in at this point.
|
||||
$user = Registry::get('user');
|
||||
if (!$user->isLoggedIn())
|
||||
throw new NotAllowedException();
|
||||
|
||||
if (!isset($_GET['tag']))
|
||||
throw new UserFacingException('No album or tag has been specified for download.');
|
||||
|
||||
$tag = (int)$_GET['tag'];
|
||||
$album = Tag::fromId($tag);
|
||||
|
||||
if (isset($_SESSION['current_export']))
|
||||
throw new UserFacingException('You can only export one album at the same time. Please wait until the other download finishes, or try again later.');
|
||||
|
||||
// So far so good?
|
||||
$this->exportAlbum($album);
|
||||
exit;
|
||||
}
|
||||
|
||||
private function exportAlbum(Tag $album)
|
||||
{
|
||||
$files = [];
|
||||
|
||||
$album_ids = array_merge([$album->id_tag], $this->getChildAlbumIds($album->id_tag));
|
||||
foreach ($album_ids as $album_id)
|
||||
{
|
||||
$iterator = AssetIterator::getByOptions(['id_tag' => $album_id]);
|
||||
while ($asset = $iterator->next())
|
||||
$files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]);
|
||||
}
|
||||
|
||||
$descriptorspec = [
|
||||
0 => ['pipe', 'r'], // STDIN
|
||||
1 => ['pipe', 'w'], // STDOUT
|
||||
];
|
||||
|
||||
// Prevent simultaneous exports.
|
||||
$_SESSION['current_export'] = $album->id_tag;
|
||||
|
||||
// Allow new exports if the connection is terminated unexpectedly (e.g. when a user aborts a download).
|
||||
register_shutdown_function(function() {
|
||||
if (isset($_SESSION['current_export']))
|
||||
unset($_SESSION['current_export']);
|
||||
});
|
||||
|
||||
$command = 'tar -cf - -C ' . escapeshellarg(ASSETSDIR) . ' --null -T -';
|
||||
|
||||
$proc = proc_open($command, $descriptorspec, $pipes, ASSETSDIR);
|
||||
|
||||
if(!$proc)
|
||||
throw new UnexpectedValueException('Could not execute TAR command');
|
||||
|
||||
if(!$pipes[0])
|
||||
throw new UnexpectedValueException('Could not open pipe for STDIN');
|
||||
|
||||
if(!$pipes[1])
|
||||
throw new UnexpectedValueException('Could not open pipe for STDOUT');
|
||||
|
||||
// STDOUT should not block.
|
||||
stream_set_blocking($pipes[1], 0);
|
||||
|
||||
header('Pragma: no-cache');
|
||||
header('Content-Description: File Download');
|
||||
header('Content-disposition: attachment; filename="' . $album->tag . '.tar"');
|
||||
header('Content-Type: application/octet-stream');
|
||||
header('Content-Transfer-Encoding: binary');
|
||||
|
||||
// Write filenames to include to STDIN, separated by null bytes.
|
||||
foreach ($files as $file)
|
||||
fwrite($pipes[0], $file . "\0");
|
||||
|
||||
// Close STDIN pipe to start archiving.
|
||||
fclose($pipes[0]);
|
||||
|
||||
// At this point, end output buffering so we can enjoy more than ~62MB of photos.
|
||||
ob_end_flush();
|
||||
|
||||
do
|
||||
{
|
||||
// Read STDOUT as `tar` is doing its work.
|
||||
echo stream_get_contents($pipes[1], 4096);
|
||||
|
||||
// Are we still running?
|
||||
$status = proc_get_status($proc);
|
||||
}
|
||||
while (!empty($status) && $status['running']);
|
||||
|
||||
// Close STDOUT pipe and clean up process.
|
||||
fclose($pipes[1]);
|
||||
|
||||
proc_close($proc);
|
||||
|
||||
// Allow new exports from this point onward.
|
||||
unset($_SESSION['current_export']);
|
||||
}
|
||||
|
||||
private function getChildAlbumIds($parent_id)
|
||||
{
|
||||
$ids = [];
|
||||
|
||||
$albums = Tag::getAlbums($parent_id, 0, PHP_INT_MAX);
|
||||
foreach ($albums as $album)
|
||||
{
|
||||
$ids[] = $album['id_tag'];
|
||||
$ids = array_merge($ids, $this->getChildAlbumIds($album['id_tag']));
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* EditAlbum.php
|
||||
* Contains the album edit controller.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class EditAlbum extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
$id_tag = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if (empty($id_tag) && !isset($_GET['add']) && $_GET['action'] !== 'addalbum')
|
||||
throw new UnexpectedValueException('Requested album not found or not requesting a new album.');
|
||||
|
||||
// Adding an album?
|
||||
if (isset($_GET['add']) || $_GET['action'] === 'addalbum')
|
||||
{
|
||||
parent::__construct('Add a new album');
|
||||
$form_title = 'Add a new album';
|
||||
$this->page->addClass('editalbum');
|
||||
}
|
||||
// Deleting one?
|
||||
elseif (isset($_GET['delete']))
|
||||
{
|
||||
// So far so good?
|
||||
$album = Tag::fromId($id_tag);
|
||||
if (Session::validateSession('get') && $album->kind === 'Album' && $album->delete())
|
||||
{
|
||||
header('Location: ' . BASEURL . '/managealbums/');
|
||||
exit;
|
||||
}
|
||||
else
|
||||
trigger_error('Cannot delete album: an error occured while processing the request.', E_USER_ERROR);
|
||||
}
|
||||
// Editing one, then, surely.
|
||||
else
|
||||
{
|
||||
$album = Tag::fromId($id_tag);
|
||||
if ($album->kind !== 'Album')
|
||||
trigger_error('Cannot edit album: not an album.', E_USER_ERROR);
|
||||
|
||||
parent::__construct('Edit album \'' . $album->tag . '\'');
|
||||
$form_title = 'Edit album \'' . $album->tag . '\'';
|
||||
$this->page->addClass('editalbum');
|
||||
}
|
||||
|
||||
// Session checking!
|
||||
if (empty($_POST))
|
||||
Session::resetSessionToken();
|
||||
else
|
||||
Session::validateSession();
|
||||
|
||||
if ($id_tag)
|
||||
$after_form = '<a href="' . BASEURL . '/editalbum/?id=' . $id_tag . '&delete&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken() . '" class="btn btn-danger" onclick="return confirm(\'Are you sure you want to delete this album? You cannot undo this!\');">Delete album</a>';
|
||||
elseif (!$id_tag)
|
||||
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>';
|
||||
|
||||
$form = new Form([
|
||||
'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
|
||||
'content_below' => $after_form,
|
||||
'fields' => [
|
||||
'id_parent' => [
|
||||
'type' => 'numeric',
|
||||
'label' => 'Parent album ID',
|
||||
],
|
||||
'id_asset_thumb' => [
|
||||
'type' => 'numeric',
|
||||
'label' => 'Thumbnail asset ID',
|
||||
'is_optional' => true,
|
||||
],
|
||||
'tag' => [
|
||||
'type' => 'text',
|
||||
'label' => 'Album title',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'slug' => [
|
||||
'type' => 'text',
|
||||
'label' => 'URL slug',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'textbox',
|
||||
'label' => 'Description',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
'is_optional' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
if (empty($_POST) && isset($_GET['tag']))
|
||||
{
|
||||
$parentTag = Tag::fromId($_GET['tag']);
|
||||
if ($parentTag->kind === 'Album')
|
||||
{
|
||||
$formDefaults = [
|
||||
'id_parent' => $parentTag->id_tag,
|
||||
'tag' => 'New Album Title Here',
|
||||
'slug' => ($parentTag->slug ? $parentTag->slug . '/' : '') . 'NEW_ALBUM_SLUG_HERE',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($formDefaults))
|
||||
$formDefaults = isset($album) ? get_object_vars($album) : $_POST;
|
||||
|
||||
// Create the form, add in default values.
|
||||
$form->setData($formDefaults);
|
||||
$formview = new FormView($form, $form_title ?? '');
|
||||
$this->page->adopt($formview);
|
||||
|
||||
if (!empty($_POST))
|
||||
{
|
||||
$form->verify($_POST);
|
||||
|
||||
// Anything missing?
|
||||
if (!empty($form->getMissing()))
|
||||
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error'));
|
||||
|
||||
$data = $form->getData();
|
||||
|
||||
// Quick stripping.
|
||||
$data['tag'] = htmlentities($data['tag']);
|
||||
$data['description'] = htmlentities($data['description']);
|
||||
$data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
|
||||
|
||||
// TODO: when updating slug, update slug for all photos in this album.
|
||||
|
||||
// Creating a new album?
|
||||
if (!$id_tag)
|
||||
{
|
||||
$data['kind'] = 'Album';
|
||||
$newTag = Tag::createNew($data);
|
||||
if ($newTag === false)
|
||||
return $formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'error'));
|
||||
|
||||
if (isset($_POST['submit_and_new']))
|
||||
{
|
||||
header('Location: ' . BASEURL . '/editalbum/?add&tag=' . $data['id_parent']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// Just updating?
|
||||
else
|
||||
{
|
||||
foreach ($data as $key => $value)
|
||||
$album->$key = $value;
|
||||
|
||||
$album->save();
|
||||
}
|
||||
|
||||
// Redirect to the album management page.
|
||||
header('Location: ' . BASEURL . '/managealbums/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,10 +33,11 @@ class EditAsset extends HTMLController
|
|||
}
|
||||
|
||||
// Key info
|
||||
if (isset($_POST['title'], $_POST['date_captured'], $_POST['priority']))
|
||||
if (isset($_POST['title'], $_POST['slug'], $_POST['date_captured'], $_POST['priority']))
|
||||
{
|
||||
$date_captured = !empty($_POST['date_captured']) ? new DateTime($_POST['date_captured']) : null;
|
||||
$asset->setKeyData(htmlentities($_POST['title']), $date_captured, intval($_POST['priority']));
|
||||
$slug = strtr($_POST['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
|
||||
$asset->setKeyData(htmlentities($_POST['title']), $slug, $date_captured, intval($_POST['priority']));
|
||||
}
|
||||
|
||||
// Handle tags
|
||||
|
@ -76,11 +77,11 @@ class EditAsset extends HTMLController
|
|||
$image->removeAllThumbnails();
|
||||
}
|
||||
}
|
||||
elseif (preg_match('~^thumb_(\d+)x(\d+)(_c[best]?)?$~', $_POST['replacement_target']))
|
||||
elseif (preg_match('~^thumb_(\d+x\d+(?:_c[best]?)?)$~', $_POST['replacement_target'], $match))
|
||||
{
|
||||
$image = $asset->getImage();
|
||||
if (($replace_result = $image->replaceThumbnail($_POST['replacement_target'], $_FILES['replacement']['tmp_name'])) !== 0)
|
||||
throw new Exception('Could not replace thumbnail \'' . $_POST['replacement_target'] . '\' with the uploaded file. Error code: ' . $replace_result);
|
||||
if (($replace_result = $image->replaceThumbnail($match[1], $_FILES['replacement']['tmp_name'])) !== 0)
|
||||
throw new Exception('Could not replace thumbnail \'' . $match[1] . '\' with the uploaded file. Error code: ' . $replace_result);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,30 +94,47 @@ class EditAsset extends HTMLController
|
|||
$page = new EditAssetForm($asset, $thumbs);
|
||||
parent::__construct('Edit asset \'' . $asset->getTitle() . '\' (' . $asset->getFilename() . ') - ' . SITE_TITLE);
|
||||
$this->page->adopt($page);
|
||||
|
||||
// Add a view button to the admin bar for photos.
|
||||
if ($asset->isImage())
|
||||
$this->admin_bar->appendItem($asset->getImage()->getPageUrl(), 'View this photo');
|
||||
}
|
||||
|
||||
private function getThumbs(Asset $asset)
|
||||
{
|
||||
$path = $asset->getPath();
|
||||
if (!$asset->isImage())
|
||||
return [];
|
||||
|
||||
$image = $asset->getImage();
|
||||
$subdir = $image->getSubdir();
|
||||
$metadata = $image->getMeta();
|
||||
$thumb_selectors = $image->getThumbnails();
|
||||
|
||||
$thumbs = [];
|
||||
$metadata = $asset->getMeta();
|
||||
foreach ($metadata as $key => $meta)
|
||||
foreach ($thumb_selectors as $selector => $filename)
|
||||
{
|
||||
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $key, $thumb))
|
||||
if (!preg_match('~^(?<width>\d+)x(?<height>\d+)(?<suffix>_c(?<method>[best]?))?$~', $selector, $thumb))
|
||||
continue;
|
||||
|
||||
$has_crop_boundary = isset($metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']]);
|
||||
$has_custom_image = isset($metadata['custom_' . $thumb['width'] . 'x' . $thumb['height']]);
|
||||
$dimensions = $thumb['width'] . 'x' . $thumb['height'];
|
||||
|
||||
// Does the thumbnail exist on disk? If not, use an url to generate it.
|
||||
if (!$filename || !file_exists(THUMBSDIR . '/' . $subdir . '/' . $filename))
|
||||
$thumb_url = BASEURL . '/thumbnail/' . $image->getId() . '/' . $dimensions . ($thumb['suffix'] ?? '') . '/';
|
||||
else
|
||||
$thumb_url = THUMBSURL . '/' . $subdir . '/' . $filename;
|
||||
|
||||
$has_crop_boundary = isset($metadata['crop_' . $dimensions]);
|
||||
$has_custom_image = isset($metadata['custom_' . $dimensions]);
|
||||
|
||||
$thumbs[] = [
|
||||
'dimensions' => [(int) $thumb['width'], (int) $thumb['height']],
|
||||
'cropped' => !$has_custom_image && (!empty($thumb['suffix']) || $has_crop_boundary),
|
||||
'crop_method' => !$has_custom_image && !empty($thumb['method']) ? $thumb['method'] : (!empty($thumb['suffix']) ? 'c' : null),
|
||||
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $thumb['width'] . 'x' . $thumb['height']] : null,
|
||||
'crop_region' => $has_crop_boundary ? $metadata['crop_' . $dimensions] : null,
|
||||
'custom_image' => $has_custom_image,
|
||||
'filename' => $meta,
|
||||
'full_path' => THUMBSDIR . '/' . $path . '/' . $meta,
|
||||
'url' => THUMBSURL . '/' . $path . '/' . $meta,
|
||||
'status' => file_exists(THUMBSDIR . '/' . $path . '/' . $meta),
|
||||
'filename' => $filename,
|
||||
'url' => $thumb_url,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -133,18 +151,19 @@ class EditAsset extends HTMLController
|
|||
$crop_value = $data->crop_width . ',' . $data->crop_height . ',' . $data->source_x . ',' . $data->source_y;
|
||||
$meta[$crop_key] = $crop_value;
|
||||
|
||||
// If we uploaded a custom thumbnail, stop considering it such.
|
||||
// If we previously uploaded a custom thumbnail, stop considering it such.
|
||||
$custom_key = 'custom_' . $data->thumb_width . 'x' . $data->thumb_height;
|
||||
if (isset($meta[$custom_key]))
|
||||
{
|
||||
// TODO: delete from disk
|
||||
unset($meta[$custom_key]);
|
||||
}
|
||||
|
||||
// Save meta changes so far.
|
||||
$image->setMetaData($meta);
|
||||
|
||||
// Force a rebuild of related thumbnails.
|
||||
$thumb_key = 'thumb_' . $data->thumb_width . 'x' . $data->thumb_height;
|
||||
foreach ($meta as $meta_key => $meta_value)
|
||||
if ($meta_key === $thumb_key || strpos($meta_key, $thumb_key . '_') !== false)
|
||||
unset($meta[$meta_key]);
|
||||
|
||||
$image->setMetaData($meta);
|
||||
$image->removeThumbnailsOfSize($data->thumb_width, $data->thumb_height);
|
||||
|
||||
$payload = [
|
||||
'key' => $crop_key,
|
||||
|
|
|
@ -0,0 +1,152 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* EditTag.php
|
||||
* Contains the tag edit controller.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class EditTag extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
$id_tag = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if (empty($id_tag) && !isset($_GET['add']))
|
||||
throw new UnexpectedValueException('Requested tag not found or not requesting a new tag.');
|
||||
|
||||
// Adding an tag?
|
||||
if (isset($_GET['add']))
|
||||
{
|
||||
parent::__construct('Add a new tag');
|
||||
$form_title = 'Add a new tag';
|
||||
$this->page->addClass('edittag');
|
||||
}
|
||||
// Deleting one?
|
||||
elseif (isset($_GET['delete']))
|
||||
{
|
||||
// So far so good?
|
||||
$tag = Tag::fromId($id_tag);
|
||||
if (Session::validateSession('get') && $tag->kind !== 'Album' && $tag->delete())
|
||||
{
|
||||
header('Location: ' . BASEURL . '/managetags/');
|
||||
exit;
|
||||
}
|
||||
else
|
||||
trigger_error('Cannot delete tag: an error occured while processing the request.', E_USER_ERROR);
|
||||
}
|
||||
// Editing one, then, surely.
|
||||
else
|
||||
{
|
||||
$tag = Tag::fromId($id_tag);
|
||||
if ($tag->kind === 'Album')
|
||||
trigger_error('Cannot edit tag: is actually an album.', E_USER_ERROR);
|
||||
|
||||
parent::__construct('Edit tag \'' . $tag->tag . '\'');
|
||||
$form_title = 'Edit tag \'' . $tag->tag . '\'';
|
||||
$this->page->addClass('edittag');
|
||||
}
|
||||
|
||||
// Session checking!
|
||||
if (empty($_POST))
|
||||
Session::resetSessionToken();
|
||||
else
|
||||
Session::validateSession();
|
||||
|
||||
if ($id_tag)
|
||||
$after_form = '<a href="' . BASEURL . '/edittag/?id=' . $id_tag . '&delete&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken() . '" class="btn btn-danger" onclick="return confirm(\'Are you sure you want to delete this tag? You cannot undo this!\');">Delete tag</a>';
|
||||
elseif (!$id_tag)
|
||||
$after_form = '<button name="submit_and_new" class="btn">Save and add another</button>';
|
||||
|
||||
$form = new Form([
|
||||
'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'),
|
||||
'content_below' => $after_form,
|
||||
'fields' => [
|
||||
'id_parent' => [
|
||||
'type' => 'numeric',
|
||||
'label' => 'Parent tag ID',
|
||||
],
|
||||
'id_asset_thumb' => [
|
||||
'type' => 'numeric',
|
||||
'label' => 'Thumbnail asset ID',
|
||||
'is_optional' => true,
|
||||
],
|
||||
'kind' => [
|
||||
'type' => 'select',
|
||||
'label' => 'Kind of tag',
|
||||
'options' => [
|
||||
'Location' => 'Location',
|
||||
'Person' => 'Person',
|
||||
],
|
||||
],
|
||||
'tag' => [
|
||||
'type' => 'text',
|
||||
'label' => 'Tag title',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'slug' => [
|
||||
'type' => 'text',
|
||||
'label' => 'URL slug',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
],
|
||||
'description' => [
|
||||
'type' => 'textbox',
|
||||
'label' => 'Description',
|
||||
'size' => 50,
|
||||
'maxlength' => 255,
|
||||
'is_optional' => true,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
// Create the form, add in default values.
|
||||
$form->setData($id_tag ? get_object_vars($tag) : $_POST);
|
||||
$formview = new FormView($form, $form_title ?? '');
|
||||
$this->page->adopt($formview);
|
||||
|
||||
if (!empty($_POST))
|
||||
{
|
||||
$form->verify($_POST);
|
||||
|
||||
// Anything missing?
|
||||
if (!empty($form->getMissing()))
|
||||
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error'));
|
||||
|
||||
$data = $form->getData();
|
||||
|
||||
// Quick stripping.
|
||||
$data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']);
|
||||
|
||||
// Creating a new tag?
|
||||
if (!$id_tag)
|
||||
{
|
||||
$return = Tag::createNew($data);
|
||||
if ($return === false)
|
||||
return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'error'));
|
||||
|
||||
if (isset($_POST['submit_and_new']))
|
||||
{
|
||||
header('Location: ' . BASEURL . '/edittag/?add');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
// Just updating?
|
||||
else
|
||||
{
|
||||
foreach ($data as $key => $value)
|
||||
$tag->$key = $value;
|
||||
|
||||
$tag->save();
|
||||
}
|
||||
|
||||
// Redirect to the tag management page.
|
||||
header('Location: ' . BASEURL . '/managetags/');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,9 +24,8 @@ class EditUser extends HTMLController
|
|||
// Adding a user?
|
||||
if (isset($_GET['add']))
|
||||
{
|
||||
parent::__construct('Add a new user');
|
||||
$view = new DummyBox('Add a new user');
|
||||
$this->page->adopt($view);
|
||||
$form_title = 'Add a new user';
|
||||
parent::__construct($form_title);
|
||||
$this->page->addClass('edituser');
|
||||
}
|
||||
// Deleting one?
|
||||
|
@ -50,9 +49,8 @@ class EditUser extends HTMLController
|
|||
else
|
||||
{
|
||||
$user = Member::fromId($id_user);
|
||||
parent::__construct('Edit user \'' . $user->getFullName() . '\'');
|
||||
$view = new DummyBox('Edit user \'' . $user->getFullName() . '\'');
|
||||
$this->page->adopt($view);
|
||||
$form_title = 'Edit user \'' . $user->getFullName() . '\'';
|
||||
parent::__construct($form_title);
|
||||
$this->page->addClass('edituser');
|
||||
}
|
||||
|
||||
|
@ -122,8 +120,8 @@ class EditUser extends HTMLController
|
|||
|
||||
// Create the form, add in default values.
|
||||
$form->setData($id_user ? $user->getProps() : $_POST);
|
||||
$formview = new FormView($form);
|
||||
$view->adopt($formview);
|
||||
$formview = new FormView($form, $form_title);
|
||||
$this->page->adopt($formview);
|
||||
|
||||
if (!empty($_POST))
|
||||
{
|
||||
|
@ -131,7 +129,7 @@ class EditUser extends HTMLController
|
|||
|
||||
// Anything missing?
|
||||
if (!empty($form->getMissing()))
|
||||
return $formview->adopt(new DummyBox('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing())));
|
||||
return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error'));
|
||||
|
||||
$data = $form->getData();
|
||||
|
||||
|
@ -152,18 +150,18 @@ class EditUser extends HTMLController
|
|||
|
||||
// If it looks like an e-mail address...
|
||||
if (!empty($data['emailaddress']) && !preg_match('~^[^ ]+@[^ ]+\.[a-z]+$~', $data['emailaddress']))
|
||||
return $formview->adopt(new DummyBox('Email addresses invalid', 'The email address you entered is not a valid email address.'));
|
||||
return $formview->adopt(new Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'error'));
|
||||
// Check whether email address is already linked to an account in the database -- just not to the account we happen to be editing, of course.
|
||||
elseif (!empty($data['emailaddress']) && Member::exists($data['emailaddress']) && !($id_user && $user->getEmailAddress() == $data['emailaddress']))
|
||||
return $formview->adopt(new DummyBox('Email address already in use', 'Another account is already using the e-mail address you entered.'));
|
||||
return $formview->adopt(new Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'error'));
|
||||
|
||||
// Setting passwords? We'll need two!
|
||||
if (!$id_user || !empty($data['password1']) && !empty($data['password2']))
|
||||
{
|
||||
if (strlen($data['password1']) < 6 || !preg_match('~[^A-z]~', $data['password1']))
|
||||
return $formview->adopt(new DummyBox('Password not acceptable', 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).'));
|
||||
return $formview->adopt(new Alert('Password not acceptable', 'Please fill in a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).', 'error'));
|
||||
elseif ($data['password1'] !== $data['password2'])
|
||||
return $formview->adopt(new DummyBox('Passwords do not match', 'The passwords you entered do not match. Please try again.'));
|
||||
return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'error'));
|
||||
else
|
||||
$data['password'] = $data['password1'];
|
||||
|
||||
|
@ -175,7 +173,7 @@ class EditUser extends HTMLController
|
|||
{
|
||||
$return = Member::createNew($data);
|
||||
if ($return === false)
|
||||
return $formview->adopt(new DummyBox('Cannot create this user', 'Something went wrong while creating the user...'));
|
||||
return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'error'));
|
||||
|
||||
if (isset($_POST['submit_and_new']))
|
||||
{
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* GenerateThumbnail.php
|
||||
* Contains the asynchronous thumbnail generation controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class GenerateThumbnail extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$asset = Asset::fromId($_GET['id']);
|
||||
if (empty($asset) || !$asset->isImage())
|
||||
throw new NotFoundException('Image not found');
|
||||
|
||||
$image = $asset->getImage();
|
||||
$crop_mode = isset($_GET['mode']) ? $_GET['mode'] : false;
|
||||
$url = $image->getThumbnailUrl($_GET['width'], $_GET['height'], $crop_mode, true, true);
|
||||
|
||||
if ($url)
|
||||
{
|
||||
header('Location: ' . $url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,19 +3,16 @@
|
|||
* JSONController.php
|
||||
* Contains the key JSON controller
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
* Kabuki CMS (C) 2013-2019, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
/**
|
||||
* The abstract class that allows easy creation of json replies.
|
||||
*/
|
||||
class JSONController
|
||||
{
|
||||
protected $payload;
|
||||
|
||||
public function showContent()
|
||||
{
|
||||
header('Content-Type: text/json; charset=utf-8');
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($this->payload);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* ManageAlbums.php
|
||||
* Contains the controller for admin album management.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ManageAlbums extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
$options = [
|
||||
'form' => [
|
||||
'action' => BASEURL . '/editalbum/',
|
||||
'method' => 'get',
|
||||
'class' => 'floatright',
|
||||
'buttons' => [
|
||||
'add' => [
|
||||
'type' => 'submit',
|
||||
'caption' => 'Add new album',
|
||||
],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'id_album' => [
|
||||
'value' => 'id_tag',
|
||||
'header' => 'ID',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'tag' => [
|
||||
'header' => 'Album',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/editalbum/?id={ID_TAG}',
|
||||
'data' => 'tag',
|
||||
],
|
||||
],
|
||||
'slug' => [
|
||||
'header' => 'Slug',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/editalbum/?id={ID_TAG}',
|
||||
'data' => 'slug',
|
||||
],
|
||||
],
|
||||
'count' => [
|
||||
'header' => '# Photos',
|
||||
'is_sortable' => true,
|
||||
'value' => 'count',
|
||||
],
|
||||
],
|
||||
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : null,
|
||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
|
||||
'title' => 'Manage albums',
|
||||
'no_items_label' => 'No albums meet the requirements of the current filter.',
|
||||
'items_per_page' => 9999,
|
||||
'index_class' => 'floatleft',
|
||||
'base_url' => BASEURL . '/managealbums/',
|
||||
'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') {
|
||||
if (!in_array($order, ['id_tag', 'tag', 'slug', 'count']))
|
||||
$order = 'tag';
|
||||
if (!in_array($direction, ['up', 'down']))
|
||||
$direction = 'up';
|
||||
|
||||
$db = Registry::get('db');
|
||||
$res = $db->query('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE kind = {string:album}
|
||||
ORDER BY id_parent, {raw:order}',
|
||||
[
|
||||
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||
'album' => 'Album',
|
||||
]);
|
||||
|
||||
$albums_by_parent = [];
|
||||
while ($row = $db->fetch_assoc($res))
|
||||
{
|
||||
if (!isset($albums_by_parent[$row['id_parent']]))
|
||||
$albums_by_parent[$row['id_parent']] = [];
|
||||
|
||||
$albums_by_parent[$row['id_parent']][] = $row + ['children' => []];
|
||||
}
|
||||
|
||||
$albums = self::getChildrenRecursively(0, 0, $albums_by_parent);
|
||||
$rows = self::flattenChildrenRecursively($albums);
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
'order' => $order,
|
||||
'direction' => ($direction == 'up' ? 'up' : 'down'),
|
||||
];
|
||||
},
|
||||
'get_count' => function() {
|
||||
return 9999;
|
||||
}
|
||||
];
|
||||
|
||||
$table = new GenericTable($options);
|
||||
parent::__construct('Album management - Page ' . $table->getCurrentPage() .' - ' . SITE_TITLE);
|
||||
$this->page->adopt(new TabularData($table));
|
||||
}
|
||||
|
||||
private static function getChildrenRecursively($id_parent, $level, &$albums_by_parent)
|
||||
{
|
||||
$children = [];
|
||||
if (!isset($albums_by_parent[$id_parent]))
|
||||
return $children;
|
||||
|
||||
foreach ($albums_by_parent[$id_parent] as $child)
|
||||
{
|
||||
if (isset($albums_by_parent[$child['id_tag']]))
|
||||
$child['children'] = self::getChildrenRecursively($child['id_tag'], $level + 1, $albums_by_parent);
|
||||
|
||||
$child['tag'] = ($level ? str_repeat('—', $level * 2) . ' ' : '') . $child['tag'];
|
||||
$children[] = $child;
|
||||
}
|
||||
|
||||
return $children;
|
||||
}
|
||||
|
||||
private static function flattenChildrenRecursively($albums)
|
||||
{
|
||||
if (empty($albums))
|
||||
return [];
|
||||
|
||||
$rows = [];
|
||||
foreach ($albums as $album)
|
||||
{
|
||||
$rows[] = array_intersect_key($album, array_flip(['id_tag', 'tag', 'slug', 'count']));
|
||||
if (!empty($album['children']))
|
||||
{
|
||||
$children = self::flattenChildrenRecursively($album['children']);
|
||||
foreach ($children as $child)
|
||||
$rows[] = array_intersect_key($child, array_flip(['id_tag', 'tag', 'slug', 'count']));
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* ManageAssets.php
|
||||
* Contains the asset management controller.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2017, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ManageAssets extends HTMLController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
// Ensure it's just admins at this point.
|
||||
if (!Registry::get('user')->isAdmin())
|
||||
throw new NotAllowedException();
|
||||
|
||||
Session::resetSessionToken();
|
||||
|
||||
$options = [
|
||||
'columns' => [
|
||||
'id_asset' => [
|
||||
'value' => 'id_asset',
|
||||
'header' => 'ID',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'subdir' => [
|
||||
'value' => 'subdir',
|
||||
'header' => 'Subdirectory',
|
||||
'is_sortable' => true,
|
||||
],
|
||||
'filename' => [
|
||||
'value' => 'filename',
|
||||
'header' => 'Filename',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'type' => 'value',
|
||||
'link' => BASEURL . '/editasset/?id={ID_ASSET}',
|
||||
'data' => 'filename',
|
||||
],
|
||||
],
|
||||
'title' => [
|
||||
'header' => 'Title',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'type' => 'value',
|
||||
'link' => BASEURL . '/editasset/?id={ID_ASSET}',
|
||||
'data' => 'title',
|
||||
],
|
||||
],
|
||||
'dimensions' => [
|
||||
'header' => 'Dimensions',
|
||||
'is_sortable' => false,
|
||||
'parse' => [
|
||||
'type' => 'function',
|
||||
'data' => function($row) {
|
||||
if (!empty($row['image_width']))
|
||||
return $row['image_width'] . ' x ' . $row['image_height'];
|
||||
else
|
||||
return 'n/a';
|
||||
},
|
||||
],
|
||||
],
|
||||
],
|
||||
'start' => !empty($_GET['start']) ? (int) $_GET['start'] : 0,
|
||||
'sort_order' => !empty($_GET['order']) ? $_GET['order'] : '',
|
||||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
||||
'title' => 'Manage assets',
|
||||
'no_items_label' => 'No assets meet the requirements of the current filter.',
|
||||
'items_per_page' => 30,
|
||||
'index_class' => 'pull_left',
|
||||
'base_url' => BASEURL . '/manageassets/',
|
||||
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
|
||||
if (!in_array($order, ['id_asset', 'title', 'subdir', 'filename']))
|
||||
$order = 'id_asset';
|
||||
|
||||
$data = Registry::get('db')->queryAssocs('
|
||||
SELECT id_asset, title, subdir, filename, image_width, image_height
|
||||
FROM assets
|
||||
ORDER BY {raw:order}
|
||||
LIMIT {int:offset}, {int:limit}',
|
||||
[
|
||||
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
]);
|
||||
|
||||
return [
|
||||
'rows' => $data,
|
||||
'order' => $order,
|
||||
'direction' => $direction,
|
||||
];
|
||||
},
|
||||
'get_count' => 'Asset::getCount',
|
||||
];
|
||||
|
||||
$table = new GenericTable($options);
|
||||
parent::__construct('Asset management - Page ' . $table->getCurrentPage() .'');
|
||||
$this->page->adopt(new TabularData($table));
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ class ManageErrors extends HTMLController
|
|||
{
|
||||
ErrorLog::flush();
|
||||
header('Location: ' . BASEURL . '/manageerrors/');
|
||||
exit;
|
||||
}
|
||||
|
||||
Session::resetSessionToken();
|
||||
|
@ -46,9 +47,13 @@ class ManageErrors extends HTMLController
|
|||
'parse' => [
|
||||
'type' => 'function',
|
||||
'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>' .
|
||||
'<pre style="display: none">' . $row['debug_info'] . '</pre></div>' .
|
||||
'<small><a href="' . BASEURL . $row['request_uri'] . '">' . $row['request_uri'] . '</a></small>';
|
||||
return $row['message'] . '<br>' .
|
||||
'<div><a onclick="this.parentNode.childNodes[1].style.display=\'block\';this.style.display=\'none\';">Show debug info</a>' .
|
||||
'<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',
|
||||
|
@ -84,7 +89,7 @@ class ManageErrors extends HTMLController
|
|||
'header' => 'UID',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/member/?id={ID_USER}',
|
||||
'link' => BASEURL . '/edituser/?id={ID_USER}',
|
||||
'data' => 'id_user',
|
||||
],
|
||||
],
|
||||
|
|
|
@ -15,8 +15,19 @@ class ManageTags extends HTMLController
|
|||
throw new NotAllowedException();
|
||||
|
||||
$options = [
|
||||
'form' => [
|
||||
'action' => BASEURL . '/edittag/',
|
||||
'method' => 'get',
|
||||
'class' => 'floatright',
|
||||
'buttons' => [
|
||||
'add' => [
|
||||
'type' => 'submit',
|
||||
'caption' => 'Add new tag',
|
||||
],
|
||||
],
|
||||
],
|
||||
'columns' => [
|
||||
'id_post' => [
|
||||
'id_tag' => [
|
||||
'value' => 'id_tag',
|
||||
'header' => 'ID',
|
||||
'is_sortable' => true,
|
||||
|
@ -25,7 +36,7 @@ class ManageTags extends HTMLController
|
|||
'header' => 'Tag',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/managetag/?id={ID_TAG}',
|
||||
'link' => BASEURL . '/edittag/?id={ID_TAG}',
|
||||
'data' => 'tag',
|
||||
],
|
||||
],
|
||||
|
@ -33,7 +44,7 @@ class ManageTags extends HTMLController
|
|||
'header' => 'Slug',
|
||||
'is_sortable' => true,
|
||||
'parse' => [
|
||||
'link' => BASEURL . '/managetag/?id={ID_TAG}',
|
||||
'link' => BASEURL . '/edittag/?id={ID_TAG}',
|
||||
'data' => 'slug',
|
||||
],
|
||||
],
|
||||
|
@ -53,10 +64,11 @@ class ManageTags extends HTMLController
|
|||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : null,
|
||||
'title' => 'Manage tags',
|
||||
'no_items_label' => 'No tags meet the requirements of the current filter.',
|
||||
'items_per_page' => 25,
|
||||
'items_per_page' => 30,
|
||||
'index_class' => 'floatleft',
|
||||
'base_url' => BASEURL . '/managetags/',
|
||||
'get_data' => function($offset = 0, $limit = 15, $order = '', $direction = 'up') {
|
||||
if (!in_array($order, ['id_post', 'tag', 'slug', 'kind', 'count']))
|
||||
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') {
|
||||
if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count']))
|
||||
$order = 'tag';
|
||||
if (!in_array($direction, ['up', 'down']))
|
||||
$direction = 'up';
|
||||
|
@ -64,12 +76,14 @@ class ManageTags extends HTMLController
|
|||
$data = Registry::get('db')->queryAssocs('
|
||||
SELECT *
|
||||
FROM tags
|
||||
WHERE kind != {string:album}
|
||||
ORDER BY {raw:order}
|
||||
LIMIT {int:offset}, {int:limit}',
|
||||
[
|
||||
'order' => $order . ($direction == 'up' ? ' ASC' : ' DESC'),
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'album' => 'Album',
|
||||
]);
|
||||
|
||||
return [
|
||||
|
@ -81,7 +95,9 @@ class ManageTags extends HTMLController
|
|||
'get_count' => function() {
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM tags');
|
||||
FROM tags
|
||||
WHERE kind != {string:album}',
|
||||
['album' => 'Album']);
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -93,10 +93,10 @@ class ManageUsers extends HTMLController
|
|||
'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '',
|
||||
'title' => 'Manage users',
|
||||
'no_items_label' => 'No users meet the requirements of the current filter.',
|
||||
'items_per_page' => 15,
|
||||
'items_per_page' => 30,
|
||||
'index_class' => 'floatleft',
|
||||
'base_url' => BASEURL . '/manageusers/',
|
||||
'get_data' => function($offset = 0, $limit = 15, $order = '', $direction = 'down') {
|
||||
'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') {
|
||||
if (!in_array($order, ['id_user', 'surname', 'first_name', 'slug', 'emailaddress', 'last_action_time', 'ip_address', 'is_admin']))
|
||||
$order = 'id_user';
|
||||
|
||||
|
|
|
@ -41,8 +41,9 @@ class ProvideAutoSuggest extends JSONController
|
|||
$results = Tag::matchPeople($data);
|
||||
foreach ($results as $id_tag => $tag)
|
||||
$this->payload['items'][] = [
|
||||
'label' => $tag,
|
||||
'label' => $tag['tag'],
|
||||
'id_tag' => $id_tag,
|
||||
'url' => BASEURL . '/' . $tag['slug'] . '/',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +53,7 @@ class ProvideAutoSuggest extends JSONController
|
|||
// It better not already exist!
|
||||
if (Tag::exactMatch($_REQUEST['tag']))
|
||||
{
|
||||
$this->payload = ['error' => true, 'msg' => "Tag already exists!"];
|
||||
$this->payload = ['error' => true, 'msg' => 'Tag already exists!'];
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -67,7 +68,7 @@ class ProvideAutoSuggest extends JSONController
|
|||
// Did we succeed?
|
||||
if (!$tag)
|
||||
{
|
||||
$this->payload = ['error' => true, 'msg' => "Could not create tag."];
|
||||
$this->payload = ['error' => true, 'msg' => 'Could not create tag.'];
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,10 +33,14 @@ class UploadMedia extends HTMLController
|
|||
if (empty($uploaded_file))
|
||||
continue;
|
||||
|
||||
// DIY slug club.
|
||||
$slug = $tag->slug . '/' . strtr($uploaded_file['name'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '\\' => '-']);
|
||||
|
||||
$asset = Asset::createNew([
|
||||
'filename_to_copy' => $uploaded_file['tmp_name'],
|
||||
'preferred_filename' => $uploaded_file['name'],
|
||||
'preferred_subdir' => $tag->slug,
|
||||
'slug' => $slug,
|
||||
]);
|
||||
|
||||
$new_ids[] = $asset->getId();
|
||||
|
|
|
@ -11,17 +11,53 @@ class ViewPhoto extends HTMLController
|
|||
public function __construct()
|
||||
{
|
||||
// Ensure we're logged in at this point.
|
||||
if (!Registry::get('user')->isLoggedIn())
|
||||
$user = Registry::get('user');
|
||||
if (!$user->isLoggedIn())
|
||||
throw new NotAllowedException();
|
||||
|
||||
$photo = Asset::fromSlug($_GET['slug']);
|
||||
if (empty($photo))
|
||||
throw new NotFoundException();
|
||||
|
||||
parent::__construct($photo->getTitle() . ' - ' . SITE_TITLE);
|
||||
|
||||
$author = $photo->getAuthor();
|
||||
|
||||
if (isset($_REQUEST['confirm_delete']) || isset($_REQUEST['delete_confirmed']))
|
||||
$this->handleConfirmDelete($user, $author, $photo);
|
||||
else
|
||||
$this->handleViewPhoto($user, $author, $photo);
|
||||
|
||||
// Add an edit button to the admin bar.
|
||||
if ($user->isAdmin())
|
||||
$this->admin_bar->appendItem(BASEURL . '/editasset/?id=' . $photo->getId(), 'Edit this photo');
|
||||
}
|
||||
|
||||
private function handleConfirmDelete(User $user, User $author, Asset $photo)
|
||||
{
|
||||
if (!($user->isAdmin() || $user->getUserId() === $author->getUserId()))
|
||||
throw new NotAllowedException();
|
||||
|
||||
if (isset($_REQUEST['confirm_delete']))
|
||||
{
|
||||
$page = new ConfirmDeletePage($photo->getImage());
|
||||
$this->page->adopt($page);
|
||||
}
|
||||
else if (isset($_REQUEST['delete_confirmed']))
|
||||
{
|
||||
$album_url = $photo->getSubdir();
|
||||
$photo->delete();
|
||||
|
||||
header('Location: ' . BASEURL . '/' . $album_url);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
private function handleViewPhoto(User $user, User $author, Asset $photo)
|
||||
{
|
||||
if (!empty($_POST))
|
||||
$this->handleTagging($photo->getImage());
|
||||
|
||||
parent::__construct($photo->getTitle() . ' - ' . SITE_TITLE);
|
||||
$page = new PhotoPage($photo->getImage());
|
||||
|
||||
// Exif data?
|
||||
|
@ -43,12 +79,11 @@ class ViewPhoto extends HTMLController
|
|||
if ($next_url)
|
||||
$page->setNextPhotoUrl($next_url);
|
||||
|
||||
if ($user->isAdmin() || $user->getUserId() === $author->getUserId())
|
||||
$page->setIsAssetOwner(true);
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
private function handleTagging(Image $photo)
|
||||
|
@ -63,8 +98,19 @@ class ViewPhoto extends HTMLController
|
|||
}
|
||||
|
||||
// We are!
|
||||
$photo->linkTags([(int) $_POST['id_tag']]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
if (!isset($_POST['delete']))
|
||||
{
|
||||
$photo->linkTags([(int) $_POST['id_tag']]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ... deleting, that is.
|
||||
else
|
||||
{
|
||||
$photo->unlinkTags([(int) $_POST['id_tag']]);
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,11 +61,19 @@ class ViewPhotoAlbum extends HTMLController
|
|||
// Can we do fancy things here?
|
||||
// !!! TODO: permission system?
|
||||
$buttons = [];
|
||||
|
||||
if (Registry::get('user')->isLoggedIn())
|
||||
{
|
||||
$buttons[] = [
|
||||
'url' => BASEURL . '/download/?tag=' . $id_tag,
|
||||
'caption' => 'Download this album',
|
||||
];
|
||||
|
||||
$buttons[] = [
|
||||
'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag,
|
||||
'caption' => 'Upload new photos here',
|
||||
];
|
||||
}
|
||||
if (Registry::get('user')->isAdmin())
|
||||
$buttons[] = [
|
||||
'url' => BASEURL . '/addalbum/?tag=' . $id_tag,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* migrate_thumbs.php
|
||||
* Migrates old-style thumbnails (meta) to new table.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
// Include the project's configuration.
|
||||
require_once 'config.php';
|
||||
|
||||
// Set up the autoloader.
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
// Initialise the database.
|
||||
$db = new Database(DB_SERVER, DB_USER, DB_PASS, DB_NAME);
|
||||
Registry::set('db', $db);
|
||||
|
||||
// Do some authentication checks.
|
||||
Session::start();
|
||||
Registry::set('user', Authentication::isLoggedIn() ? Member::fromId($_SESSION['user_id']) : new Guest());
|
||||
|
||||
|
||||
$res = $db->query('
|
||||
SELECT id_asset, variable, value
|
||||
FROM assets_meta
|
||||
WHERE variable LIKE {string:thumbs}',
|
||||
['thumbs' => 'thumb_%']);
|
||||
|
||||
while ($row = $db->fetch_assoc($res))
|
||||
{
|
||||
if (!preg_match('~^thumb_(?<width>\d+)x(?<height>\d+)(?:_(?<mode>c[best]?))?$~', $row['variable'], $match))
|
||||
continue;
|
||||
|
||||
echo 'Migrating ... ', $row['value'], '(#', $row['id_asset'], ")\r";
|
||||
|
||||
$db->insert('replace', 'assets_thumbs', [
|
||||
'id_asset' => 'int',
|
||||
'width' => 'int',
|
||||
'height' => 'int',
|
||||
'mode' => 'string-3',
|
||||
'filename' => 'string-255',
|
||||
], [
|
||||
'id_asset' => $row['id_asset'],
|
||||
'width' => $match['width'],
|
||||
'height' => $match['height'],
|
||||
'mode' => $match['mode'] ?? '',
|
||||
'filename' => $row['value'],
|
||||
]);
|
||||
}
|
||||
|
||||
echo "\nDone\n";
|
||||
|
118
models/Asset.php
118
models/Asset.php
|
@ -18,8 +18,10 @@ class Asset
|
|||
protected $image_height;
|
||||
protected $date_captured;
|
||||
protected $priority;
|
||||
|
||||
protected $meta;
|
||||
protected $tags;
|
||||
protected $thumbnails;
|
||||
|
||||
protected function __construct(array $data)
|
||||
{
|
||||
|
@ -58,8 +60,10 @@ class Asset
|
|||
|
||||
public static function byRow(array $row, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
// Supplement with metadata.
|
||||
$row['meta'] = Registry::get('db')->queryPair('
|
||||
$row['meta'] = $db->queryPair('
|
||||
SELECT variable, value
|
||||
FROM assets_meta
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
|
@ -67,6 +71,24 @@ class Asset
|
|||
'id_asset' => $row['id_asset'],
|
||||
]);
|
||||
|
||||
// And thumbnails.
|
||||
$row['thumbnails'] = $db->queryPair('
|
||||
SELECT
|
||||
CONCAT(
|
||||
width,
|
||||
{string:x},
|
||||
height,
|
||||
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||
) AS selector, filename
|
||||
FROM assets_thumbs
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $row['id_asset'],
|
||||
'empty' => '',
|
||||
'x' => 'x',
|
||||
'_' => '_',
|
||||
]);
|
||||
|
||||
return $return_format == 'object' ? new Asset($row) : $row;
|
||||
}
|
||||
|
||||
|
@ -91,6 +113,7 @@ class Asset
|
|||
{
|
||||
$assets[$asset['id_asset']] = $asset;
|
||||
$assets[$asset['id_asset']]['meta'] = [];
|
||||
$assets[$asset['id_asset']]['thumbnails'] = [];
|
||||
}
|
||||
|
||||
$metas = $db->queryRows('
|
||||
|
@ -105,6 +128,27 @@ class Asset
|
|||
foreach ($metas as $meta)
|
||||
$assets[$meta[0]]['meta'][$meta[1]] = $meta[2];
|
||||
|
||||
$thumbnails = $db->queryRows('
|
||||
SELECT id_asset,
|
||||
CONCAT(
|
||||
width,
|
||||
{string:x},
|
||||
height,
|
||||
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||
) AS selector, filename
|
||||
FROM assets_thumbs
|
||||
WHERE id_asset IN ({array_int:id_assets})
|
||||
ORDER BY id_asset',
|
||||
[
|
||||
'id_assets' => $id_assets,
|
||||
'empty' => '',
|
||||
'x' => 'x',
|
||||
'_' => '_',
|
||||
]);
|
||||
|
||||
foreach ($thumbnails as $thumb)
|
||||
$assets[$thumb[0]]['thumbnails'][$thumb[1]] = $thumb[2];
|
||||
|
||||
if ($return_format == 'array')
|
||||
return $assets;
|
||||
else
|
||||
|
@ -116,13 +160,6 @@ class Asset
|
|||
}
|
||||
}
|
||||
|
||||
public static function byPostId($id_post, $return_format = 'object')
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
// !!! TODO
|
||||
}
|
||||
|
||||
public static function createNew(array $data, $return_format = 'object')
|
||||
{
|
||||
// Extract the data array.
|
||||
|
@ -277,7 +314,12 @@ class Asset
|
|||
return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
|
||||
}
|
||||
|
||||
public function getPath()
|
||||
public function getSlug()
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function getSubdir()
|
||||
{
|
||||
return $this->subdir;
|
||||
}
|
||||
|
@ -438,12 +480,60 @@ class Asset
|
|||
|
||||
public function delete()
|
||||
{
|
||||
return Registry::get('db')->query('
|
||||
$db = Registry::get('db');
|
||||
|
||||
if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename))
|
||||
return false;
|
||||
|
||||
$db->query('
|
||||
DELETE FROM assets_meta
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
]);
|
||||
|
||||
$recount_tags = $db->queryValues('
|
||||
SELECT id_tag
|
||||
FROM assets_tags
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
]);
|
||||
|
||||
$db->query('
|
||||
DELETE FROM assets_tags
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
]);
|
||||
|
||||
Tag::recount($recount_tags);
|
||||
|
||||
$return = $db->query('
|
||||
DELETE FROM assets
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
]);
|
||||
|
||||
$rows = $db->query('
|
||||
SELECT id_tag
|
||||
FROM tags
|
||||
WHERE id_asset_thumb = {int:id_asset}',
|
||||
[
|
||||
'id_asset' => $this->id_asset,
|
||||
]);
|
||||
|
||||
if (!empty($rows))
|
||||
{
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$tag = Tag::fromId($row['id_tag']);
|
||||
$tag->resetIdAsset();
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
public function linkTags(array $id_tags)
|
||||
|
@ -481,16 +571,17 @@ class Asset
|
|||
|
||||
public static function getCount()
|
||||
{
|
||||
return $db->queryValue('
|
||||
return Registry::get('db')->queryValue('
|
||||
SELECT COUNT(*)
|
||||
FROM assets');
|
||||
}
|
||||
|
||||
public function setKeyData($title, DateTime $date_captured = null, $priority)
|
||||
public function setKeyData($title, $slug, DateTime $date_captured = null, $priority)
|
||||
{
|
||||
$params = [
|
||||
'id_asset' => $this->id_asset,
|
||||
'title' => $title,
|
||||
'slug' => $slug,
|
||||
'priority' => $priority,
|
||||
];
|
||||
|
||||
|
@ -499,7 +590,8 @@ class Asset
|
|||
|
||||
return Registry::get('db')->query('
|
||||
UPDATE assets
|
||||
SET title = {string:title},' . (isset($date_captured) ? '
|
||||
SET title = {string:title},
|
||||
slug = {string:slug},' . (isset($date_captured) ? '
|
||||
date_captured = {datetime:date_captured},' : '') . '
|
||||
priority = {int:priority}
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
|
|
|
@ -11,12 +11,14 @@ class AssetIterator extends Asset
|
|||
private $return_format;
|
||||
private $res_assets;
|
||||
private $res_meta;
|
||||
private $res_thumbs;
|
||||
|
||||
protected function __construct($res_assets, $res_meta, $return_format)
|
||||
protected function __construct($res_assets, $res_meta, $res_thumbs, $return_format)
|
||||
{
|
||||
$this->db = Registry::get('db');
|
||||
$this->res_assets = $res_assets;
|
||||
$this->res_meta = $res_meta;
|
||||
$this->res_thumbs = $res_thumbs;
|
||||
$this->return_format = $return_format;
|
||||
}
|
||||
|
||||
|
@ -41,6 +43,19 @@ class AssetIterator extends Asset
|
|||
// Reset internal pointer for next asset.
|
||||
$this->db->data_seek($this->res_meta, 0);
|
||||
|
||||
// Looks up thumbnails.
|
||||
$row['thumbnails'] = [];
|
||||
while ($thumbs = $this->db->fetch_assoc($this->res_thumbs))
|
||||
{
|
||||
if ($thumbs['id_asset'] != $row['id_asset'])
|
||||
continue;
|
||||
|
||||
$row['thumbnails'][$thumbs['selector']] = $thumbs['filename'];
|
||||
}
|
||||
|
||||
// Reset internal pointer for next asset.
|
||||
$this->db->data_seek($this->res_thumbs, 0);
|
||||
|
||||
if ($this->return_format == 'object')
|
||||
return new Asset($row);
|
||||
else
|
||||
|
@ -51,6 +66,7 @@ class AssetIterator extends Asset
|
|||
{
|
||||
$this->db->data_seek($this->res_assets, 0);
|
||||
$this->db->data_seek($this->res_meta, 0);
|
||||
$this->db->data_seek($this->res_thumbs, 0);
|
||||
}
|
||||
|
||||
public function clean()
|
||||
|
@ -135,7 +151,29 @@ class AssetIterator extends Asset
|
|||
ORDER BY id_asset',
|
||||
$params);
|
||||
|
||||
$iterator = new self($res_assets, $res_meta, $return_format);
|
||||
// Get a resource object for the asset thumbs.
|
||||
$res_thumbs = $db->query('
|
||||
SELECT id_asset, filename,
|
||||
CONCAT(
|
||||
width,
|
||||
{string:x},
|
||||
height,
|
||||
IF(mode != {string:empty}, CONCAT({string:_}, mode), {string:empty})
|
||||
) AS selector
|
||||
FROM assets_thumbs
|
||||
WHERE id_asset IN(
|
||||
SELECT id_asset
|
||||
FROM assets AS a
|
||||
WHERE ' . $where . '
|
||||
)
|
||||
ORDER BY id_asset',
|
||||
$params + [
|
||||
'empty' => '',
|
||||
'x' => 'x',
|
||||
'_' => '_',
|
||||
]);
|
||||
|
||||
$iterator = new self($res_assets, $res_meta, $res_thumbs, $return_format);
|
||||
|
||||
// Returning total count, too?
|
||||
if ($return_count)
|
||||
|
|
|
@ -1,149 +0,0 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* BestColor.php
|
||||
* Contains key class BestColor.
|
||||
*
|
||||
* !!! Licensing?
|
||||
*****************************************************************************/
|
||||
|
||||
class BestColor
|
||||
{
|
||||
private $best;
|
||||
|
||||
public function __construct(Image $asset)
|
||||
{
|
||||
// Set fallback color.
|
||||
$this->best = ['r' => 204, 'g' => 204, 'b' => 204]; // #cccccc
|
||||
|
||||
// We will be needing to read this...
|
||||
if (!file_exists($asset->getPath()))
|
||||
return;
|
||||
|
||||
// Try the arcane stuff again.
|
||||
try
|
||||
{
|
||||
$image = new Imagick($asset->getPath());
|
||||
$width = $image->getImageWidth();
|
||||
$height = $image->getImageHeight();
|
||||
|
||||
// Sample six points in the image: four based on the rule of thirds, as well as the horizontal and vertical centre.
|
||||
$topy = round($height / 3);
|
||||
$bottomy = round(($height / 3) * 2);
|
||||
$leftx = round($width / 3);
|
||||
$rightx = round(($width / 3) * 2);
|
||||
$centery = round($height / 2);
|
||||
$centerx = round($width / 2);
|
||||
|
||||
// Grab their colours.
|
||||
$rgb = [
|
||||
$image->getImagePixelColor($leftx, $topy)->getColor(),
|
||||
$image->getImagePixelColor($rightx, $topy)->getColor(),
|
||||
$image->getImagePixelColor($leftx, $bottomy)->getColor(),
|
||||
$image->getImagePixelColor($rightx, $bottomy)->getColor(),
|
||||
$image->getImagePixelColor($centerx, $centery)->getColor(),
|
||||
];
|
||||
|
||||
// We won't be needing this anymore, so save us some memory.
|
||||
$image->clear();
|
||||
$image->destroy();
|
||||
}
|
||||
// In case something does go wrong...
|
||||
catch (ImagickException $e)
|
||||
{
|
||||
// Fall back to default color.
|
||||
return;
|
||||
}
|
||||
|
||||
// Process rgb values into hsv values
|
||||
foreach ($rgb as $i => $color)
|
||||
{
|
||||
$colors[$i] = $color;
|
||||
list($colors[$i]['h'], $colors[$i]['s'], $colors[$i]['v']) = self::rgb2hsv($color['r'], $color['g'], $color['b']);
|
||||
}
|
||||
|
||||
// Figure out which color is the best saturated.
|
||||
$best_saturation = $best_brightness = 0;
|
||||
$the_best_s = $the_best_v = ['v' => 0];
|
||||
foreach ($colors as $color)
|
||||
{
|
||||
if ($color['s'] > $best_saturation)
|
||||
{
|
||||
$best_saturation = $color['s'];
|
||||
$the_best_s = $color;
|
||||
}
|
||||
if ($color['v'] > $best_brightness)
|
||||
{
|
||||
$best_brightness = $color['v'];
|
||||
$the_best_v = $color;
|
||||
}
|
||||
}
|
||||
|
||||
// Is brightest the same as most saturated?
|
||||
$this->best = ($the_best_s['v'] >= ($the_best_v['v'] - ($the_best_v['v'] / 2))) ? $the_best_s : $the_best_v;
|
||||
}
|
||||
|
||||
public static function hex2rgb($hex)
|
||||
{
|
||||
return sscanf($hex, '%2X%2X%2X');
|
||||
}
|
||||
|
||||
public static function rgb2hex($red, $green, $blue)
|
||||
{
|
||||
return sprintf('%02X%02X%02X', $red, $green, $blue);
|
||||
}
|
||||
|
||||
public static function rgb2hsv($r, $g, $b)
|
||||
{
|
||||
$max = max($r, $g, $b);
|
||||
$min = min($r, $g, $b);
|
||||
$delta = $max - $min;
|
||||
$v = round(($max / 255) * 100);
|
||||
$s = ($max != 0) ? (round($delta / $max * 100)) : 0;
|
||||
if ($s == 0)
|
||||
{
|
||||
$h = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if ($r == $max)
|
||||
$h = ($g - $b) / $delta;
|
||||
elseif ($g == $max)
|
||||
$h = 2 + ($b - $r) / $delta;
|
||||
elseif ($b == $max)
|
||||
$h = 4 + ($r - $g) / $delta;
|
||||
|
||||
$h = round($h * 60);
|
||||
|
||||
if ($h > 360)
|
||||
$h = 360;
|
||||
|
||||
if ($h < 0)
|
||||
$h += 360;
|
||||
}
|
||||
|
||||
return [$h, $s, $v];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a normal (light) background color as hexadecimal value (without hash prefix).
|
||||
* @return color string
|
||||
*/
|
||||
public function hex()
|
||||
{
|
||||
$c = $this->best;
|
||||
return self::rgb2hex($c['r'], $c['g'], $c['b']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a 50% darker version of the best color as string.
|
||||
* @param factor, defaults to 0.5
|
||||
* @param alpha, defaults to 0.7
|
||||
* @return rgba(r * factor, g * factor, b * factor, alpha)
|
||||
*/
|
||||
public function rgba($factor = 0.5, $alpha = 0.7)
|
||||
{
|
||||
$c = $this->best;
|
||||
return 'rgba(' . round($c['r'] * $factor) . ', ' . round($c['g'] * $factor) . ', ' . round($c['b'] * $factor) . ', ' . $alpha . ')';
|
||||
}
|
||||
|
||||
}
|
|
@ -37,7 +37,7 @@ class Database
|
|||
exit;
|
||||
}
|
||||
|
||||
$this->query('SET NAMES {string:utf8}', array('utf8' => 'utf8'));
|
||||
$this->query('SET NAMES {string:utf8}', ['utf8' => 'utf8']);
|
||||
}
|
||||
|
||||
public function getQueryCount()
|
||||
|
@ -256,7 +256,7 @@ class Database
|
|||
|
||||
case 'identifier':
|
||||
// Backticks inside identifiers are supported as of MySQL 4.1. We don't need them here.
|
||||
return '`' . strtr($replacement, array('`' => '', '.' => '')) . '`';
|
||||
return '`' . strtr($replacement, ['`' => '', '.' => '']) . '`';
|
||||
break;
|
||||
|
||||
case 'raw':
|
||||
|
@ -278,7 +278,7 @@ class Database
|
|||
/**
|
||||
* Escapes and quotes a string using values passed, and executes the query.
|
||||
*/
|
||||
public function query($db_string, $db_values = array())
|
||||
public function query($db_string, $db_values = [])
|
||||
{
|
||||
// One more query....
|
||||
$this->query_count ++;
|
||||
|
@ -293,10 +293,10 @@ class Database
|
|||
if (!$security_override && !empty($db_values))
|
||||
{
|
||||
// Set some values for use in the callback function.
|
||||
$this->db_callback = array($db_values, $this->connection);
|
||||
$this->db_callback = [$db_values, $this->connection];
|
||||
|
||||
// Insert the values passed to this function.
|
||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array(&$this, 'replacement_callback'), $db_string);
|
||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', [&$this, 'replacement_callback'], $db_string);
|
||||
|
||||
// Save some memory.
|
||||
$this->db_callback = [];
|
||||
|
@ -320,20 +320,20 @@ class Database
|
|||
* Escapes and quotes a string just like db_query, but does not execute the query.
|
||||
* Useful for debugging purposes.
|
||||
*/
|
||||
public function quote($db_string, $db_values = array())
|
||||
public function quote($db_string, $db_values = [])
|
||||
{
|
||||
// Please, just use new style queries.
|
||||
if (strpos($db_string, '\'') !== false)
|
||||
trigger_error('Hack attempt!', 'Illegal character (\') used in query.', E_USER_ERROR);
|
||||
|
||||
// Save some values for use in the callback function.
|
||||
$this->db_callback = array($db_values, $this->connection);
|
||||
$this->db_callback = [$db_values, $this->connection];
|
||||
|
||||
// Insert the values passed to this function.
|
||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', array(&$this, 'replacement_callback'), $db_string);
|
||||
$db_string = preg_replace_callback('~{([a-z_]+)(?::([a-zA-Z0-9_-]+))?}~', [&$this, 'replacement_callback'], $db_string);
|
||||
|
||||
// Save some memory.
|
||||
$this->db_callback = array();
|
||||
$this->db_callback = [];
|
||||
|
||||
return $db_string;
|
||||
}
|
||||
|
@ -341,12 +341,12 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryRow($db_string, $db_values = array())
|
||||
public function queryRow($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$row = $this->fetch_row($res);
|
||||
$this->free_result($res);
|
||||
|
@ -357,14 +357,14 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryRows($db_string, $db_values = array())
|
||||
public function queryRows($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$rows = array();
|
||||
$rows = [];
|
||||
while ($row = $this->fetch_row($res))
|
||||
$rows[] = $row;
|
||||
|
||||
|
@ -376,14 +376,14 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryPair($db_string, $db_values = array())
|
||||
public function queryPair($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$rows = array();
|
||||
$rows = [];
|
||||
while ($row = $this->fetch_row($res))
|
||||
$rows[$row[0]] = $row[1];
|
||||
|
||||
|
@ -395,14 +395,14 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an array of all the rows it returns.
|
||||
*/
|
||||
public function queryPairs($db_string, $db_values = array())
|
||||
public function queryPairs($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$rows = array();
|
||||
$rows = [];
|
||||
while ($row = $this->fetch_assoc($res))
|
||||
{
|
||||
$key_value = reset($row);
|
||||
|
@ -417,12 +417,12 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an associative array of all the rows it returns.
|
||||
*/
|
||||
public function queryAssoc($db_string, $db_values = array())
|
||||
public function queryAssoc($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$row = $this->fetch_assoc($res);
|
||||
$this->free_result($res);
|
||||
|
@ -433,14 +433,14 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an associative array of all the rows it returns.
|
||||
*/
|
||||
public function queryAssocs($db_string, $db_values = array(), $connection = null)
|
||||
public function queryAssocs($db_string, $db_values = [], $connection = null)
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$rows = array();
|
||||
$rows = [];
|
||||
while ($row = $this->fetch_assoc($res))
|
||||
$rows[] = $row;
|
||||
|
||||
|
@ -452,7 +452,7 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning the first value of the first row.
|
||||
*/
|
||||
public function queryValue($db_string, $db_values = array())
|
||||
public function queryValue($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
|
@ -469,14 +469,14 @@ class Database
|
|||
/**
|
||||
* Executes a query, returning an array of the first value of each row.
|
||||
*/
|
||||
public function queryValues($db_string, $db_values = array())
|
||||
public function queryValues($db_string, $db_values = [])
|
||||
{
|
||||
$res = $this->query($db_string, $db_values);
|
||||
|
||||
if (!$res || $this->num_rows($res) == 0)
|
||||
return array();
|
||||
return [];
|
||||
|
||||
$rows = array();
|
||||
$rows = [];
|
||||
while ($row = $this->fetch_row($res))
|
||||
$rows[] = $row[0];
|
||||
|
||||
|
@ -496,7 +496,7 @@ class Database
|
|||
|
||||
// Inserting data as a single row can be done as a single array.
|
||||
if (!is_array($data[array_rand($data)]))
|
||||
$data = array($data);
|
||||
$data = [$data];
|
||||
|
||||
// Create the mold for a single row insert.
|
||||
$insertData = '(';
|
||||
|
@ -514,7 +514,7 @@ class Database
|
|||
$indexed_columns = array_keys($columns);
|
||||
|
||||
// Here's where the variables are injected to the query.
|
||||
$insertRows = array();
|
||||
$insertRows = [];
|
||||
foreach ($data as $dataRow)
|
||||
$insertRows[] = $this->quote($insertData, array_combine($indexed_columns, $dataRow));
|
||||
|
||||
|
@ -527,9 +527,6 @@ class Database
|
|||
VALUES
|
||||
' . implode(',
|
||||
', $insertRows),
|
||||
array(
|
||||
'security_override' => true,
|
||||
)
|
||||
);
|
||||
['security_override' => true]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,12 +11,16 @@ class Dispatcher
|
|||
public static function route()
|
||||
{
|
||||
$possibleActions = [
|
||||
'addalbum' => 'EditAlbum',
|
||||
'albums' => 'ViewPhotoAlbums',
|
||||
'editalbum' => 'EditAlbum',
|
||||
'editasset' => 'EditAsset',
|
||||
'edittag' => 'EditTag',
|
||||
'edituser' => 'EditUser',
|
||||
'login' => 'Login',
|
||||
'logout' => 'Logout',
|
||||
'managecomments' => 'ManageComments',
|
||||
'managealbums' => 'ManageAlbums',
|
||||
'manageassets' => 'ManageAssets',
|
||||
'manageerrors' => 'ManageErrors',
|
||||
'managetags' => 'ManageTags',
|
||||
'manageusers' => 'ManageUsers',
|
||||
|
@ -25,6 +29,7 @@ class Dispatcher
|
|||
'suggest' => 'ProvideAutoSuggest',
|
||||
'timeline' => 'ViewTimeline',
|
||||
'uploadmedia' => 'UploadMedia',
|
||||
'download' => 'Download',
|
||||
];
|
||||
|
||||
// Work around PHP's FPM not always providing PATH_INFO.
|
||||
|
@ -41,6 +46,12 @@ class Dispatcher
|
|||
{
|
||||
return new ViewPhotoAlbum();
|
||||
}
|
||||
// Asynchronously generating thumbnails?
|
||||
elseif (preg_match('~^/thumbnail/(?<id>\d+)/(?<width>\d+)x(?<height>\d+)(?:_(?<mode>c(t|b|s|)))?/?~', $_SERVER['PATH_INFO'], $path))
|
||||
{
|
||||
$_GET = array_merge($_GET, $path);
|
||||
return new GenerateThumbnail();
|
||||
}
|
||||
// Look for particular actions...
|
||||
elseif (preg_match('~^/(?<action>[a-z]+)(?:/page/(?<page>\d+))?/?~', $_SERVER['PATH_INFO'], $path) && isset($possibleActions[$path['action']]))
|
||||
{
|
||||
|
@ -103,10 +114,10 @@ class Dispatcher
|
|||
/**
|
||||
* Kicks a guest to a login form, redirecting them back to this page upon login.
|
||||
*/
|
||||
public static function kickGuest()
|
||||
public static function kickGuest($title = null, $message = null)
|
||||
{
|
||||
$form = new LogInForm('Log in');
|
||||
$form->adopt(new Alert('', 'You need to be logged in to view this page.', 'error'));
|
||||
$form->adopt(new Alert($title ?? '', $message ?? 'You need to be logged in to view this page.', 'error'));
|
||||
$form->setRedirectUrl($_SERVER['REQUEST_URI']);
|
||||
|
||||
$page = new MainTemplate('Login required');
|
||||
|
|
|
@ -96,7 +96,9 @@ class EXIF
|
|||
elseif (!empty($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']);
|
||||
|
||||
return new self($meta);
|
||||
|
|
|
@ -14,7 +14,7 @@ class Email
|
|||
$boundary = uniqid('sr');
|
||||
|
||||
if (empty($headers))
|
||||
$headers .= "From: HashRU Pics <no-reply@aaronweb.net>\r\n";
|
||||
$headers .= "From: " . SITE_TITLE . " <" . REPLY_TO_ADDRESS . ">\r\n";
|
||||
|
||||
// Set up headers.
|
||||
$headers .= "MIME-Version: 1.0\r\n";
|
||||
|
|
|
@ -61,7 +61,7 @@ class Form
|
|||
{
|
||||
$this->missing[] = $field_id;
|
||||
$this->data[$field_id] = '';
|
||||
continue;
|
||||
continue 2;
|
||||
}
|
||||
else
|
||||
$this->data[$field_id] = $post[$field_id];
|
||||
|
@ -78,7 +78,7 @@ class Form
|
|||
{
|
||||
$this->missing[] = $field_id;
|
||||
$this->data[$field_id] = '';
|
||||
continue;
|
||||
continue 2;
|
||||
}
|
||||
else
|
||||
$this->data[$field_id] = $post[$field_id];
|
||||
|
|
|
@ -6,19 +6,16 @@
|
|||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class GenericTable extends PageIndex
|
||||
class GenericTable
|
||||
{
|
||||
protected $header = [];
|
||||
protected $body = [];
|
||||
protected $page_index = [];
|
||||
private $header = [];
|
||||
private $body = [];
|
||||
private $pageIndex = null;
|
||||
private $currentPage = 1;
|
||||
|
||||
protected $title;
|
||||
protected $title_class;
|
||||
protected $tableIsSortable = false;
|
||||
protected $recordCount;
|
||||
protected $needsPageIndex = false;
|
||||
protected $current_page;
|
||||
protected $num_pages;
|
||||
private $title;
|
||||
private $title_class;
|
||||
private $tableIsSortable = false;
|
||||
|
||||
public $form_above;
|
||||
public $form_below;
|
||||
|
@ -30,40 +27,38 @@ class GenericTable extends PageIndex
|
|||
$options['sort_order'] = '';
|
||||
|
||||
// Order in which direction?
|
||||
if (!empty($options['sort_direction']) && !in_array($options['sort_direction'], array('up', 'down')))
|
||||
if (!empty($options['sort_direction']) && !in_array($options['sort_direction'], ['up', 'down']))
|
||||
$options['sort_direction'] = 'up';
|
||||
|
||||
// Make sure we know whether we can actually sort on something.
|
||||
$this->tableIsSortable = !empty($options['base_url']);
|
||||
|
||||
// How much stuff do we have?
|
||||
$this->recordCount = call_user_func_array($options['get_count'], !empty($options['get_count_params']) ? $options['get_count_params'] : array());
|
||||
// How much data do we have?
|
||||
$this->recordCount = $options['get_count'](...(!empty($options['get_count_params']) ? $options['get_count_params'] : []));
|
||||
|
||||
// Should we create a page index?
|
||||
// How much data do we need to retrieve?
|
||||
$this->items_per_page = !empty($options['items_per_page']) ? $options['items_per_page'] : 30;
|
||||
$this->needsPageIndex = !empty($this->items_per_page) && $this->recordCount > $this->items_per_page;
|
||||
$this->index_class = isset($options['index_class']) ? $options['index_class'] : '';
|
||||
|
||||
// Figure out where to 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.
|
||||
$numPages = ceil($this->recordCount / $this->items_per_page);
|
||||
$this->currentPage = min(ceil($this->start / $this->items_per_page) + 1, $numPages);
|
||||
|
||||
// Let's bear a few things in mind...
|
||||
$this->base_url = $options['base_url'];
|
||||
|
||||
// This should be set at all times, too.
|
||||
if (empty($options['no_items_label']))
|
||||
$options['no_items_label'] = '';
|
||||
|
||||
// Gather parameters for the data gather function first.
|
||||
$parameters = array($this->start, $this->items_per_page, $options['sort_order'], $options['sort_direction']);
|
||||
$parameters = [$this->start, $this->items_per_page, $options['sort_order'], $options['sort_direction']];
|
||||
if (!empty($options['get_data_params']) && is_array($options['get_data_params']))
|
||||
$parameters = array_merge($parameters, $options['get_data_params']);
|
||||
|
||||
// Okay, let's fetch the data!
|
||||
$data = call_user_func_array($options['get_data'], $parameters);
|
||||
$data = $options['get_data'](...$parameters);
|
||||
|
||||
// Clean up a bit.
|
||||
$rows = $data['rows'];
|
||||
// Extract data into local variables.
|
||||
$rawRowData = $data['rows'];
|
||||
$this->sort_order = $data['order'];
|
||||
$this->sort_direction = $data['direction'];
|
||||
unset($data);
|
||||
|
@ -71,24 +66,24 @@ class GenericTable extends PageIndex
|
|||
// Okay, now for the column headers...
|
||||
$this->generateColumnHeaders($options);
|
||||
|
||||
// Generate a pagination if requested
|
||||
if ($this->needsPageIndex)
|
||||
$this->generatePageIndex();
|
||||
// Should we create a page index?
|
||||
$needsPageIndex = !empty($this->items_per_page) && $this->recordCount > $this->items_per_page;
|
||||
if ($needsPageIndex)
|
||||
$this->generatePageIndex($options);
|
||||
|
||||
// Not a single row in sight?
|
||||
if (empty($rows))
|
||||
$this->body = $options['no_items_label'];
|
||||
// Otherwise, parse it all!
|
||||
// Process the data to be shown into rows.
|
||||
if (!empty($rawRowData))
|
||||
$this->processAllRows($rawRowData, $options);
|
||||
else
|
||||
$this->parseAllRows($rows, $options);
|
||||
$this->body = $options['no_items_label'] ?? '';
|
||||
|
||||
// Got a title?
|
||||
$this->title = isset($options['title']) ? htmlentities($options['title']) : '';
|
||||
$this->title_class = isset($options['title_class']) ? $options['title_class'] : '';
|
||||
$this->title = $options['title'] ?? '';
|
||||
$this->title_class = $options['title_class'] ?? '';
|
||||
|
||||
// Maybe even a form or two?
|
||||
$this->form_above = isset($options['form_above']) ? $options['form_above'] : (isset($options['form']) ? $options['form'] : null);
|
||||
$this->form_below = isset($options['form_below']) ? $options['form_below'] : (isset($options['form']) ? $options['form'] : null);
|
||||
$this->form_above = $options['form_above'] ?? $options['form'] ?? null;
|
||||
$this->form_below = $options['form_below'] ?? $options['form'] ?? null;
|
||||
}
|
||||
|
||||
private function generateColumnHeaders($options)
|
||||
|
@ -98,107 +93,35 @@ class GenericTable extends PageIndex
|
|||
if (empty($column['header']))
|
||||
continue;
|
||||
|
||||
$header = array(
|
||||
$isSortable = $this->tableIsSortable && !empty($column['is_sortable']);
|
||||
$sortDirection = $key == $this->sort_order && $this->sort_direction === 'up' ? 'down' : 'up';
|
||||
|
||||
$header = [
|
||||
'class' => isset($column['class']) ? $column['class'] : '',
|
||||
'colspan' => !empty($column['header_colspan']) ? $column['header_colspan'] : 1,
|
||||
'href' => !$this->tableIsSortable || empty($column['is_sortable']) ? '' : $this->getLink($this->start, $key, $key == $this->sort_order && $this->sort_direction == 'up' ? 'down' : 'up'),
|
||||
'href' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null,
|
||||
'label' => $column['header'],
|
||||
'scope' => 'col',
|
||||
'sort_mode' => $key == $this->sort_order ? $this->sort_direction : null,
|
||||
'width' => !empty($column['header_width']) && is_int($column['header_width']) ? $column['header_width'] : null,
|
||||
);
|
||||
];
|
||||
|
||||
$this->header[] = $header;
|
||||
}
|
||||
}
|
||||
|
||||
private function parseAllRows($rows, $options)
|
||||
private function generatePageIndex($options)
|
||||
{
|
||||
// Parse all rows...
|
||||
$i = 0;
|
||||
foreach ($rows as $row)
|
||||
{
|
||||
$i ++;
|
||||
$newRow = array(
|
||||
'class' => $i %2 == 1 ? 'odd' : 'even',
|
||||
'cells' => array(),
|
||||
);
|
||||
|
||||
foreach ($options['columns'] as $column)
|
||||
{
|
||||
if (isset($column['enabled']) && $column['enabled'] == false)
|
||||
continue;
|
||||
|
||||
// The hard way?
|
||||
if (isset($column['parse']))
|
||||
{
|
||||
if (!isset($column['parse']['type']))
|
||||
$column['parse']['type'] = 'value';
|
||||
|
||||
// Parse the basic value first.
|
||||
switch ($column['parse']['type'])
|
||||
{
|
||||
// value: easy as pie.
|
||||
default:
|
||||
case 'value':
|
||||
$value = $row[$column['parse']['data']];
|
||||
break;
|
||||
|
||||
// sprintf: filling the gaps!
|
||||
case 'sprintf':
|
||||
$parameters = array($column['parse']['data']['pattern']);
|
||||
foreach ($column['parse']['data']['arguments'] as $identifier)
|
||||
$parameters[] = $row[$identifier];
|
||||
$value = call_user_func_array('sprintf', $parameters);
|
||||
break;
|
||||
|
||||
// timestamps: let's make them readable!
|
||||
case 'timestamp':
|
||||
$pattern = !empty($column['parse']['data']['pattern']) && $column['parse']['data']['pattern'] == 'long' ? '%F %H:%M' : '%H:%M';
|
||||
|
||||
if (!is_numeric($row[$column['parse']['data']['timestamp']]))
|
||||
$timestamp = strtotime($row[$column['parse']['data']['timestamp']]);
|
||||
else
|
||||
$timestamp = (int) $row[$column['parse']['data']['timestamp']];
|
||||
|
||||
if (isset($column['parse']['data']['if_null']) && $timestamp == 0)
|
||||
$value = $column['parse']['data']['if_null'];
|
||||
else
|
||||
$value = strftime($pattern, $timestamp);
|
||||
break;
|
||||
|
||||
// function: the flexible way!
|
||||
case 'function':
|
||||
$value = $column['parse']['data']($row);
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate a link, if requested.
|
||||
if (!empty($column['parse']['link']))
|
||||
{
|
||||
// First, generate the replacement variables.
|
||||
$keys = array_keys($row);
|
||||
$values = array_values($row);
|
||||
foreach ($keys as $keyKey => $keyValue)
|
||||
$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
|
||||
|
||||
$value = '<a href="' . str_replace($keys, $values, $column['parse']['link']) . '">' . $value . '</a>';
|
||||
}
|
||||
}
|
||||
// The easy way!
|
||||
else
|
||||
$value = $row[$column['value']];
|
||||
|
||||
// Append the cell to the row.
|
||||
$newRow['cells'][] = array(
|
||||
'width' => !empty($column['cell_width']) && is_int($column['cell_width']) ? $column['cell_width'] : null,
|
||||
'value' => $value,
|
||||
);
|
||||
}
|
||||
|
||||
// Append the new row in the body.
|
||||
$this->body[] = $newRow;
|
||||
}
|
||||
$this->pageIndex = new PageIndex([
|
||||
'base_url' => $this->base_url,
|
||||
'index_class' => $options['index_class'] ?? '',
|
||||
'items_per_page' => $this->items_per_page,
|
||||
'linkBuilder' => [$this, 'getLink'],
|
||||
'recordCount' => $this->recordCount,
|
||||
'sort_direction' => $this->sort_direction,
|
||||
'sort_order' => $this->sort_order,
|
||||
'start' => $this->start,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getLink($start = null, $order = null, $dir = null)
|
||||
|
@ -218,12 +141,6 @@ class GenericTable extends PageIndex
|
|||
return $this->start;
|
||||
}
|
||||
|
||||
public function getArray()
|
||||
{
|
||||
// Makes no sense to call it for a table, but inherits from PageIndex due to poor design, sorry.
|
||||
throw new Exception('Function call is ambiguous.');
|
||||
}
|
||||
|
||||
public function getHeader()
|
||||
{
|
||||
return $this->header;
|
||||
|
@ -234,6 +151,16 @@ class GenericTable extends PageIndex
|
|||
return $this->body;
|
||||
}
|
||||
|
||||
public function getCurrentPage()
|
||||
{
|
||||
return $this->currentPage;
|
||||
}
|
||||
|
||||
public function getPageIndex()
|
||||
{
|
||||
return $this->pageIndex;
|
||||
}
|
||||
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
|
@ -243,4 +170,94 @@ class GenericTable extends PageIndex
|
|||
{
|
||||
return $this->title_class;
|
||||
}
|
||||
|
||||
private function processAllRows($rows, $options)
|
||||
{
|
||||
foreach ($rows as $i => $row)
|
||||
{
|
||||
$newRow = [
|
||||
'cells' => [],
|
||||
];
|
||||
|
||||
foreach ($options['columns'] as $column)
|
||||
{
|
||||
// Process data for this particular cell.
|
||||
if (isset($column['parse']))
|
||||
$value = self::processCell($column['parse'], $row);
|
||||
else
|
||||
$value = $row[$column['value']];
|
||||
|
||||
// Append the cell to the row.
|
||||
$newRow['cells'][] = [
|
||||
'value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
// Append the new row in the body.
|
||||
$this->body[] = $newRow;
|
||||
}
|
||||
}
|
||||
|
||||
private function processCell($options, $rowData)
|
||||
{
|
||||
if (!isset($options['type']))
|
||||
$options['type'] = 'value';
|
||||
|
||||
// Parse the basic value first.
|
||||
switch ($options['type'])
|
||||
{
|
||||
// Basic option: simply take a use a particular data property.
|
||||
case 'value':
|
||||
$value = htmlspecialchars($rowData[$options['data']]);
|
||||
break;
|
||||
|
||||
// Processing via a lambda function.
|
||||
case 'function':
|
||||
$value = $options['data']($rowData);
|
||||
break;
|
||||
|
||||
// Using sprintf to fill out a particular pattern.
|
||||
case 'sprintf':
|
||||
$parameters = [$options['data']['pattern']];
|
||||
foreach ($options['data']['arguments'] as $identifier)
|
||||
$parameters[] = $rowData[$identifier];
|
||||
|
||||
$value = sprintf(...$parameters);
|
||||
break;
|
||||
|
||||
// Timestamps get custom treatment.
|
||||
case 'timestamp':
|
||||
if (empty($options['data']['pattern']) || $options['data']['pattern'] === 'long')
|
||||
$pattern = 'Y-m-d H:i';
|
||||
elseif ($options['data']['pattern'] === 'short')
|
||||
$pattern = 'Y-m-d';
|
||||
else
|
||||
$pattern = $options['data']['pattern'];
|
||||
|
||||
if (!is_numeric($rowData[$options['data']['timestamp']]))
|
||||
$timestamp = strtotime($rowData[$options['data']['timestamp']]);
|
||||
else
|
||||
$timestamp = (int) $rowData[$options['data']['timestamp']];
|
||||
|
||||
if (isset($options['data']['if_null']) && $timestamp == 0)
|
||||
$value = $options['data']['if_null'];
|
||||
else
|
||||
$value = date($pattern, $timestamp);
|
||||
break;
|
||||
}
|
||||
|
||||
// Generate a link, if requested.
|
||||
if (!empty($options['link']))
|
||||
{
|
||||
// First, generate the replacement variables.
|
||||
$keys = array_keys($rowData);
|
||||
$values = array_values($rowData);
|
||||
foreach ($keys as $keyKey => $keyValue)
|
||||
$keys[$keyKey] = '{' . strtoupper($keyValue) . '}';
|
||||
|
||||
$value = '<a href="' . str_replace($keys, $values, $options['link']) . '">' . $value . '</a>';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
|
280
models/Image.php
280
models/Image.php
|
@ -82,221 +82,17 @@ class Image extends Asset
|
|||
* @param height: height of the thumbnail.
|
||||
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false].
|
||||
* @param generate: whether or not to generate a thumbnail if no existing file was found.
|
||||
*/
|
||||
public function getThumbnailUrl($width, $height, $crop = true, $fit = true)
|
||||
public function getThumbnailUrl($width, $height, $crop = true, $fit = true, $generate = false)
|
||||
{
|
||||
// First, assert the image's dimensions are properly known in the database.
|
||||
if (!isset($this->image_height, $this->image_width))
|
||||
throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
|
||||
|
||||
// Inferring width or height?
|
||||
if (!$height)
|
||||
$height = ceil($width / $this->image_width * $this->image_height);
|
||||
elseif (!$width)
|
||||
$width = ceil($height / $this->image_height * $this->image_width);
|
||||
|
||||
// Inferring the height from the original image's ratio?
|
||||
if (!$fit)
|
||||
$height = floor($width / ($this->image_width / $this->image_height));
|
||||
|
||||
// Assert we have both, now...
|
||||
if (empty($width) || empty($height))
|
||||
throw new InvalidArgumentException('Expecting at least either width or height as argument.');
|
||||
|
||||
// If we're cropping, verify we're in the right mode.
|
||||
if ($crop)
|
||||
{
|
||||
// If the original image's aspect ratio is much wider, take a slice instead.
|
||||
if ($this->image_width / $this->image_height > $width / $height)
|
||||
$crop = 'slice';
|
||||
|
||||
// We won't be cropping if the thumbnail is proportional to its original.
|
||||
if (abs($width / $height - $this->image_width / $this->image_height) <= 0.05)
|
||||
$crop = false;
|
||||
}
|
||||
|
||||
// Do we have an exact crop boundary for these dimensions?
|
||||
$crop_selector = "crop_{$width}x{$height}";
|
||||
if (isset($this->meta[$crop_selector]))
|
||||
$crop = 'exact';
|
||||
|
||||
// Now, do we need to suffix the filename?
|
||||
if ($crop)
|
||||
$suffix = '_c' . (is_string($crop) && $crop !== 'center' ? substr($crop, 0, 1) : '');
|
||||
else
|
||||
$suffix = '';
|
||||
|
||||
// Check whether we already resized this earlier.
|
||||
$thumb_selector = "thumb_{$width}x{$height}{$suffix}";
|
||||
if (isset($this->meta[$thumb_selector]) && file_exists(THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]))
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
||||
|
||||
// Do we have a custom thumbnail on file?
|
||||
$custom_selector = "custom_{$width}x{$height}";
|
||||
if (isset($this->meta[$custom_selector]))
|
||||
{
|
||||
if (file_exists(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]))
|
||||
{
|
||||
// Copy the custom thumbail to the general thumbnail directory.
|
||||
copy(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector],
|
||||
THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]);
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->meta[$thumb_selector] = $this->meta[$custom_selector];
|
||||
$this->save();
|
||||
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$custom_selector];
|
||||
}
|
||||
else
|
||||
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
|
||||
}
|
||||
|
||||
// Let's try some arcane stuff...
|
||||
try
|
||||
{
|
||||
if (!class_exists('Imagick'))
|
||||
throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");
|
||||
|
||||
$thumb = new Imagick(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename);
|
||||
|
||||
// The image might have some orientation set through EXIF. Let's apply this first.
|
||||
self::applyRotation($thumb);
|
||||
|
||||
// Just resizing? Easy peasy.
|
||||
if (!$crop)
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
// Cropping in the center?
|
||||
elseif ($crop === true || $crop === 'center')
|
||||
$thumb->cropThumbnailImage($width, $height);
|
||||
// Exact cropping? We can do that.
|
||||
elseif ($crop === 'exact')
|
||||
{
|
||||
list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->meta[$crop_selector]);
|
||||
$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
// Advanced cropping? Fun!
|
||||
else
|
||||
{
|
||||
$size = $thumb->getImageGeometry();
|
||||
|
||||
// Taking a horizontal slice from the top or bottom of the original image?
|
||||
if ($crop === 'top' || $crop === 'bottom')
|
||||
{
|
||||
$crop_width = $size['width'];
|
||||
$crop_height = floor($size['width'] / $width * $height);
|
||||
$target_x = 0;
|
||||
$target_y = $crop === 'top' ? 0 : $size['height'] - $crop_height;
|
||||
}
|
||||
// Otherwise, we're taking a vertical slice from the centre.
|
||||
else
|
||||
{
|
||||
$crop_width = floor($size['height'] / $height * $width);
|
||||
$crop_height = $size['height'];
|
||||
$target_x = floor(($size['width'] - $crop_width) / 2);
|
||||
$target_y = 0;
|
||||
}
|
||||
|
||||
$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
|
||||
$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
// What sort of image is this? Fall back to PNG if we must.
|
||||
switch ($thumb->getImageFormat())
|
||||
{
|
||||
case 'JPEG':
|
||||
$ext = 'jpg';
|
||||
$thumb->setImageCompressionQuality(60);
|
||||
break;
|
||||
|
||||
case 'GIF':
|
||||
$ext = 'gif';
|
||||
break;
|
||||
|
||||
case 'PNG':
|
||||
default:
|
||||
$thumb->setFormat('PNG');
|
||||
$ext = 'png';
|
||||
break;
|
||||
}
|
||||
|
||||
// So, how do we name this?
|
||||
$thumbfilename = substr($this->filename, 0, strrpos($this->filename, '.')) . "_{$width}x{$height}{$suffix}.$ext";
|
||||
|
||||
// Ensure the thumbnail subdirectory exists.
|
||||
if (!is_dir(THUMBSDIR . '/' . $this->subdir))
|
||||
mkdir(THUMBSDIR . '/' . $this->subdir, 0755, true);
|
||||
|
||||
// Save it in a public spot.
|
||||
$thumb->writeImage(THUMBSDIR . '/' . $this->subdir . '/' . $thumbfilename);
|
||||
$thumb->clear();
|
||||
$thumb->destroy();
|
||||
}
|
||||
// Blast! Curse your sudden but inevitable betrayal!
|
||||
catch (ImagickException $e)
|
||||
{
|
||||
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->meta[$thumb_selector] = $thumbfilename;
|
||||
$this->save();
|
||||
|
||||
// Ah yes, you wanted a URL, didn't you...
|
||||
return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
|
||||
$thumbnail = new Thumbnail($this);
|
||||
return $thumbnail->getUrl($width, $height, $crop, $fit, $generate);
|
||||
}
|
||||
|
||||
private static function applyRotation(Imagick $image)
|
||||
public function getId()
|
||||
{
|
||||
switch ($image->getImageOrientation())
|
||||
{
|
||||
// Clockwise rotation
|
||||
case Imagick::ORIENTATION_RIGHTTOP:
|
||||
$image->rotateImage("#000", 90);
|
||||
break;
|
||||
|
||||
// Counter-clockwise rotation
|
||||
case Imagick::ORIENTATION_LEFTBOTTOM:
|
||||
$image->rotateImage("#000", 270);
|
||||
break;
|
||||
|
||||
// Upside down?
|
||||
case Imagick::ORIENTATION_BOTTOMRIGHT:
|
||||
$image->rotateImage("#000", 180);
|
||||
}
|
||||
|
||||
// Having rotated the image, make sure the EXIF data is set properly.
|
||||
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
|
||||
}
|
||||
|
||||
public function bestColor()
|
||||
{
|
||||
// Save some computations if we can.
|
||||
if (isset($this->meta['best_color']))
|
||||
return $this->meta['best_color'];
|
||||
|
||||
// Find out what colour is most prominent.
|
||||
$color = new BestColor($this);
|
||||
$this->meta['best_color'] = $color->hex();
|
||||
$this->save();
|
||||
|
||||
// There's your colour.
|
||||
return $this->meta['best_color'];
|
||||
}
|
||||
|
||||
public function bestLabelColor()
|
||||
{
|
||||
// Save some computations if we can.
|
||||
if (isset($this->meta['best_color_label']))
|
||||
return $this->meta['best_color_label'];
|
||||
|
||||
// Find out what colour is most prominent.
|
||||
$color = new BestColor($this);
|
||||
$this->meta['best_color_label'] = $color->rgba();
|
||||
$this->save();
|
||||
|
||||
// There's your colour.
|
||||
return $this->meta['best_color_label'];
|
||||
return $this->id_asset;
|
||||
}
|
||||
|
||||
public function width()
|
||||
|
@ -309,37 +105,69 @@ class Image extends Asset
|
|||
return $this->image_height;
|
||||
}
|
||||
|
||||
public function ratio()
|
||||
{
|
||||
return $this->image_width / $this->image_height;
|
||||
}
|
||||
|
||||
public function isPanorama()
|
||||
{
|
||||
return $this->image_width / $this->image_height > 2;
|
||||
return $this->ratio() >= 2;
|
||||
}
|
||||
|
||||
public function isPortrait()
|
||||
{
|
||||
return $this->image_width / $this->image_height < 1;
|
||||
return $this->ratio() < 1;
|
||||
}
|
||||
|
||||
public function isLandscape()
|
||||
{
|
||||
$ratio = $this->image_width / $this->image_height;
|
||||
$ratio = $this->ratio();
|
||||
return $ratio >= 1 && $ratio <= 2;
|
||||
}
|
||||
|
||||
public function getThumbnails()
|
||||
{
|
||||
return $this->thumbnails;
|
||||
}
|
||||
|
||||
public function removeAllThumbnails()
|
||||
{
|
||||
foreach ($this->meta as $key => $value)
|
||||
foreach ($this->thumbnails as $key => $filename)
|
||||
{
|
||||
if (substr($key, 0, 6) !== 'thumb_')
|
||||
continue;
|
||||
|
||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
|
||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
|
||||
if (is_file($thumb_path))
|
||||
unlink($thumb_path);
|
||||
|
||||
unset($this->meta[$key]);
|
||||
}
|
||||
|
||||
$this->saveMetaData();
|
||||
return Registry::get('db')->query('
|
||||
DELETE FROM assets_thumbs
|
||||
WHERE id_asset = {int:id_asset}',
|
||||
['id_asset' => $this->id_asset]);
|
||||
}
|
||||
|
||||
public function removeThumbnailsOfSize($width, $height)
|
||||
{
|
||||
foreach ($this->thumbnails as $key => $filename)
|
||||
{
|
||||
if (strpos($key, $width . 'x' . $height) !== 0)
|
||||
continue;
|
||||
|
||||
$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename;
|
||||
if (is_file($thumb_path))
|
||||
unlink($thumb_path);
|
||||
}
|
||||
|
||||
return Registry::get('db')->query('
|
||||
DELETE FROM assets_thumbs
|
||||
WHERE id_asset = {int:id_asset} AND
|
||||
width = {int:width} AND
|
||||
height = {int:height}',
|
||||
[
|
||||
'height' => $height,
|
||||
'id_asset' => $this->id_asset,
|
||||
'width' => $width,
|
||||
]);
|
||||
}
|
||||
|
||||
public function replaceThumbnail($descriptor, $tmp_file)
|
||||
|
@ -347,7 +175,7 @@ class Image extends Asset
|
|||
if (!is_file($tmp_file))
|
||||
return -1;
|
||||
|
||||
if (!isset($this->meta[$descriptor]))
|
||||
if (!isset($this->thumbnails[$descriptor]))
|
||||
return -2;
|
||||
|
||||
$image = new Imagick($tmp_file);
|
||||
|
@ -355,12 +183,12 @@ class Image extends Asset
|
|||
unset($image);
|
||||
|
||||
// Check whether dimensions match.
|
||||
$test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height'];
|
||||
$test_descriptor = $d['width'] . 'x' . $d['height'];
|
||||
if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
|
||||
return -3;
|
||||
|
||||
// Save the custom thumbnail in the assets directory.
|
||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
||||
$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->thumbnails[$descriptor];
|
||||
if (file_exists($destination) && !is_writable($destination))
|
||||
return -4;
|
||||
|
||||
|
@ -368,7 +196,7 @@ class Image extends Asset
|
|||
return -5;
|
||||
|
||||
// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
|
||||
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
|
||||
$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->thumbnails[$descriptor];
|
||||
if (file_exists($destination) && !is_writable($destination))
|
||||
return -6;
|
||||
|
||||
|
@ -376,7 +204,7 @@ class Image extends Asset
|
|||
return -7;
|
||||
|
||||
// A little bookkeeping
|
||||
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor];
|
||||
$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->thumbnails[$descriptor];
|
||||
$this->saveMetaData();
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -8,26 +8,47 @@
|
|||
|
||||
class PageIndex
|
||||
{
|
||||
protected $page_index = [];
|
||||
protected $current_page = 0;
|
||||
protected $items_per_page = 0;
|
||||
protected $needsPageIndex = false;
|
||||
protected $num_pages = 0;
|
||||
protected $recordCount = 0;
|
||||
protected $start = 0;
|
||||
protected $sort_order = null;
|
||||
protected $sort_direction = null;
|
||||
protected $base_url;
|
||||
protected $index_class = 'pagination';
|
||||
protected $page_slug = '%AMP%page=%PAGE%';
|
||||
private $base_url;
|
||||
private $current_page = 1;
|
||||
private $index_class = 'pagination';
|
||||
private $items_per_page = 0;
|
||||
private $linkBuilder;
|
||||
private $needsPageIndex = false;
|
||||
private $num_pages = 1;
|
||||
private $page_index = [];
|
||||
private $page_slug = '%AMP%page=%PAGE%';
|
||||
private $recordCount = 0;
|
||||
private $sort_direction = null;
|
||||
private $sort_order = null;
|
||||
private $start = 0;
|
||||
|
||||
public function __construct($options)
|
||||
{
|
||||
foreach ($options as $key => $value)
|
||||
$this->$key = $value;
|
||||
static $neededKeys = ['base_url', 'items_per_page', 'recordCount'];
|
||||
foreach ($neededKeys as $key)
|
||||
{
|
||||
if (!isset($options[$key]))
|
||||
throw new Exception('PageIndex: argument ' . $key . ' missing in options');
|
||||
|
||||
$this->$key = $options[$key];
|
||||
}
|
||||
|
||||
static $optionalKeys = ['index_class', 'linkBuilder', 'page_slug', 'sort_direction', 'sort_order', 'start'];
|
||||
foreach ($optionalKeys as $key)
|
||||
if (isset($options[$key]))
|
||||
$this->$key = $options[$key];
|
||||
|
||||
$this->generatePageIndex();
|
||||
}
|
||||
|
||||
private function buildLink($start = null, $order = null, $dir = null)
|
||||
{
|
||||
if (isset($this->linkBuilder))
|
||||
return call_user_func($this->linkBuilder, $start, $order, $dir);
|
||||
else
|
||||
return $this->getLink($start, $order, $dir);
|
||||
}
|
||||
|
||||
protected function generatePageIndex()
|
||||
{
|
||||
/*
|
||||
|
@ -68,7 +89,7 @@ class PageIndex
|
|||
$this->page_index[$p] = [
|
||||
'index' => $p,
|
||||
'is_selected' => $this->current_page == $p,
|
||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
'href'=> $this->buildLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
|
||||
// The center of the page index.
|
||||
|
@ -81,7 +102,7 @@ class PageIndex
|
|||
$this->page_index[$center] = [
|
||||
'index' => $center,
|
||||
'is_selected' => $this->current_page == $center,
|
||||
'href'=> $this->getLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
'href'=> $this->buildLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -94,7 +115,7 @@ class PageIndex
|
|||
$this->page_index[$p] = [
|
||||
'index' => $p,
|
||||
'is_selected' => $this->current_page == $p,
|
||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
'href'=> $this->buildLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
|
||||
// The center of the page index.
|
||||
|
@ -107,7 +128,7 @@ class PageIndex
|
|||
$this->page_index[$center] = [
|
||||
'index' => $center,
|
||||
'is_selected' => $this->current_page == $center,
|
||||
'href'=> $this->getLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
'href'=> $this->buildLink(($center - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -120,7 +141,7 @@ class PageIndex
|
|||
$this->page_index[$p] = [
|
||||
'index' => $p,
|
||||
'is_selected' => $this->current_page == $p,
|
||||
'href'=> $this->getLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
'href'=> $this->buildLink(($p - 1) * $this->items_per_page, $this->sort_order, $this->sort_direction),
|
||||
];
|
||||
|
||||
// Previous page?
|
||||
|
@ -157,11 +178,6 @@ class PageIndex
|
|||
return $url;
|
||||
}
|
||||
|
||||
public function getArray()
|
||||
{
|
||||
return $this->page_index;
|
||||
}
|
||||
|
||||
public function getPageIndex()
|
||||
{
|
||||
return $this->page_index;
|
||||
|
|
|
@ -19,13 +19,13 @@ class Session
|
|||
if (!isset($_SERVER['HTTPS']) && isset($_SERVER['REMOTE_ADDR']) && $_SESSION['ip_address'] !== $_SERVER['REMOTE_ADDR'])
|
||||
{
|
||||
$_SESSION = [];
|
||||
throw new UserFacingException('Your session failed to validate: your IP address has changed. Please re-login and try again.');
|
||||
Dispatcher::kickGuest('Your session failed to validate', 'Your IP address has changed. Please re-login and try again.');
|
||||
}
|
||||
// Either way, require re-login if the browser identifier has changed.
|
||||
elseif (isset($_SERVER['HTTP_USER_AGENT']) && $_SESSION['user_agent'] !== $_SERVER['HTTP_USER_AGENT'])
|
||||
{
|
||||
$_SESSION = [];
|
||||
throw new UserFacingException('Your session failed to validate: your browser identifier has changed. Please re-login and try again.');
|
||||
Dispatcher::kickGuest('Your session failed to validate', 'Your browser identifier has changed. Please re-login and try again.');
|
||||
}
|
||||
}
|
||||
elseif (!isset($_SESSION['ip_address'], $_SESSION['user_agent']))
|
||||
|
|
|
@ -260,6 +260,8 @@ class Tag
|
|||
id_parent = {int:id_parent},
|
||||
id_asset_thumb = {int:id_asset_thumb},
|
||||
tag = {string:tag},
|
||||
slug = {string:slug},
|
||||
description = {string:description},
|
||||
count = {int:count}
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
get_object_vars($this));
|
||||
|
@ -270,7 +272,7 @@ class Tag
|
|||
$db = Registry::get('db');
|
||||
|
||||
$res = $db->query('
|
||||
DELETE FROM posts_tags
|
||||
DELETE FROM assets_tags
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
[
|
||||
'id_tag' => $this->id_tag,
|
||||
|
@ -287,6 +289,34 @@ class Tag
|
|||
]);
|
||||
}
|
||||
|
||||
public function resetIdAsset()
|
||||
{
|
||||
$db = Registry::get('db');
|
||||
|
||||
$row = $db->query('
|
||||
SELECT MAX(id_asset) as new_id
|
||||
FROM assets_tags
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
[
|
||||
'id_tag' => $this->id_tag,
|
||||
]);
|
||||
|
||||
$new_id = 0;
|
||||
if(!empty($row))
|
||||
{
|
||||
$new_id = $row->fetch_assoc()['new_id'];
|
||||
}
|
||||
|
||||
return $db->query('
|
||||
UPDATE tags
|
||||
SET id_asset_thumb = {int:new_id}
|
||||
WHERE id_tag = {int:id_tag}',
|
||||
[
|
||||
'new_id' => $new_id,
|
||||
'id_tag' => $this->id_tag,
|
||||
]);
|
||||
}
|
||||
|
||||
public static function match($tokens)
|
||||
{
|
||||
if (!is_array($tokens))
|
||||
|
@ -305,8 +335,8 @@ class Tag
|
|||
if (!is_array($tokens))
|
||||
$tokens = explode(' ', $tokens);
|
||||
|
||||
return Registry::get('db')->queryPair('
|
||||
SELECT id_tag, tag
|
||||
return Registry::get('db')->queryPairs('
|
||||
SELECT id_tag, tag, slug
|
||||
FROM tags
|
||||
WHERE LOWER(tag) LIKE {string:tokens} AND
|
||||
kind = {string:person}
|
||||
|
|
|
@ -0,0 +1,354 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* Thumbnail.php
|
||||
* Contains key class Thumbnail.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2020, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Thumbnail
|
||||
{
|
||||
private $image;
|
||||
private $thumbnails;
|
||||
|
||||
private $properly_initialised;
|
||||
private $width;
|
||||
private $height;
|
||||
private $crop_mode;
|
||||
|
||||
const CROP_MODE_NONE = 0;
|
||||
const CROP_MODE_BOUNDARY = 1;
|
||||
const CROP_MODE_CUSTOM_FILE = 2;
|
||||
const CROP_MODE_SLICE_TOP = 3;
|
||||
const CROP_MODE_SLICE_CENTRE = 4;
|
||||
const CROP_MODE_SLICE_BOTTOM = 5;
|
||||
|
||||
public function __construct($image)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->image_meta = $image->getMeta();
|
||||
$this->thumbnails = $image->getThumbnails();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width: width of the thumbnail.
|
||||
* @param height: height of the thumbnail.
|
||||
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimate [false].
|
||||
* @param generate: whether or not to generate a thumbnail if no existing file was found.
|
||||
*/
|
||||
public function getUrl($width, $height, $crop = true, $fit = true, $generate = false)
|
||||
{
|
||||
$this->init($width, $height, $crop, $fit);
|
||||
|
||||
// Check whether we've already resized this earlier.
|
||||
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
|
||||
if (!empty($this->thumbnails[$thumb_selector]))
|
||||
{
|
||||
$thumb_path = '/' . $this->image->getSubdir() . '/' . $this->thumbnails[$thumb_selector];
|
||||
if (file_exists(THUMBSDIR . $thumb_path))
|
||||
return THUMBSURL . $thumb_path;
|
||||
}
|
||||
|
||||
// Do we have a custom thumbnail on file?
|
||||
$custom_selector = 'custom_' . $this->width . 'x' . $this->height;
|
||||
if (isset($this->image_meta[$custom_selector]))
|
||||
{
|
||||
$custom_thumb_path = '/' . $this->image->getSubdir() . '/' . $this->image_meta[$custom_selector];
|
||||
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(ASSETSDIR . $custom_thumb_path, THUMBSDIR . $custom_thumb_path);
|
||||
|
||||
// Let's remember this for future reference.
|
||||
$this->markAsGenerated($this->image_meta[$custom_selector]);
|
||||
|
||||
return THUMBSURL . $custom_thumb_path;
|
||||
}
|
||||
else
|
||||
throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
|
||||
}
|
||||
|
||||
// Is this the right moment to generate a thumbnail, then?
|
||||
if ($generate && array_key_exists($thumb_selector, $this->thumbnails))
|
||||
{
|
||||
return $this->generate();
|
||||
}
|
||||
|
||||
// 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
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param width: width of the thumbnail.
|
||||
* @param height: height of the thumbnail.
|
||||
* @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
|
||||
* @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimate [false].
|
||||
*/
|
||||
private function init($width, $height, $crop = true, $fit = true)
|
||||
{
|
||||
$this->properly_initialised = false;
|
||||
|
||||
// First, assert the image's dimensions are properly known in the database.
|
||||
if ($this->image->width() === null || $this->image->height() === null)
|
||||
throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');
|
||||
|
||||
$this->width = $width;
|
||||
$this->height = $height;
|
||||
|
||||
// Inferring width or height?
|
||||
if (!$this->height)
|
||||
$this->height = ceil($this->width / $this->image->ratio());
|
||||
elseif (!$this->width)
|
||||
$this->width = ceil($this->height * $this->image->ratio());
|
||||
|
||||
// Inferring the height from the original image's ratio?
|
||||
if (!$fit)
|
||||
$this->height = floor($this->width / $this->image->ratio());
|
||||
|
||||
// Assert we have both, now...
|
||||
if (empty($this->width) || empty($this->height))
|
||||
throw new InvalidArgumentException('Expecting at least either width or height as argument.');
|
||||
|
||||
// If we're cropping, verify we're in the right mode.
|
||||
if ($crop)
|
||||
{
|
||||
// Do we have an exact crop boundary set for these dimensions?
|
||||
$crop_selector = 'crop_' . $this->width . 'x' . $this->height;
|
||||
if (isset($this->image_meta[$crop_selector]))
|
||||
$this->crop_mode = self::CROP_MODE_BOUNDARY;
|
||||
|
||||
// We won't be cropping if the thumbnail is proportional to its original.
|
||||
elseif (abs($this->ratio() - $this->image->ratio()) <= 0.025)
|
||||
$this->crop_mode = self::CROP_MODE_NONE;
|
||||
|
||||
// If the original image's aspect ratio is much wider, take a slice instead.
|
||||
elseif ($this->image->ratio() > $this->ratio())
|
||||
$this->crop_mode = self::CROP_MODE_SLICE_CENTRE;
|
||||
|
||||
// Slice from the top?
|
||||
elseif ($crop === 'top' || $crop === 'ct')
|
||||
$this->crop_mode = self::CROP_MODE_SLICE_TOP;
|
||||
|
||||
// Slice from the bottom?
|
||||
elseif ($crop === 'bottom' || $crop === 'cb')
|
||||
$this->crop_mode = self::CROP_MODE_SLICE_BOTTOM;
|
||||
|
||||
// Slice from the centre?
|
||||
elseif ($crop === 'centre' || $crop === 'center' || $crop === 'cs' || $crop === true)
|
||||
$this->crop_mode = self::CROP_MODE_SLICE_CENTRE;
|
||||
|
||||
// Unexpected value? Assume no crop.
|
||||
else
|
||||
$this->crop_mode = self::CROP_MODE_NONE;
|
||||
}
|
||||
else
|
||||
$this->crop_mode = self::CROP_MODE_NONE;
|
||||
|
||||
// Now, do we need to suffix the filename?
|
||||
if ($this->crop_mode !== self::CROP_MODE_NONE)
|
||||
{
|
||||
$this->filename_suffix = '_c';
|
||||
if ($this->crop_mode === self::CROP_MODE_SLICE_TOP)
|
||||
$this->filename_suffix .= 't';
|
||||
elseif ($this->crop_mode === self::CROP_MODE_SLICE_CENTRE)
|
||||
$this->filename_suffix .= 's';
|
||||
elseif ($this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
|
||||
$this->filename_suffix .= 'b';
|
||||
elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
|
||||
$this->filename_suffix .= 'e';
|
||||
}
|
||||
else
|
||||
$this->filename_suffix = '';
|
||||
|
||||
$this->properly_initialised = true;
|
||||
}
|
||||
|
||||
private function generate()
|
||||
{
|
||||
if (!$this->properly_initialised)
|
||||
throw new UnexpectedValueException('The thumbnail factory was not intialised before use!');
|
||||
|
||||
// Let's try some arcane stuff...
|
||||
try
|
||||
{
|
||||
if (!class_exists('Imagick'))
|
||||
throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");
|
||||
|
||||
$thumb = new Imagick(ASSETSDIR . '/' . $this->image->getSubdir() . '/' . $this->image->getFilename());
|
||||
|
||||
// The image might have some orientation set through EXIF. Let's apply this first.
|
||||
self::applyRotation($thumb);
|
||||
|
||||
// Just resizing? Easy peasy.
|
||||
if ($this->crop_mode === self::CROP_MODE_NONE)
|
||||
$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
|
||||
|
||||
// // Cropping in the center?
|
||||
elseif ($this->crop_mode === self::CROP_MODE_SLICE_CENTRE)
|
||||
$thumb->cropThumbnailImage($this->width, $this->height);
|
||||
|
||||
// Exact cropping? We can do that.
|
||||
elseif ($this->crop_mode === self::CROP_MODE_BOUNDARY)
|
||||
{
|
||||
$crop_selector = 'crop_' . $this->width . 'x' . $this->height;
|
||||
list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->image_meta[$crop_selector]);
|
||||
$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
|
||||
$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
// Advanced cropping? Fun!
|
||||
else
|
||||
{
|
||||
$size = $thumb->getImageGeometry();
|
||||
|
||||
// Taking a horizontal slice from the top or bottom of the original image?
|
||||
if ($this->crop_mode === self::CROP_MODE_SLICE_TOP || $this->crop_mode === self::CROP_MODE_SLICE_BOTTOM)
|
||||
{
|
||||
$crop_width = $size['width'];
|
||||
$crop_height = floor($size['width'] / $this->width * $this->height);
|
||||
$target_x = 0;
|
||||
$target_y = $this->crop_mode === self::CROP_MODE_SLICE_TOP ? 0 : $size['height'] - $crop_height;
|
||||
}
|
||||
// Otherwise, we're taking a vertical slice from the centre.
|
||||
else
|
||||
{
|
||||
$crop_width = floor($size['height'] / $this->height * $this->width);
|
||||
$crop_height = $size['height'];
|
||||
$target_x = floor(($size['width'] - $crop_width) / 2);
|
||||
$target_y = 0;
|
||||
}
|
||||
|
||||
$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
|
||||
$thumb->resizeImage($this->width, $this->height, Imagick::FILTER_LANCZOS, 1);
|
||||
}
|
||||
|
||||
// What sort of image is this? Fall back to PNG if we must.
|
||||
switch ($thumb->getImageFormat())
|
||||
{
|
||||
case 'JPEG':
|
||||
$ext = 'jpg';
|
||||
break;
|
||||
|
||||
case 'GIF':
|
||||
$ext = 'gif';
|
||||
break;
|
||||
|
||||
case 'PNG':
|
||||
default:
|
||||
$thumb->setFormat('PNG');
|
||||
$ext = 'png';
|
||||
break;
|
||||
}
|
||||
|
||||
// So, how do we name this?
|
||||
$thumb_filename = substr($this->image->getFilename(), 0, strrpos($this->image->getFilename(), '.')) .
|
||||
'_' . $this->width . 'x' . $this->height . $this->filename_suffix . '.' . $ext;
|
||||
|
||||
// Ensure the thumbnail subdirectory exists.
|
||||
if (!is_dir(THUMBSDIR . '/' . $this->image->getSubdir()))
|
||||
mkdir(THUMBSDIR . '/' . $this->image->getSubdir(), 0755, true);
|
||||
|
||||
// No need to preserve every detail.
|
||||
$thumb->setImageCompressionQuality(80);
|
||||
|
||||
// Save it in a public spot.
|
||||
$thumb->writeImage(THUMBSDIR . '/' . $this->image->getSubdir() . '/' . $thumb_filename);
|
||||
|
||||
// Let's remember this for future reference...
|
||||
$this->markAsGenerated($thumb_filename);
|
||||
|
||||
$thumb->clear();
|
||||
$thumb->destroy();
|
||||
|
||||
// Finally, return the URL for the generated thumbnail image.
|
||||
return THUMBSURL . '/' . $this->image->getSubdir() . '/' . $thumb_filename;
|
||||
}
|
||||
// Blast! Curse your sudden but inevitable betrayal!
|
||||
catch (ImagickException $e)
|
||||
{
|
||||
throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static function applyRotation(Imagick $image)
|
||||
{
|
||||
switch ($image->getImageOrientation())
|
||||
{
|
||||
// Clockwise rotation
|
||||
case Imagick::ORIENTATION_RIGHTTOP:
|
||||
$image->rotateImage("#000", 90);
|
||||
break;
|
||||
|
||||
// Counter-clockwise rotation
|
||||
case Imagick::ORIENTATION_LEFTBOTTOM:
|
||||
$image->rotateImage("#000", 270);
|
||||
break;
|
||||
|
||||
// Upside down?
|
||||
case Imagick::ORIENTATION_BOTTOMRIGHT:
|
||||
$image->rotateImage("#000", 180);
|
||||
}
|
||||
|
||||
// Having rotated the image, make sure the EXIF data is set properly.
|
||||
$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
|
||||
}
|
||||
|
||||
private function ratio()
|
||||
{
|
||||
return $this->width / $this->height;
|
||||
}
|
||||
|
||||
private function updateDb($filename)
|
||||
{
|
||||
if (!$this->properly_initialised)
|
||||
throw new UnexpectedValueException('The thumbnail factory was not intialised before use!');
|
||||
|
||||
$mode = !empty($this->filename_suffix) ? substr($this->filename_suffix, 1) : '';
|
||||
$success = Registry::get('db')->insert('replace', 'assets_thumbs', [
|
||||
'id_asset' => 'int',
|
||||
'width' => 'int',
|
||||
'height' => 'int',
|
||||
'mode' => 'string-3',
|
||||
'filename' => 'string-255',
|
||||
], [
|
||||
'id_asset' => $this->image->getId(),
|
||||
'width' => $this->width,
|
||||
'height' => $this->height,
|
||||
'mode' => $mode,
|
||||
'filename' => $filename,
|
||||
]);
|
||||
|
||||
if ($success)
|
||||
{
|
||||
$thumb_selector = $this->width . 'x' . $this->height . $this->filename_suffix;
|
||||
$this->thumbnails[$thumb_selector] = $filename !== 'NULL' ? $filename : '';
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
private function markAsQueued()
|
||||
{
|
||||
$this->updateDb('NULL');
|
||||
}
|
||||
|
||||
private function markAsGenerated($filename)
|
||||
{
|
||||
$this->updateDb($filename);
|
||||
}
|
||||
}
|
|
@ -145,46 +145,55 @@ body {
|
|||
/* Crop editor
|
||||
----------------*/
|
||||
#crop_editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #000;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: 100;
|
||||
color: #fff;
|
||||
}
|
||||
#crop_editor input {
|
||||
#crop_editor input[type=number] {
|
||||
width: 50px;
|
||||
background: #555;
|
||||
color: #fff;
|
||||
}
|
||||
.crop_image_container {
|
||||
position: relative;
|
||||
#crop_editor input[type=checkbox] {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.crop_position {
|
||||
background: rgba(0, 0, 0, 1.0);
|
||||
border: none;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.crop_position input, .crop_position .btn {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.crop_image_container {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
max-height: calc(100% - 34px);
|
||||
}
|
||||
.crop_image_container img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
border: 1px solid #000;
|
||||
max-height: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 700px;
|
||||
}
|
||||
#crop_boundary {
|
||||
border: 1px solid rgba(255, 255, 255, 0.75);
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
border: 1px dashed rgb(255, 255, 255);
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
cursor: move;
|
||||
position: absolute;
|
||||
z-index: 200;
|
||||
width: 500px;
|
||||
height: 300px;
|
||||
top: 400px;
|
||||
left: 300px;
|
||||
filter: invert(100%); /* temp */
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -8,18 +8,18 @@
|
|||
@import url(//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic);
|
||||
|
||||
@font-face {
|
||||
font-family: 'Invaders';
|
||||
src: url('fonts/invaders.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-family: 'Invaders';
|
||||
src: url('fonts/invaders.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
font: 13px/1.7 "Open Sans", sans-serif;
|
||||
padding: 0 0 3em;
|
||||
margin: 0;
|
||||
background: #99BFCE 0 -50% fixed;
|
||||
background-image: radial-gradient(ellipse at top, #c3dee5 0%,#92b9ca 55%,#365e77 100%); /* W3C */
|
||||
background: #aaa 0 -50% fixed;
|
||||
background-image: radial-gradient(ellipse at top, #ccc 0%, #aaa 55%, #333 100%);
|
||||
}
|
||||
|
||||
#wrapper, header {
|
||||
|
@ -34,11 +34,11 @@ header {
|
|||
}
|
||||
|
||||
a {
|
||||
color: #487C96;
|
||||
color: #963626;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
color: #222;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* Logo
|
||||
|
@ -135,7 +135,7 @@ ul#nav li a:hover {
|
|||
}
|
||||
|
||||
.pagination .page-padding {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
|
@ -163,14 +163,12 @@ ul#nav li a:hover {
|
|||
.tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama,
|
||||
.tiled_grid div.duo, .tiled_grid div.single {
|
||||
background: #fff;
|
||||
border-bottom-style: none !important;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
float: left;
|
||||
line-height: 0;
|
||||
margin: 0 3.5% 3.5% 0;
|
||||
overflow: none;
|
||||
position: relative;
|
||||
padding-bottom: 5px;
|
||||
width: 31%;
|
||||
}
|
||||
.tiled_grid div img {
|
||||
|
@ -181,7 +179,7 @@ ul#nav li a:hover {
|
|||
color: #000;
|
||||
margin: 0;
|
||||
font: 400 18px "Open Sans", sans-serif;
|
||||
padding: 20px 5px 15px;
|
||||
padding: 15px 5px;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
|
@ -192,9 +190,7 @@ ul#nav li a:hover {
|
|||
}
|
||||
.tiled_grid div.landscape:hover, .tiled_grid div.portrait:hover, .tiled_grid div.panorama:hover,
|
||||
.tiled_grid div.duo:hover, .tiled_grid div.single:hover {
|
||||
padding-bottom: 0;
|
||||
border-bottom-width: 5px;
|
||||
border-bottom-style: solid !important;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Panoramas */
|
||||
|
@ -305,7 +301,7 @@ ul#nav li a:hover {
|
|||
margin: 0 0 1.5% 0;
|
||||
}
|
||||
.album_title_box h2 {
|
||||
color: #487C96;
|
||||
color: #262626;
|
||||
font: 400 18px/2 "Open Sans", sans-serif !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
@ -379,34 +375,59 @@ footer a {
|
|||
|
||||
input, select, .btn {
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 4px;
|
||||
color: #000;
|
||||
font: 13px/1.7 "Open Sans", "Helvetica", sans-serif;
|
||||
padding: 3px;
|
||||
}
|
||||
input[type=submit], button, .btn {
|
||||
background: #C5E2EA;
|
||||
border-radius: 3px;
|
||||
border: 1px solid #8BBFCE;
|
||||
display: inline-block;
|
||||
font: inherit;
|
||||
padding: 4px 5px;
|
||||
}
|
||||
input[type=submit]:hover, button:hover, .btn:hover {
|
||||
background-color: #bddce5;
|
||||
border-color: #88b7c6;
|
||||
}
|
||||
textarea {
|
||||
border: 1px solid #ccc;
|
||||
font: 12px/1.4 'Monaco', 'Inconsolata', 'DejaVu Sans Mono', monospace;
|
||||
border: 1px solid #dbdbdb;
|
||||
border-radius: 4px;
|
||||
font: 14px/1.4 'Inconsolata', 'DejaVu Sans Mono', monospace;
|
||||
padding: 0.75%;
|
||||
width: 98.5%;
|
||||
}
|
||||
|
||||
input[type=submit], button, .btn {
|
||||
background-color: #eee;
|
||||
border-color: #dbdbdb;
|
||||
border-width: 1px;
|
||||
border-radius: 4px;
|
||||
color: #363636;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
justify-content: center;
|
||||
padding-bottom: calc(0.4em - 1px);
|
||||
padding-left: 0.8em;
|
||||
padding-right: 0.8em;
|
||||
padding-top: calc(0.4em - 1px);
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
input:hover, select:hover, button:hover, .btn:hover {
|
||||
border-color: #b5b5b5;
|
||||
}
|
||||
input:focus, select:focus, button:focus, .btn:focus {
|
||||
border-color: #3273dc;
|
||||
}
|
||||
input:focus:not(:active), select:focus:not(:active), button:focus:not(:active), .btn:focus:not(:active) {
|
||||
box-shadow: 0px 0px 0px 2px rgba(50, 115, 220, 0.25);
|
||||
}
|
||||
input:active, select:active, button:active, .btn:active {
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.btn-red {
|
||||
background: #F3B076;
|
||||
border-color: #C98245;
|
||||
background: #eebbaa;
|
||||
border-color: #cc9988;
|
||||
}
|
||||
.btn-red:hover, .btn-red:focus {
|
||||
border-color: #bb7766;
|
||||
color: #000;
|
||||
}
|
||||
.btn-red:focus:not(:active) {
|
||||
box-shadow: 0px 0px 0px 2px rgba(241, 70, 104, 0.25);
|
||||
}
|
||||
|
||||
|
||||
/* Login box styles
|
||||
|
@ -441,6 +462,7 @@ textarea {
|
|||
width: 100%;
|
||||
}
|
||||
#login div.alert {
|
||||
line-height: normal;
|
||||
margin: 15px 0;
|
||||
}
|
||||
#login div.buttonstrip {
|
||||
|
@ -519,12 +541,9 @@ 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;
|
||||
|
@ -539,7 +558,7 @@ textarea {
|
|||
#previous_photo, #next_photo {
|
||||
background: #fff;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
color: #678FA4;
|
||||
color: #262626;
|
||||
font-size: 3em;
|
||||
line-height: 0.5;
|
||||
padding: 32px 8px;
|
||||
|
@ -572,7 +591,7 @@ a#previous_photo:hover, a#next_photo:hover {
|
|||
content: '→';
|
||||
}
|
||||
|
||||
#sub_photo h2, #sub_photo h3, #photo_exif_box h3 {
|
||||
#sub_photo h2, #sub_photo h3, #photo_exif_box h3, #user_actions_box h3 {
|
||||
font: 600 20px/30px "Open Sans", sans-serif;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
@ -595,14 +614,16 @@ a#previous_photo:hover, a#next_photo:hover {
|
|||
}
|
||||
#sub_photo #tag_list li {
|
||||
display: inline;
|
||||
padding-right: 0.75em;
|
||||
}
|
||||
#sub_photo #tag_list li:after {
|
||||
content: ', ';
|
||||
#tag_list .delete-tag {
|
||||
opacity: 0.25;
|
||||
}
|
||||
#sub_photo #tag_list li:last-child:after {
|
||||
content: '';
|
||||
#tag_list .delete-tag:hover {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
|
||||
#photo_exif_box {
|
||||
background: #fff;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
|
@ -626,6 +647,15 @@ a#previous_photo:hover, a#next_photo:hover {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
#user_actions_box {
|
||||
background: #fff;
|
||||
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
float: left;
|
||||
margin: 25px 0 25px 0;
|
||||
overflow: auto;
|
||||
padding: 2%;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
/* Responsive: smartphone in portrait
|
||||
---------------------------------------*/
|
||||
|
@ -668,13 +698,17 @@ a#previous_photo:hover, a#next_photo:hover {
|
|||
padding: 12px 4px;
|
||||
}
|
||||
|
||||
.album_title_box {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tiled_header {
|
||||
font-size: 14px;
|
||||
margin: 0 0 3.5% 0;
|
||||
}
|
||||
.tiled_grid div h4 {
|
||||
font-size: 14px;
|
||||
padding: 15px 5px 10px;
|
||||
padding: 15px 5px;
|
||||
}
|
||||
|
||||
.tiled_row > div, .tiled_row .single, .tiled_row .duo {
|
||||
|
|
|
@ -13,166 +13,165 @@ provided that the following conditions are met:
|
|||
|
||||
'use strict';
|
||||
|
||||
function AutoSuggest(opt) {
|
||||
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" || typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
||||
return;
|
||||
class AutoSuggest {
|
||||
constructor(opt) {
|
||||
if (typeof opt.inputElement === "undefined" || typeof opt.listElement === "undefined" ||
|
||||
typeof opt.baseUrl === "undefined" || typeof opt.appendCallback === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
this.input = document.getElementById(opt.inputElement);
|
||||
this.input.autocomplete = "off";
|
||||
this.list = document.getElementById(opt.listElement);
|
||||
this.appendCallback = opt.appendCallback;
|
||||
this.baseurl = opt.baseUrl;
|
||||
|
||||
this.input.addEventListener('keydown', event => this.doSelection(event), false);
|
||||
this.input.addEventListener('keyup', event => this.onType(event), false);
|
||||
}
|
||||
|
||||
this.input = document.getElementById(opt.inputElement);
|
||||
this.input.autocomplete = "off";
|
||||
this.list = document.getElementById(opt.listElement);
|
||||
this.appendCallback = opt.appendCallback;
|
||||
this.baseurl = opt.baseUrl;
|
||||
doSelection(event) {
|
||||
if (typeof this.container === "undefined" || this.container.children.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.input.addEventListener('keydown', function(event) {
|
||||
self.doSelection(event);
|
||||
}, false);
|
||||
this.input.addEventListener('keyup', function(event) {
|
||||
self.onType(this, event);
|
||||
}, false);
|
||||
switch (event.key) {
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
this.container.children[this.selectedIndex].click();
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
this.findSelectedElement().className = '';
|
||||
this.selectedIndex += event.key === 'ArrowUp' ? -1 : 1;
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = this.container.children.length - 1;
|
||||
} else if (this.selectedIndex === this.container.children.length) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
let new_el = this.findSelectedElement().className = 'selected';
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
findSelectedElement() {
|
||||
return this.container.children[this.selectedIndex];
|
||||
};
|
||||
|
||||
onType(event) {
|
||||
if (['Enter', 'ArrowDown', 'ArrowUp'].indexOf(event.key) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let tokens = event.target.value.split(/\s+/).filter(token => token.length >= 2);
|
||||
|
||||
if (tokens.length === 0) {
|
||||
if (typeof this.container !== "undefined") {
|
||||
this.clearContainer();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
let request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
||||
let request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
||||
};
|
||||
|
||||
onReceive(response, self) {
|
||||
self.openContainer();
|
||||
self.clearContainer();
|
||||
self.fillContainer(response);
|
||||
};
|
||||
|
||||
openContainer() {
|
||||
if (this.container) {
|
||||
if (!this.container.parentNode) {
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
}
|
||||
return this.container;
|
||||
}
|
||||
|
||||
this.container = document.createElement('ul');
|
||||
this.container.className = 'autosuggest';
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
return this.container;
|
||||
};
|
||||
|
||||
clearContainer() {
|
||||
while (this.container.children.length > 0) {
|
||||
this.container.removeChild(this.container.children[0]);
|
||||
}
|
||||
};
|
||||
|
||||
clearInput() {
|
||||
this.input.value = "";
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
closeContainer() {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
};
|
||||
|
||||
fillContainer(response) {
|
||||
this.selectedIndex = 0;
|
||||
|
||||
let query = this.input.value.trim().replace(/[\-\[\]{}()*+?.,\\\/^\$|#]/g, ' ');
|
||||
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.addEventListener('click', event => {
|
||||
this.appendCallback(event.target.jsondata);
|
||||
this.closeContainer();
|
||||
this.clearInput();
|
||||
});
|
||||
this.container.appendChild(node);
|
||||
if (this.container.children.length === 1) {
|
||||
node.className = 'selected';
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
highlightMatches(query_tokens, item) {
|
||||
let itemTokens = item.split(/ +/);
|
||||
let queryTokens = new RegExp('(' + query_tokens.join('\|') + ')', 'i');
|
||||
itemTokens.forEach((token, index) => {
|
||||
item = item.replace(token, token.replace(queryTokens, ($1, match) => '<strong>' + match + '</strong>'));
|
||||
});
|
||||
return item;
|
||||
};
|
||||
}
|
||||
|
||||
AutoSuggest.prototype.doSelection = function(event) {
|
||||
if (typeof this.container === "undefined" || this.container.children.length === 0) {
|
||||
return;
|
||||
class TagAutoSuggest extends AutoSuggest {
|
||||
constructor(opt) {
|
||||
super(opt);
|
||||
this.type = "tags";
|
||||
}
|
||||
|
||||
switch (event.keyCode) {
|
||||
case 13: // Enter
|
||||
event.preventDefault();
|
||||
this.container.children[this.selectedIndex].click();
|
||||
break;
|
||||
fillContainer(response) {
|
||||
if (response.items.length > 0) {
|
||||
super.fillContainer.call(this, response);
|
||||
} else {
|
||||
let node = document.createElement('li')
|
||||
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
|
||||
|
||||
case 38: // Arrow up
|
||||
case 40: // Arrow down
|
||||
event.preventDefault();
|
||||
this.findSelectedElement().className = '';
|
||||
this.selectedIndex += event.keyCode === 38 ? -1 : 1;
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = this.container.children.length - 1;
|
||||
} else if (this.selectedIndex === this.container.children.length) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
var new_el = this.findSelectedElement().className = 'selected';
|
||||
break;
|
||||
}
|
||||
};
|
||||
node.addEventListener('click', event => {
|
||||
this.createNewTag(response => this.appendCallback(response));
|
||||
this.closeContainer();
|
||||
this.clearInput();
|
||||
});
|
||||
|
||||
AutoSuggest.prototype.findSelectedElement = function() {
|
||||
return this.container.children[this.selectedIndex];
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.onType = function(input, event) {
|
||||
if (event.keyCode === 13 || event.keyCode === 38 || event.keyCode === 40) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tokens = input.value.split(/\s+/).filter(function(token) {
|
||||
return token.length >= 2;
|
||||
});
|
||||
|
||||
if (tokens.length === 0) {
|
||||
if (typeof this.container !== "undefined") {
|
||||
this.clearContainer();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
var request_uri = this.baseurl + '/suggest/?type=tags&data=' + window.encodeURIComponent(tokens.join(" "));
|
||||
var request = new HttpRequest('get', request_uri, {}, this.onReceive, this);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.onReceive = function(response, self) {
|
||||
self.openContainer();
|
||||
self.clearContainer();
|
||||
self.fillContainer(response);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.openContainer = function() {
|
||||
if (this.container) {
|
||||
if (!this.container.parentNode) {
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
}
|
||||
return this.container;
|
||||
}
|
||||
|
||||
this.container = document.createElement('ul');
|
||||
this.container.className = 'autosuggest';
|
||||
this.input.parentNode.appendChild(this.container);
|
||||
return this.container;
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.clearContainer = function() {
|
||||
while (this.container.children.length > 0) {
|
||||
this.container.removeChild(this.container.children[0]);
|
||||
}
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.clearInput = function() {
|
||||
this.input.value = "";
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.closeContainer = function() {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
};
|
||||
|
||||
AutoSuggest.prototype.fillContainer = function(response) {
|
||||
var self = this;
|
||||
this.selectedIndex = 0;
|
||||
response.items.forEach(function(item, i) {
|
||||
var node = document.createElement('li');
|
||||
var text = document.createTextNode(item.label);
|
||||
node.jsondata = item;
|
||||
node.addEventListener('click', function(event) {
|
||||
self.appendCallback(this.jsondata);
|
||||
self.closeContainer();
|
||||
self.clearInput();
|
||||
});
|
||||
node.appendChild(text);
|
||||
self.container.appendChild(node);
|
||||
if (self.container.children.length === 1) {
|
||||
this.container.appendChild(node);
|
||||
this.selectedIndex = 0;
|
||||
node.className = 'selected';
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function TagAutoSuggest(opt) {
|
||||
AutoSuggest.prototype.constructor.call(this, opt);
|
||||
this.type = "tags";
|
||||
}
|
||||
|
||||
TagAutoSuggest.prototype = Object.create(AutoSuggest.prototype);
|
||||
|
||||
TagAutoSuggest.prototype.constructor = TagAutoSuggest;
|
||||
|
||||
TagAutoSuggest.prototype.fillContainer = function(response) {
|
||||
if (response.items.length > 0) {
|
||||
AutoSuggest.prototype.fillContainer.call(this, response);
|
||||
} else {
|
||||
var node = document.createElement('li')
|
||||
node.innerHTML = "<em>Tag does not exist yet. Create it?</em>";
|
||||
|
||||
var self = this;
|
||||
node.addEventListener('click', function(event) {
|
||||
self.createNewTag(function(response) {
|
||||
console.log('Nieuwe tag!!');
|
||||
console.log(response);
|
||||
self.appendCallback(response);
|
||||
});
|
||||
self.closeContainer();
|
||||
self.clearInput();
|
||||
});
|
||||
|
||||
self.container.appendChild(node);
|
||||
this.selectedIndex = 0;
|
||||
node.className = 'selected';
|
||||
createNewTag(callback) {
|
||||
let request_uri = this.baseurl + '/suggest/?type=createtag';
|
||||
let request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
||||
}
|
||||
};
|
||||
|
||||
TagAutoSuggest.prototype.createNewTag = function(callback) {
|
||||
var request_uri = this.baseurl + '/suggest/?type=createtag';
|
||||
var request = new HttpRequest('post', request_uri, 'tag=' + encodeURIComponent(this.input.value), callback, this);
|
||||
}
|
||||
|
|
|
@ -1,218 +1,374 @@
|
|||
function CropEditor(opt) {
|
||||
this.opt = opt;
|
||||
class CropEditor {
|
||||
constructor(opt) {
|
||||
this.opt = opt;
|
||||
|
||||
this.edit_crop_button = document.createElement("span");
|
||||
this.edit_crop_button.className = "btn";
|
||||
this.edit_crop_button.innerHTML = "Edit crop";
|
||||
this.edit_crop_button.addEventListener('click', this.show.bind(this));
|
||||
this.edit_crop_button = document.createElement("span");
|
||||
this.edit_crop_button.className = "btn";
|
||||
this.edit_crop_button.textContent = "Edit crop";
|
||||
this.edit_crop_button.addEventListener('click', this.show.bind(this));
|
||||
|
||||
this.thumbnail_select = document.getElementById(opt.thumbnail_select_id);
|
||||
this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this));
|
||||
this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling);
|
||||
this.thumbnail_select = document.getElementById(opt.thumbnail_select_id);
|
||||
this.thumbnail_select.addEventListener('change', this.toggleCropButton.bind(this));
|
||||
this.thumbnail_select.parentNode.insertBefore(this.edit_crop_button, this.thumbnail_select.nextSibling);
|
||||
|
||||
this.toggleCropButton();
|
||||
}
|
||||
this.toggleCropButton();
|
||||
}
|
||||
|
||||
CropEditor.prototype.buildContainer = function() {
|
||||
this.container = document.createElement("div");
|
||||
this.container.id = "crop_editor";
|
||||
initDOM() {
|
||||
this.container = document.createElement("div");
|
||||
this.container.id = "crop_editor";
|
||||
|
||||
this.position = document.createElement("div");
|
||||
this.position.className = "crop_position";
|
||||
this.container.appendChild(this.position);
|
||||
this.initPositionForm();
|
||||
this.initImageContainer();
|
||||
|
||||
var source_x_label = document.createTextNode("Source X:");
|
||||
this.position.appendChild(source_x_label);
|
||||
this.parent = document.getElementById(this.opt.editor_container_parent_id);
|
||||
this.parent.appendChild(this.container);
|
||||
}
|
||||
|
||||
this.source_x = document.createElement("input");
|
||||
this.source_x.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.source_x);
|
||||
initPositionForm() {
|
||||
this.position = document.createElement("fieldset");
|
||||
this.position.className = "crop_position";
|
||||
this.container.appendChild(this.position);
|
||||
|
||||
var source_y_label = document.createTextNode("Source Y:");
|
||||
this.position.appendChild(source_y_label);
|
||||
let source_x_label = document.createTextNode("Source X:");
|
||||
this.position.appendChild(source_x_label);
|
||||
|
||||
this.source_y = document.createElement("input");
|
||||
this.source_y.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.source_y);
|
||||
this.source_x = document.createElement("input");
|
||||
this.source_x.type = 'number';
|
||||
this.source_x.addEventListener("change", this.positionBoundary.bind(this));
|
||||
this.source_x.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.source_x);
|
||||
|
||||
var crop_width_label = document.createTextNode("Crop width:");
|
||||
this.position.appendChild(crop_width_label);
|
||||
let source_y_label = document.createTextNode("Source Y:");
|
||||
this.position.appendChild(source_y_label);
|
||||
|
||||
this.crop_width = document.createElement("input");
|
||||
this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.crop_width);
|
||||
this.source_y = document.createElement("input");
|
||||
this.source_y.type = 'number';
|
||||
this.source_y.addEventListener("change", this.positionBoundary.bind(this));
|
||||
this.source_y.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.source_y);
|
||||
|
||||
var crop_height_label = document.createTextNode("Crop height:");
|
||||
this.position.appendChild(crop_height_label);
|
||||
let crop_width_label = document.createTextNode("Crop width:");
|
||||
this.position.appendChild(crop_width_label);
|
||||
|
||||
this.crop_height = document.createElement("input");
|
||||
this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.crop_height);
|
||||
this.crop_width = document.createElement("input");
|
||||
this.crop_width.type = 'number';
|
||||
this.crop_width.addEventListener("change", this.positionBoundary.bind(this));
|
||||
this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.crop_width);
|
||||
|
||||
this.save_button = document.createElement("span");
|
||||
this.save_button.className = "btn";
|
||||
this.save_button.innerHTML = "Save";
|
||||
this.save_button.addEventListener('click', this.save.bind(this));
|
||||
this.position.appendChild(this.save_button);
|
||||
let crop_height_label = document.createTextNode("Crop height:");
|
||||
this.position.appendChild(crop_height_label);
|
||||
|
||||
this.abort_button = document.createElement("span");
|
||||
this.abort_button.className = "btn btn-red";
|
||||
this.abort_button.innerHTML = "Abort";
|
||||
this.abort_button.addEventListener('click', this.hide.bind(this));
|
||||
this.position.appendChild(this.abort_button);
|
||||
this.crop_height = document.createElement("input");
|
||||
this.crop_height.type = 'number';
|
||||
this.crop_height.addEventListener("change", this.positionBoundary.bind(this));
|
||||
this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this));
|
||||
this.position.appendChild(this.crop_height);
|
||||
|
||||
this.image_container = document.createElement("div");
|
||||
this.image_container.className = "crop_image_container";
|
||||
this.container.appendChild(this.image_container);
|
||||
this.crop_constrain_label = document.createElement("label");
|
||||
this.position.appendChild(this.crop_constrain_label);
|
||||
|
||||
this.crop_boundary = document.createElement("div");
|
||||
this.crop_boundary.id = "crop_boundary";
|
||||
this.image_container.appendChild(this.crop_boundary);
|
||||
this.crop_constrain = document.createElement("input");
|
||||
this.crop_constrain.checked = true;
|
||||
this.crop_constrain.type = 'checkbox';
|
||||
this.crop_constrain_label.appendChild(this.crop_constrain);
|
||||
|
||||
this.original_image = document.createElement("img");
|
||||
this.original_image.id = "original_image";
|
||||
this.original_image.src = this.opt.original_image_src;
|
||||
this.image_container.appendChild(this.original_image);
|
||||
this.crop_constrain_text = document.createTextNode('Constrain proportions');
|
||||
this.crop_constrain_label.appendChild(this.crop_constrain_text);
|
||||
|
||||
this.parent = document.getElementById(this.opt.editor_container_parent_id);
|
||||
this.parent.appendChild(this.container);
|
||||
};
|
||||
this.save_button = document.createElement("span");
|
||||
this.save_button.className = "btn";
|
||||
this.save_button.textContent = "Save";
|
||||
this.save_button.addEventListener('click', this.save.bind(this));
|
||||
this.position.appendChild(this.save_button);
|
||||
|
||||
CropEditor.prototype.setInputValues = function() {
|
||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
this.abort_button = document.createElement("span");
|
||||
this.abort_button.className = "btn btn-red";
|
||||
this.abort_button.textContent = "Abort";
|
||||
this.abort_button.addEventListener('click', this.hide.bind(this));
|
||||
this.position.appendChild(this.abort_button);
|
||||
}
|
||||
|
||||
if (typeof current.crop_region === "undefined") {
|
||||
var source_ratio = this.original_image.naturalWidth / this.original_image.naturalHeight,
|
||||
crop_ratio = current.crop_width / current.crop_height,
|
||||
min_dim = Math.min(this.original_image.naturalWidth, this.original_image.naturalHeight);
|
||||
initImageContainer() {
|
||||
this.image_container = document.createElement("div");
|
||||
this.image_container.className = "crop_image_container";
|
||||
this.container.appendChild(this.image_container);
|
||||
|
||||
this.crop_boundary = document.createElement("div");
|
||||
this.crop_boundary.id = "crop_boundary";
|
||||
this.image_container.appendChild(this.crop_boundary);
|
||||
|
||||
this.original_image = document.createElement("img");
|
||||
this.original_image.draggable = false;
|
||||
this.original_image.id = "original_image";
|
||||
this.original_image.src = this.opt.original_image_src;
|
||||
this.image_container.appendChild(this.original_image);
|
||||
}
|
||||
|
||||
setDefaultCrop(cropAspectRatio, cropMethod) {
|
||||
let source = this.original_image;
|
||||
let sourceAspectRatio = source.naturalWidth / source.naturalHeight;
|
||||
|
||||
// Cropping from the centre?
|
||||
if (current.crop_method === "c") {
|
||||
if (cropMethod === "c" || cropMethod === "s") {
|
||||
// Crop vertically from the centre, using the entire width.
|
||||
if (source_ratio < crop_ratio) {
|
||||
this.crop_width.value = this.original_image.naturalWidth;
|
||||
this.crop_height.value = Math.ceil(this.original_image.naturalWidth / crop_ratio);
|
||||
if (sourceAspectRatio <= cropAspectRatio) {
|
||||
this.crop_width.value = source.naturalWidth;
|
||||
this.crop_height.value = Math.ceil(source.naturalWidth / cropAspectRatio);
|
||||
this.source_x.value = 0;
|
||||
this.source_y.value = Math.ceil((this.original_image.naturalHeight - this.crop_height.value) / 2);
|
||||
this.source_y.value = Math.ceil((source.naturalHeight - this.crop_height.value) / 2);
|
||||
}
|
||||
// Crop horizontally from the centre, using the entire height.
|
||||
else {
|
||||
this.crop_width.value = Math.ceil(current.crop_width * this.original_image.naturalHeight / current.crop_height);
|
||||
this.crop_height.value = this.original_image.naturalHeight;
|
||||
this.source_x.value = Math.ceil((this.original_image.naturalWidth - this.crop_width.value) / 2);
|
||||
this.crop_width.value = Math.ceil(cropAspectRatio * source.naturalHeight);
|
||||
this.crop_height.value = source.naturalHeight;
|
||||
this.source_x.value = Math.ceil((source.naturalWidth - this.crop_width.value) / 2);
|
||||
this.source_y.value = 0;
|
||||
}
|
||||
}
|
||||
// Cropping a top or bottom slice?
|
||||
else {
|
||||
// Can we actually take a top or bottom slice from the original image?
|
||||
if (source_ratio < crop_ratio) {
|
||||
this.crop_width.value = this.original_image.naturalWidth;
|
||||
this.crop_height.value = Math.floor(this.original_image.naturalHeight / crop_ratio);
|
||||
if (sourceAspectRatio <= cropAspectRatio) {
|
||||
this.crop_width.value = source.naturalWidth;
|
||||
this.crop_height.value = Math.floor(source.naturalWidth / cropAspectRatio);
|
||||
this.source_x.value = "0";
|
||||
this.source_y.value = current.crop_method.indexOf("t") !== -1 ? "0" : this.original_image.naturalHeight - this.crop_height.value;
|
||||
this.source_y.value = cropMethod.indexOf("t") !== -1 ? "0" : source.naturalHeight - this.crop_height.value;
|
||||
}
|
||||
// Otherwise, take a vertical slice from the centre.
|
||||
else {
|
||||
this.crop_width.value = Math.floor(this.original_image.naturalHeight * crop_ratio);
|
||||
this.crop_height.value = this.original_image.naturalHeight;
|
||||
this.source_x.value = Math.floor((this.original_image.naturalWidth - this.crop_width.value) / 2);
|
||||
this.crop_width.value = Math.floor(source.naturalHeight * cropAspectRatio);
|
||||
this.crop_height.value = source.naturalHeight;
|
||||
this.source_x.value = Math.floor((source.naturalWidth - this.crop_width.value) / 2);
|
||||
this.source_y.value = "0";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var region = current.crop_region.split(',');
|
||||
this.crop_width.value = region[0];
|
||||
this.crop_height.value = region[1];
|
||||
this.source_x.value = region[2];
|
||||
this.source_y.value = region[3];
|
||||
}
|
||||
};
|
||||
|
||||
CropEditor.prototype.showContainer = function() {
|
||||
this.container.style.display = "block";
|
||||
this.setInputValues();
|
||||
this.positionBoundary();
|
||||
}
|
||||
|
||||
CropEditor.prototype.save = function() {
|
||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
var payload = {
|
||||
thumb_width: current.crop_width,
|
||||
thumb_height: current.crop_height,
|
||||
crop_method: current.crop_method,
|
||||
crop_width: this.crop_width.value,
|
||||
crop_height: this.crop_height.value,
|
||||
source_x: this.source_x.value,
|
||||
source_y: this.source_y.value
|
||||
};
|
||||
var req = HttpRequest("post", this.parent.action + "?id=" + this.opt.asset_id + "&updatethumb",
|
||||
"data=" + encodeURIComponent(JSON.stringify(payload)), function(response) {
|
||||
this.opt.after_save(response);
|
||||
this.hide();
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
CropEditor.prototype.show = function() {
|
||||
if (typeof this.container === "undefined") {
|
||||
this.buildContainer();
|
||||
}
|
||||
|
||||
// Defer showing and positioning until image is loaded.
|
||||
// !!! TODO: add a spinner in the mean time?
|
||||
if (this.original_image.naturalWidth > 0) {
|
||||
this.showContainer();
|
||||
} else {
|
||||
this.original_image.addEventListener("load", function() {
|
||||
setPositionFormValues() {
|
||||
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
|
||||
if (typeof current.crop_region === "undefined") {
|
||||
let aspectRatio = current.crop_width / current.crop_height;
|
||||
this.setDefaultCrop(aspectRatio, current.crop_method);
|
||||
} else {
|
||||
let region = current.crop_region.split(',');
|
||||
this.crop_width.value = region[0];
|
||||
this.crop_height.value = region[1];
|
||||
this.source_x.value = region[2];
|
||||
this.source_y.value = region[3];
|
||||
}
|
||||
|
||||
this.crop_width.min = 1;
|
||||
this.crop_height.min = 1;
|
||||
this.source_x.min = 0;
|
||||
this.source_y.min = 0;
|
||||
|
||||
let source = this.original_image;
|
||||
this.crop_width.max = source.naturalWidth;
|
||||
this.crop_height.max = source.naturalHeight;
|
||||
this.source_x.max = source.naturalWidth - 1;
|
||||
this.source_y.max = source.naturalHeight - 1;
|
||||
|
||||
this.crop_constrain_text.textContent = `Constrain proportions (${current.crop_width} × ${current.crop_height})`;
|
||||
}
|
||||
|
||||
showContainer() {
|
||||
this.container.style.display = '';
|
||||
this.setPositionFormValues();
|
||||
this.positionBoundary();
|
||||
this.addEvents();
|
||||
}
|
||||
|
||||
save() {
|
||||
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
let payload = {
|
||||
thumb_width: current.crop_width,
|
||||
thumb_height: current.crop_height,
|
||||
crop_method: current.crop_method,
|
||||
crop_width: this.crop_width.value,
|
||||
crop_height: this.crop_height.value,
|
||||
source_x: this.source_x.value,
|
||||
source_y: this.source_y.value
|
||||
};
|
||||
let req = HttpRequest("post", this.opt.submitUrl + "?id=" + this.opt.asset_id + "&updatethumb",
|
||||
"data=" + encodeURIComponent(JSON.stringify(payload)), function(response) {
|
||||
this.opt.after_save(response);
|
||||
this.hide();
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
show() {
|
||||
if (typeof this.container === "undefined") {
|
||||
this.initDOM();
|
||||
}
|
||||
|
||||
// Defer showing and positioning until image is loaded.
|
||||
// !!! TODO: add a spinner in the mean time?
|
||||
if (this.original_image.naturalWidth > 0) {
|
||||
this.showContainer();
|
||||
}.bind(this));
|
||||
} else {
|
||||
this.original_image.addEventListener("load", event => this.showContainer());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
CropEditor.prototype.hide = function() {
|
||||
this.container.style.display = "none";
|
||||
};
|
||||
hide() {
|
||||
this.container.style.display = "none";
|
||||
}
|
||||
|
||||
CropEditor.prototype.addEvents = function(event) {
|
||||
var drag_target = document.getElementById(opt.drag_target);
|
||||
drag_target.addEventListener('dragstart', this.dragStart);
|
||||
drag_target.addEventListener('drag', this.drag);
|
||||
drag_target.addEventListener('dragend', this.dragEnd);
|
||||
};
|
||||
addEvents(event) {
|
||||
let cropTarget = this.image_container;
|
||||
cropTarget.addEventListener('mousedown', this.cropSelectionStart.bind(this));
|
||||
cropTarget.addEventListener('mousemove', this.cropSelection.bind(this));
|
||||
cropTarget.addEventListener('mouseup', this.cropSelectionEnd.bind(this));
|
||||
// cropTarget.addEventListener('mouseout', this.cropSelectionEnd.bind(this));
|
||||
|
||||
CropEditor.prototype.dragStart = function(event) {
|
||||
console.log(event);
|
||||
event.preventDefault();
|
||||
};
|
||||
this.original_image.addEventListener('mousedown', event => {return false});
|
||||
this.original_image.addEventListener('dragstart', event => {return false});
|
||||
|
||||
CropEditor.prototype.dragEnd = function(event) {
|
||||
console.log(event);
|
||||
};
|
||||
let moveTarget = this.crop_boundary;
|
||||
moveTarget.addEventListener('mousedown', this.moveSelectionStart.bind(this));
|
||||
moveTarget.addEventListener('mousemove', this.moveSelection.bind(this));
|
||||
moveTarget.addEventListener('mouseup', this.moveSelectionEnd.bind(this));
|
||||
|
||||
CropEditor.prototype.drag = function(event) {
|
||||
console.log(event);
|
||||
};
|
||||
window.addEventListener('resize', this.positionBoundary.bind(this));
|
||||
}
|
||||
|
||||
CropEditor.prototype.toggleCropButton = function() {
|
||||
var current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : "";
|
||||
};
|
||||
cropSelectionStart(event) {
|
||||
if (this.isMoving) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CropEditor.prototype.positionBoundary = function(event) {
|
||||
var source_x = parseInt(this.source_x.value),
|
||||
source_y = parseInt(this.source_y.value),
|
||||
crop_width = parseInt(this.crop_width.value),
|
||||
crop_height = parseInt(this.crop_height.value),
|
||||
real_width = this.original_image.naturalWidth,
|
||||
real_height = this.original_image.naturalHeight,
|
||||
scaled_width = this.original_image.clientWidth,
|
||||
scaled_height = this.original_image.clientHeight;
|
||||
let dragStartX = event.x - this.image_container.offsetLeft;
|
||||
let dragStartY = event.y - this.image_container.offsetTop;
|
||||
|
||||
var width_scale = scaled_width / real_width,
|
||||
height_scale = scaled_height / real_height;
|
||||
if (dragStartX > this.original_image.clientWidth ||
|
||||
dragStartY > this.original_image.clientHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
crop_boundary.style.left = (this.source_x.value) * width_scale + "px";
|
||||
crop_boundary.style.top = (this.source_y.value) * height_scale + "px";
|
||||
crop_boundary.style.width = (this.crop_width.value) * width_scale + "px";
|
||||
crop_boundary.style.height = (this.crop_height.value) * height_scale + "px";
|
||||
};
|
||||
this.isDragging = true;
|
||||
this.dragStartX = dragStartX;
|
||||
this.dragStartY = dragStartY;
|
||||
}
|
||||
|
||||
cropSelectionEnd(event) {
|
||||
this.isDragging = false;
|
||||
this.handleCropSelectionEvent(event);
|
||||
}
|
||||
|
||||
cropSelection(event) {
|
||||
this.handleCropSelectionEvent(event);
|
||||
}
|
||||
|
||||
getScaleFactor() {
|
||||
return this.original_image.naturalWidth / this.original_image.clientWidth;
|
||||
}
|
||||
|
||||
handleCropSelectionEvent(event) {
|
||||
if (!this.isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragEndX = event.x - this.image_container.offsetLeft;
|
||||
this.dragEndY = event.y - this.image_container.offsetTop;
|
||||
|
||||
let scaleFactor = this.getScaleFactor();
|
||||
|
||||
this.source_x.value = Math.ceil(Math.min(this.dragStartX, this.dragEndX) * scaleFactor);
|
||||
this.source_y.value = Math.ceil(Math.min(this.dragStartY, this.dragEndY) * scaleFactor);
|
||||
|
||||
let width = Math.ceil(Math.abs(this.dragEndX - this.dragStartX) * scaleFactor);
|
||||
this.crop_width.value = Math.min(width, this.original_image.naturalWidth - this.source_x.value);
|
||||
|
||||
let height = Math.ceil(Math.abs(this.dragEndY - this.dragStartY) * scaleFactor);
|
||||
this.crop_height.value = Math.min(height, this.original_image.naturalHeight - this.source_y.value);
|
||||
|
||||
if (this.crop_constrain.checked) {
|
||||
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
|
||||
let currentAspectRatio = parseInt(this.crop_width.value) / parseInt(this.crop_height.value);
|
||||
let targetAspectRatio = current.crop_width / current.crop_height;
|
||||
|
||||
if (Math.abs(currentAspectRatio - targetAspectRatio) > 0.001) {
|
||||
// Landscape?
|
||||
if (targetAspectRatio > 1.0) {
|
||||
let height = Math.ceil(this.crop_width.value / targetAspectRatio);
|
||||
if (parseInt(this.source_y.value) + height > this.original_image.naturalHeight) {
|
||||
height = this.original_image.naturalHeight - this.source_y.value;
|
||||
}
|
||||
this.crop_width.value = height * targetAspectRatio;
|
||||
this.crop_height.value = height;
|
||||
}
|
||||
// Portrait?
|
||||
else {
|
||||
let width = Math.ceil(this.crop_height.value * targetAspectRatio);
|
||||
if (parseInt(this.source_x.value) + width > this.original_image.naturalWidth) {
|
||||
width = this.original_image.naturalWidth - this.source_x.value;
|
||||
}
|
||||
this.crop_width.value = width;
|
||||
this.crop_height.value = width / targetAspectRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.positionBoundary();
|
||||
}
|
||||
|
||||
handleCropMoveEvent(event) {
|
||||
if (!this.isMoving) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dragEndX = event.x - this.crop_boundary.offsetLeft;
|
||||
this.dragEndY = event.y - this.crop_boundary.offsetTop;
|
||||
|
||||
let scaleFactor = this.getScaleFactor();
|
||||
|
||||
let x = parseInt(this.source_x.value) + Math.ceil((this.dragEndX - this.dragStartX) * scaleFactor);
|
||||
if (x + parseInt(this.crop_width.value) > this.original_image.naturalWidth) {
|
||||
x += this.original_image.naturalWidth - (x + parseInt(this.crop_width.value));
|
||||
}
|
||||
this.source_x.value = Math.max(x, 0);
|
||||
|
||||
let y = parseInt(this.source_y.value) + Math.ceil((this.dragEndY - this.dragStartY) * scaleFactor);
|
||||
if (y + parseInt(this.crop_height.value) > this.original_image.naturalHeight) {
|
||||
y += this.original_image.naturalHeight - (y + parseInt(this.crop_height.value));
|
||||
}
|
||||
this.source_y.value = Math.max(y, 0);
|
||||
|
||||
this.positionBoundary();
|
||||
}
|
||||
|
||||
moveSelectionStart(event) {
|
||||
if (this.isDragging) {
|
||||
return false;
|
||||
}
|
||||
this.isMoving = true;
|
||||
this.dragStartX = event.x - this.crop_boundary.offsetLeft;
|
||||
this.dragStartY = event.y - this.crop_boundary.offsetTop;
|
||||
}
|
||||
|
||||
moveSelectionEnd(event) {
|
||||
this.isMoving = false;
|
||||
this.handleCropMoveEvent(event);
|
||||
}
|
||||
|
||||
moveSelection(event) {
|
||||
this.handleCropMoveEvent(event);
|
||||
}
|
||||
|
||||
toggleCropButton() {
|
||||
let current = this.thumbnail_select.options[this.thumbnail_select.selectedIndex].dataset;
|
||||
this.edit_crop_button.style.display = typeof current.crop_method === "undefined" ? "none" : "";
|
||||
}
|
||||
|
||||
positionBoundary(event) {
|
||||
let scaleFactor = this.getScaleFactor();
|
||||
crop_boundary.style.left = parseInt(this.source_x.value) / scaleFactor + "px";
|
||||
crop_boundary.style.top = parseInt(this.source_y.value) / scaleFactor + "px";
|
||||
crop_boundary.style.width = parseInt(this.crop_width.value) / scaleFactor + "px";
|
||||
crop_boundary.style.height = parseInt(this.crop_height.value) / scaleFactor + "px";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,9 +45,8 @@ UploadQueue.prototype.addPreviewForFile = function(file, index, callback) {
|
|||
return false;
|
||||
}
|
||||
|
||||
var preview = document.createElement('img');
|
||||
var preview = document.createElement('canvas');
|
||||
preview.title = file.name;
|
||||
preview.style.maxHeight = '150px';
|
||||
|
||||
var preview_box = document.getElementById('upload_preview_' + index);
|
||||
preview_box.appendChild(preview);
|
||||
|
@ -55,12 +54,34 @@ UploadQueue.prototype.addPreviewForFile = function(file, index, callback) {
|
|||
var reader = new FileReader();
|
||||
var that = this;
|
||||
reader.addEventListener('load', function() {
|
||||
preview.src = reader.result;
|
||||
if (callback) {
|
||||
preview.addEventListener('load', function() {
|
||||
var original = document.createElement('img');
|
||||
original.src = reader.result;
|
||||
|
||||
original.addEventListener('load', function() {
|
||||
// Preparation: make canvas size proportional to the original image.
|
||||
preview.height = 150;
|
||||
preview.width = preview.height * (original.width / original.height);
|
||||
|
||||
// First pass: resize to 50% on temp canvas.
|
||||
var temp = document.createElement('canvas'),
|
||||
tempCtx = temp.getContext('2d');
|
||||
|
||||
temp.width = original.width * 0.5;
|
||||
temp.height = original.height * 0.5;
|
||||
tempCtx.drawImage(original, 0, 0, temp.width, temp.height);
|
||||
|
||||
// Second pass: resize again on temp canvas.
|
||||
tempCtx.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5);
|
||||
|
||||
// Final pass: resize to desired size on preview canvas.
|
||||
var context = preview.getContext('2d');
|
||||
context.drawImage(temp, 0, 0, temp.width * 0.5, temp.height * 0.5,
|
||||
0, 0, preview.width, preview.height);
|
||||
|
||||
if (callback) {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, false);
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
|
|
@ -15,6 +15,8 @@ class AdminBar extends SubTemplate
|
|||
echo '
|
||||
<div id="admin_bar">
|
||||
<ul>
|
||||
<li><a href="', BASEURL, '/managealbums/">Albums</a></li>
|
||||
<li><a href="', BASEURL, '/manageassets/">Assets</a></li>
|
||||
<li><a href="', BASEURL, '/managetags/">Tags</a></li>
|
||||
<li><a href="', BASEURL, '/manageusers/">Users</a></li>
|
||||
<li><a href="', BASEURL, '/manageerrors/">Errors [', ErrorLog::getCount(), ']</a></li>';
|
||||
|
|
|
@ -14,7 +14,7 @@ class AlbumIndex extends SubTemplate
|
|||
protected $row_limit = 1000;
|
||||
|
||||
const TILE_WIDTH = 400;
|
||||
const TILE_HEIGHT = 267;
|
||||
const TILE_HEIGHT = 300;
|
||||
|
||||
public function __construct(array $albums, $show_edit_buttons = false, $show_labels = true)
|
||||
{
|
||||
|
@ -35,12 +35,8 @@ class AlbumIndex extends SubTemplate
|
|||
|
||||
foreach ($photos as $album)
|
||||
{
|
||||
$color = isset($album['thumbnail']) ? $album['thumbnail']->bestColor() : 'ccc';
|
||||
if ($color == 'FFFFFF')
|
||||
$color = 'ccc';
|
||||
|
||||
echo '
|
||||
<div class="landscape" style="border-color: #', $color, '">';
|
||||
<div class="landscape">';
|
||||
|
||||
if ($this->show_edit_buttons)
|
||||
echo '
|
||||
|
|
|
@ -19,6 +19,13 @@ class Alert extends SubTemplate
|
|||
{
|
||||
echo '
|
||||
<div class="alert', $this->_type != 'alert' ? ' alert-' . $this->_type : '', '">', (!empty($this->_title) ? '
|
||||
<strong>' . $this->_title . '</strong><br>' : ''), $this->_message, '</div>';
|
||||
<strong>' . $this->_title . '</strong><br>' : ''), '<p>', $this->_message, '</p>';
|
||||
|
||||
$this->additional_alert_content();
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
protected function additional_alert_content()
|
||||
{}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* Button.php
|
||||
* Defines the Button template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class Button extends SubTemplate
|
||||
{
|
||||
private $content = '';
|
||||
private $href = '';
|
||||
private $class = '';
|
||||
|
||||
public function __construct($content = '', $href = '', $class = '')
|
||||
{
|
||||
$this->content = $content;
|
||||
$this->href = $href;
|
||||
$this->class = $class;
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
echo '
|
||||
<a class="', $this->class, '" href="', $this->href, '">', $this->content, '</a>';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* ConfirmDeletePage.php
|
||||
* Contains the confirm delete page template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2016, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class ConfirmDeletePage extends PhotoPage
|
||||
{
|
||||
public function __construct(Image $photo)
|
||||
{
|
||||
parent::__construct($photo);
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
$this->confirm();
|
||||
$this->photo();
|
||||
}
|
||||
|
||||
private function confirm()
|
||||
{
|
||||
$buttons = [];
|
||||
$buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '?delete_confirmed', "btn btn-red");
|
||||
$buttons[] = new Button("Cancel", $this->photo->getPageUrl(), "btn");
|
||||
|
||||
$alert = new WarningDialog(
|
||||
"Confirm deletion.",
|
||||
"You are about to permanently delete the following photo.",
|
||||
$buttons
|
||||
);
|
||||
$alert->html_content();
|
||||
}
|
||||
}
|
|
@ -23,7 +23,7 @@ class EditAssetForm extends SubTemplate
|
|||
<form id="asset_form" action="" method="post" enctype="multipart/form-data">
|
||||
<div class="boxed_content" style="margin-bottom: 2%">
|
||||
<div style="float: right">
|
||||
<a class="btn btn-red" href="', BASEURL, '/editasset/?id=', $this->asset->getId(), '&delete">Delete asset</a>
|
||||
<a class="btn btn-red" href="', BASEURL, '/', $this->asset->getSlug(), '?delete_confirmed">Delete asset</a>
|
||||
<input type="submit" value="Save asset data">
|
||||
</div>
|
||||
<h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2>
|
||||
|
@ -65,6 +65,9 @@ class EditAssetForm extends SubTemplate
|
|||
<dt>Title</dt>
|
||||
<dd><input type="text" name="title" maxlength="255" size="70" value="', $this->asset->getTitle(), '">
|
||||
|
||||
<dt>URL slug</dt>
|
||||
<dd><input type="text" name="slug" maxlength="255" size="70" value="', $this->asset->getSlug(), '">
|
||||
|
||||
<dt>Date captured</dt>
|
||||
<dd><input type="text" name="date_captured" size="30" value="',
|
||||
$date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '" placeholder="Y-m-d H:i:s">
|
||||
|
@ -135,10 +138,10 @@ class EditAssetForm extends SubTemplate
|
|||
<h3>Thumbnails</h3>
|
||||
View: <select id="thumbnail_src">';
|
||||
|
||||
foreach ($this->thumbs as $thumb)
|
||||
$first = INF;
|
||||
foreach ($this->thumbs as $i => $thumb)
|
||||
{
|
||||
if (!$thumb['status'])
|
||||
continue;
|
||||
$first = min($i, $first);
|
||||
|
||||
echo '
|
||||
<option data-url="', $thumb['url'], '" data-crop_width="', $thumb['dimensions'][0], '" data-crop_height="', $thumb['dimensions'][1], '"',
|
||||
|
@ -168,18 +171,16 @@ class EditAssetForm extends SubTemplate
|
|||
|
||||
echo '
|
||||
</select>
|
||||
<a id="thumbnail_link" href="', $this->thumbs[0]['url'], '" target="_blank">
|
||||
<img id="thumbnail" src="', $this->thumbs[0]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
|
||||
<a id="thumbnail_link" href="', $this->thumbs[$first]['url'], '" target="_blank">
|
||||
<img id="thumbnail" src="', $this->thumbs[$first]['url'], '" alt="Thumbnail" style="width: 100%; height: auto;">
|
||||
</a>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
document.getElementById("thumbnail_src").addEventListener("change", function(event) {
|
||||
var selection = event.target.options[event.target.selectedIndex];
|
||||
document.getElementById("thumbnail_link").href = selection.dataset.url;
|
||||
document.getElementById("thumbnail").src = selection.dataset.url;
|
||||
});
|
||||
}, 100);
|
||||
<script type="text/javascript" defer="defer">
|
||||
document.getElementById("thumbnail_src").addEventListener("change", event => {
|
||||
let selection = event.target.options[event.target.selectedIndex];
|
||||
document.getElementById("thumbnail_link").href = selection.dataset.url;
|
||||
document.getElementById("thumbnail").src = selection.dataset.url;
|
||||
});
|
||||
</script>';
|
||||
}
|
||||
|
||||
|
@ -190,26 +191,27 @@ class EditAssetForm extends SubTemplate
|
|||
|
||||
echo '
|
||||
<script type="text/javascript" src="', BASEURL, '/js/crop_editor.js"></script>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
var editor = new CropEditor({
|
||||
original_image_src: "', $this->asset->getUrl(), '",
|
||||
editor_container_parent_id: "asset_form",
|
||||
thumbnail_select_id: "thumbnail_src",
|
||||
drag_target: "drag_target",
|
||||
asset_id: ', $this->asset->getId(), ',
|
||||
after_save: function(data) {
|
||||
// Update thumbnail
|
||||
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
|
||||
<script type="text/javascript" defer="defer">
|
||||
let editor = new CropEditor({
|
||||
submit_url: "', BASEURL, '/editasset/",
|
||||
original_image_src: "', $this->asset->getUrl(), '",
|
||||
editor_container_parent_id: "asset_form",
|
||||
thumbnail_select_id: "thumbnail_src",
|
||||
drag_target: ".crop_image_container",
|
||||
asset_id: ', $this->asset->getId(), ',
|
||||
after_save: function(data) {
|
||||
// Update thumbnail
|
||||
document.getElementById("thumbnail").src = data.url + "?" + (new Date()).getTime();
|
||||
|
||||
// Update select
|
||||
var src = document.getElementById("thumbnail_src");
|
||||
src.options[src.selectedIndex].dataset.crop_region = data.value;
|
||||
// Update select
|
||||
let src = document.getElementById("thumbnail_src");
|
||||
let option = src.options[src.selectedIndex];
|
||||
option.dataset.crop_region = data.value;
|
||||
option.textContent = option.textContent.replace(/top|bottom|centre|slice/, "exact");
|
||||
|
||||
// TODO: update meta
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
// TODO: update meta
|
||||
}
|
||||
});
|
||||
</script>';
|
||||
}
|
||||
|
||||
|
@ -252,9 +254,6 @@ class EditAssetForm extends SubTemplate
|
|||
|
||||
foreach ($this->thumbs as $thumb)
|
||||
{
|
||||
if (!$thumb['status'])
|
||||
continue;
|
||||
|
||||
echo '
|
||||
<option value="thumb_', implode('x', $thumb['dimensions']);
|
||||
|
||||
|
@ -278,7 +277,7 @@ class EditAssetForm extends SubTemplate
|
|||
echo ' crop';
|
||||
}
|
||||
elseif ($thumb['custom_image'])
|
||||
echo ' (custom)';
|
||||
echo ', custom';
|
||||
|
||||
echo ')
|
||||
</option>';
|
||||
|
|
|
@ -24,10 +24,8 @@ class FormView extends SubTemplate
|
|||
{
|
||||
if (!empty($this->title))
|
||||
echo '
|
||||
<div id="journal_title">
|
||||
<h3>', $this->title, '</h3>
|
||||
</div>
|
||||
<div id="inner">';
|
||||
<div class="admin_box">
|
||||
<h2>', htmlspecialchars($this->title), '</h2>';
|
||||
|
||||
foreach ($this->_subtemplates as $template)
|
||||
$template->html_main();
|
||||
|
@ -134,7 +132,7 @@ class FormView extends SubTemplate
|
|||
|
||||
case 'numeric':
|
||||
echo '
|
||||
<input type="number"', isset($field['step']) ? ' step="' . $field['step'] . '"' : '', '" min="', isset($field['min_value']) ? $field['min_value'] : '0', '" max="', isset($field['max_value']) ? $field['max_value'] : '9999', '" name="', $field_id, '" id="', $field_id, '"', isset($field['size']) ? ' size="' . $field['size'] . '"' : '', isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', ' value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||
<input type="number"', isset($field['step']) ? ' step="' . $field['step'] . '"' : '', ' min="', isset($field['min_value']) ? $field['min_value'] : '0', '" max="', isset($field['max_value']) ? $field['max_value'] : '9999', '" name="', $field_id, '" id="', $field_id, '"', isset($field['size']) ? ' size="' . $field['size'] . '"' : '', isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', ' value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>';
|
||||
break;
|
||||
|
||||
case 'file':
|
||||
|
|
|
@ -8,10 +8,11 @@
|
|||
|
||||
class PhotoPage extends SubTemplate
|
||||
{
|
||||
private $photo;
|
||||
protected $photo;
|
||||
private $exif;
|
||||
private $previous_photo_url = '';
|
||||
private $next_photo_url = '';
|
||||
private $is_asset_owner = false;
|
||||
|
||||
public function __construct(Image $photo)
|
||||
{
|
||||
|
@ -28,6 +29,11 @@ class PhotoPage extends SubTemplate
|
|||
$this->next_photo_url = $url;
|
||||
}
|
||||
|
||||
public function setIsAssetOwner($flag)
|
||||
{
|
||||
$this->is_asset_owner = $flag;
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
{
|
||||
$this->photoNav();
|
||||
|
@ -45,11 +51,14 @@ class PhotoPage extends SubTemplate
|
|||
|
||||
$this->photoMeta();
|
||||
|
||||
if($this->is_asset_owner)
|
||||
$this->addUserActions();
|
||||
|
||||
echo '
|
||||
<script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>';
|
||||
}
|
||||
|
||||
private function photo()
|
||||
protected function photo()
|
||||
{
|
||||
echo '
|
||||
<div id="photo_frame">
|
||||
|
@ -139,8 +148,14 @@ class PhotoPage extends SubTemplate
|
|||
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 id="tag-', $tag->id_tag, '">
|
||||
<a rel="tag" title="View all posts tagged ', $tag->tag, '" href="', $tag->getUrl(), '" class="entry-tag">', $tag->tag, '</a>';
|
||||
|
||||
if ($tag->kind === 'Person')
|
||||
echo '
|
||||
<a class="delete-tag" title="Unlink this tag from this photo" href="#" data-id="', $tag->id_tag, '">❌</a>';
|
||||
|
||||
echo '
|
||||
</li>';
|
||||
}
|
||||
|
||||
|
@ -159,6 +174,25 @@ class PhotoPage extends SubTemplate
|
|||
<script type="text/javascript" src="', BASEURL, '/js/autosuggest.js"></script>
|
||||
<script type="text/javascript">
|
||||
setTimeout(function() {
|
||||
var removeTag = function(event) {
|
||||
event.preventDefault();
|
||||
var that = this;
|
||||
var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
|
||||
"id_tag=" + this.dataset["id"] + "&delete", function(response) {
|
||||
if (!response.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
var tagNode = document.getElementById("tag-" + that.dataset["id"]);
|
||||
tagNode.parentNode.removeChild(tagNode);
|
||||
});
|
||||
};
|
||||
|
||||
var tagRemovalTargets = document.getElementsByClassName("delete-tag");
|
||||
for (var i = 0; i < tagRemovalTargets.length; i++) {
|
||||
tagRemovalTargets[i].addEventListener("click", removeTag);
|
||||
}
|
||||
|
||||
var tag_autosuggest = new TagAutoSuggest({
|
||||
inputElement: "new_tag",
|
||||
listElement: "tag_list",
|
||||
|
@ -166,9 +200,25 @@ class PhotoPage extends SubTemplate
|
|||
appendCallback: function(item) {
|
||||
var request = new HttpRequest("post", "', $this->photo->getPageUrl(), '",
|
||||
"id_tag=" + item.id_tag, function(response) {
|
||||
var newNode = document.createElement("li");
|
||||
var newLink = document.createElement("a");
|
||||
newLink.href = item.url;
|
||||
|
||||
var newLabel = document.createTextNode(item.label);
|
||||
newNode.appendChild(newLabel);
|
||||
newLink.appendChild(newLabel);
|
||||
|
||||
var removeLink = document.createElement("a");
|
||||
removeLink.className = "delete-tag";
|
||||
removeLink.dataset["id"] = item.id_tag;
|
||||
removeLink.href = "#";
|
||||
removeLink.addEventListener("click", removeTag);
|
||||
|
||||
var crossmark = document.createTextNode("❌");
|
||||
removeLink.appendChild(crossmark);
|
||||
|
||||
var newNode = document.createElement("li");
|
||||
newNode.id = "tag-" + item.id_tag;
|
||||
newNode.appendChild(newLink);
|
||||
newNode.appendChild(removeLink);
|
||||
|
||||
var list = document.getElementById("tag_list");
|
||||
list.appendChild(newNode);
|
||||
|
@ -183,4 +233,13 @@ class PhotoPage extends SubTemplate
|
|||
{
|
||||
$this->exif = $exif;
|
||||
}
|
||||
|
||||
public function addUserActions()
|
||||
{
|
||||
echo '
|
||||
<div id=user_actions_box>
|
||||
<h3>Actions</h3>
|
||||
<a class="btn btn-red" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete</a>
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ class PhotosIndex extends SubTemplate
|
|||
const PANORAMA_HEIGHT = null;
|
||||
|
||||
const PORTRAIT_WIDTH = 400;
|
||||
const PORTRAIT_HEIGHT = 640;
|
||||
const PORTRAIT_HEIGHT = 645;
|
||||
|
||||
const LANDSCAPE_WIDTH = 850;
|
||||
const LANDSCAPE_HEIGHT = 640;
|
||||
|
@ -31,9 +31,9 @@ class PhotosIndex extends SubTemplate
|
|||
const SINGLE_HEIGHT = 412;
|
||||
|
||||
const TILE_WIDTH = 400;
|
||||
const TILE_HEIGHT = 267;
|
||||
const TILE_HEIGHT = 300;
|
||||
|
||||
public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = true, $show_headers = true)
|
||||
public function __construct(PhotoMosaic $mosaic, $show_edit_buttons = false, $show_labels = false, $show_headers = true)
|
||||
{
|
||||
$this->mosaic = $mosaic;
|
||||
$this->show_edit_buttons = $show_edit_buttons;
|
||||
|
@ -73,24 +73,18 @@ class PhotosIndex extends SubTemplate
|
|||
|
||||
$name = str_replace(' ', '', strtolower($header));
|
||||
echo '
|
||||
<div class="tiled_header" id="', $name, '">
|
||||
<h4 class="tiled_header" id="', $name, '">
|
||||
<a href="#', $name, '">', $header, '</a>
|
||||
</div>';
|
||||
</h4>';
|
||||
|
||||
$this->previous_header = $header;
|
||||
}
|
||||
|
||||
protected function color(Image $image)
|
||||
protected function photo(Image $image, $className, $width, $height, $crop = true, $fit = true)
|
||||
{
|
||||
$color = $image->bestColor();
|
||||
if ($color == 'FFFFFF')
|
||||
$color = 'ccc';
|
||||
echo '
|
||||
<div class="', $className, '">';
|
||||
|
||||
return $color;
|
||||
}
|
||||
|
||||
protected function photo(Image $image, $width, $height, $crop = true, $fit = true)
|
||||
{
|
||||
if ($this->show_edit_buttons)
|
||||
echo '
|
||||
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
|
||||
|
@ -104,21 +98,15 @@ class PhotosIndex extends SubTemplate
|
|||
<h4>', $image->getTitle(), '</h4>';
|
||||
|
||||
echo '
|
||||
</a>';
|
||||
|
||||
</a>
|
||||
</div>';
|
||||
}
|
||||
|
||||
protected function panorama(array $photos)
|
||||
{
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="panorama">';
|
||||
|
||||
$this->photo($image, static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
$this->photo($image, 'panorama', static::PANORAMA_WIDTH, static::PANORAMA_HEIGHT, false, false);
|
||||
}
|
||||
|
||||
protected function portrait(array $photos)
|
||||
|
@ -127,26 +115,16 @@ class PhotosIndex extends SubTemplate
|
|||
|
||||
echo '
|
||||
<div class="tiled_row">
|
||||
<div class="column_portrait">
|
||||
<div style="border-color: #', $this->color($image), '" class="portrait">';
|
||||
<div class="column_portrait">';
|
||||
|
||||
$this->photo($image, static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'top');
|
||||
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, 'centre');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>
|
||||
<div class="column_tiles_four">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'centre');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
|
@ -159,26 +137,16 @@ class PhotosIndex extends SubTemplate
|
|||
|
||||
echo '
|
||||
<div class="tiled_row">
|
||||
<div class="column_landscape">
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
<div class="column_landscape">';
|
||||
|
||||
$this->photo($image, static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
|
||||
$this->photo($image, 'landscape', static::LANDSCAPE_WIDTH, static::LANDSCAPE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>
|
||||
<div class="column_tiles_two">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
|
@ -191,15 +159,7 @@ class PhotosIndex extends SubTemplate
|
|||
<div class="tiled_row">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="duo">';
|
||||
|
||||
$this->photo($image, static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
$this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
|
@ -207,16 +167,13 @@ class PhotosIndex extends SubTemplate
|
|||
|
||||
protected function single(array $photos)
|
||||
{
|
||||
echo '
|
||||
<div class="tiled_row">';
|
||||
|
||||
$image = array_shift($photos);
|
||||
$this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
<div class="tiled_row">
|
||||
<div style="border-color: #', $this->color($image), '" class="single">';
|
||||
|
||||
$this->photo($image, static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top');
|
||||
|
||||
echo '
|
||||
</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
|
@ -226,15 +183,7 @@ class PhotosIndex extends SubTemplate
|
|||
<div class="tiled_row">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="landscape">';
|
||||
|
||||
$this->photo($image, static::TILE_WIDTH, static::TILE_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
$this->photo($image, 'landscape', static::TILE_WIDTH, static::TILE_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
|
@ -246,15 +195,7 @@ class PhotosIndex extends SubTemplate
|
|||
<div class="tiled_row">';
|
||||
|
||||
foreach ($photos as $image)
|
||||
{
|
||||
echo '
|
||||
<div style="border-color: #', $this->color($image), '" class="portrait">';
|
||||
|
||||
$this->photo($image, static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
}
|
||||
$this->photo($image, 'portrait', static::PORTRAIT_WIDTH, static::PORTRAIT_HEIGHT, true);
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
|
|
|
@ -6,12 +6,15 @@
|
|||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class TabularData extends Pagination
|
||||
class TabularData extends SubTemplate
|
||||
{
|
||||
public function __construct(GenericTable $table)
|
||||
{
|
||||
$this->_t = $table;
|
||||
parent::__construct($table);
|
||||
|
||||
$pageIndex = $table->getPageIndex();
|
||||
if ($pageIndex)
|
||||
$this->pager = new Pagination($pageIndex);
|
||||
}
|
||||
|
||||
protected function html_content()
|
||||
|
@ -25,7 +28,8 @@ class TabularData extends Pagination
|
|||
<h2>', $title, '</h2>';
|
||||
|
||||
// Showing a page index?
|
||||
parent::html_content();
|
||||
if (isset($this->pager))
|
||||
$this->pager->html_content();
|
||||
|
||||
// Maybe even a small form?
|
||||
if (isset($this->_t->form_above))
|
||||
|
@ -87,7 +91,8 @@ class TabularData extends Pagination
|
|||
$this->showForm($this->_t->form_below);
|
||||
|
||||
// Showing a page index?
|
||||
parent::html_content();
|
||||
if (isset($this->pager))
|
||||
$this->pager->html_content();
|
||||
|
||||
echo '
|
||||
</div>';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
abstract class Template
|
||||
{
|
||||
protected $_subtemplates = array();
|
||||
protected $_subtemplates = [];
|
||||
|
||||
abstract public function html_main();
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
/*****************************************************************************
|
||||
* WarningDialog.php
|
||||
* Defines the WarningDialog template.
|
||||
*
|
||||
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
|
||||
*****************************************************************************/
|
||||
|
||||
class WarningDialog extends Alert
|
||||
{
|
||||
protected $buttons;
|
||||
|
||||
public function __construct($title = '', $message = '', $buttons = [])
|
||||
{
|
||||
parent::__construct($title, $message);
|
||||
$this->buttons = $buttons;
|
||||
}
|
||||
|
||||
protected function additional_alert_content()
|
||||
{
|
||||
$this->addButtons();
|
||||
}
|
||||
|
||||
private function addButtons()
|
||||
{
|
||||
foreach ($this->buttons as $button)
|
||||
$button->html_content();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue