Compare commits

...

26 Commits

Author SHA1 Message Date
25feb31c1a EditAsset: some hardening; deduplicate redirect code 2024-01-18 13:40:17 +01:00
6ec5994de0 ViewPhotoAlbum: build edit menu in controller 2024-01-18 13:18:22 +01:00
24c2e9cdcf PhotosIndex: allow setting image as the album cover as well 2024-01-17 18:28:24 +01:00
0487ad16b9 Asset: remove old setKeyData method 2024-01-17 17:54:18 +01:00
c2aae4fb6e EditAsset: replace Asset::setKeyData with Asset::save equivalent 2024-01-17 17:54:14 +01:00
069d56383e PhotosIndex: replace edit button with edit menu 2024-01-17 17:51:45 +01:00
8613054d69 Asset: introduce save method 2024-01-17 17:51:25 +01:00
cf6adbf80c Merge pull request 'Allow users to filter albums by contributors' (#48) from refactor/viewalbum into master
Reviewed-on: #48
Reviewed-by: Bart Schuurmans <bart@minnozz.com>
2024-01-20 20:11:16 +01:00
30bc0bb884 ViewPhotoAlbum: don't include empty $by in page links 2024-01-15 13:44:51 +01:00
c0dd2cbd49 ViewPhotoAlbum: drop 'Show' from empty filter caption 2024-01-15 13:41:51 +01:00
bb81f7e086 Download: remove limits on maximum execution time 2024-01-15 11:46:01 +01:00
4b289a5e83 Download: allow limiting by user uploaded as well 2024-01-15 11:40:33 +01:00
ec2d702a0d ViewPhoto: simplify filter verification 2024-01-15 11:33:43 +01:00
52472d8b58 ViewPhotoAlbum: add 'label' key to empty filter as well 2024-01-15 11:26:17 +01:00
5d990501f6 ViewPhotoAlbum: move $is_person declaration to where it's used 2024-01-15 11:25:04 +01:00
1f53689e4b AlbumButtonBox: add visual cue to indicate a filter is active 2024-01-15 00:55:33 +01:00
accf093935 PageIndex: rewrite getLink to be way less messy 2024-01-15 00:51:06 +01:00
d8c3e76df6 ViewPhoto: take filter into account for prev/next links 2024-01-15 00:43:02 +01:00
f33a7e397c Asset: combine getUrlFor{Next,Previous}InSet into one private method 2024-01-15 00:19:39 +01:00
9c00248a7f ViewPhotoAlbum: don't populate filter box if there are no album contributors 2024-01-14 22:17:09 +01:00
99b867b241 AlbumButtonBox: add way for users to select an album filter 2024-01-14 21:28:45 +01:00
6a25ecec23 ViewPhotoAlbum: add method to filter by id_user_uploaded 2024-01-14 21:06:54 +01:00
16683d2f1f Tag: add getContributorList method 2024-01-14 21:06:34 +01:00
7cdcf8197c ViewPhotoAlbum: use Tag::getUrl instead of fumbling with $_GET['tag'] 2024-01-14 20:40:58 +01:00
25b9528628 ViewPhotoAlbum: simplify tag handling in getAlbumButtons 2024-01-14 20:40:58 +01:00
08cdbfe7b6 ViewPhotoAlbum: move some logic into new prepareHeaderBox method 2024-01-14 20:40:58 +01:00
14 changed files with 459 additions and 185 deletions

View File

@ -21,22 +21,30 @@ class Download
$tag = (int)$_GET['tag'];
$album = Tag::fromId($tag);
if (isset($_GET['by']) && ($user = Member::fromSlug($_GET['by'])) !== false)
$id_user_uploaded = $user->getUserId();
else
$id_user_uploaded = null;
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);
$this->exportAlbum($album, $id_user_uploaded);
exit;
}
private function exportAlbum(Tag $album)
private function exportAlbum(Tag $album, $id_user_uploaded)
{
$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]);
$iterator = AssetIterator::getByOptions([
'id_tag' => $album_id,
'id_user_uploaded' => $id_user_uploaded,
]);
while ($asset = $iterator->next())
$files[] = join(DIRECTORY_SEPARATOR, [$asset->getSubdir(), $asset->getFilename()]);
}
@ -71,6 +79,9 @@ class Download
// STDOUT should not block.
stream_set_blocking($pipes[1], 0);
// Allow this the download to take its time...
set_time_limit(0);
header('Pragma: no-cache');
header('Content-Description: File Download');
header('Content-disposition: attachment; filename="' . $album->tag . '.tar"');

View File

@ -30,6 +30,40 @@ class EditAsset extends HTMLController
header('Location: ' . $redirectUrl);
exit;
}
else
{
$isPrioChange = isset($_REQUEST['inc_prio']) || isset($_REQUEST['dec_prio']);
$isCoverChange = isset($_REQUEST['album_cover'], $_REQUEST['in']);
$madeChanges = false;
if ($user->isAdmin() && $isPrioChange && Session::validateSession('get'))
{
if (isset($_REQUEST['inc_prio']))
$priority = $asset->priority + 1;
else
$priority = $asset->priority - 1;
$asset->priority = max(0, min(100, $priority));
$asset->save();
$madeChanges = true;
}
elseif ($user->isAdmin() && $isCoverChange && Session::validateSession('get'))
{
$tag = Tag::fromId($_REQUEST['in']);
$tag->id_asset_thumb = $asset->getId();
$tag->save();
$madeChanges = true;
}
if ($madeChanges)
{
if (isset($_SERVER['HTTP_REFERER']))
header('Location: ' . $_SERVER['HTTP_REFERER']);
else
header('Location: ' . BASEURL . '/' . $asset->getSubdir());
exit;
}
}
// Get a list of available photo albums
$allAlbums = [];
@ -61,10 +95,12 @@ class EditAsset extends HTMLController
// Key info
if (isset($_POST['title'], $_POST['slug'], $_POST['date_captured'], $_POST['priority']))
{
$date_captured = !empty($_POST['date_captured']) ?
$asset->date_captured = !empty($_POST['date_captured']) ?
new DateTime(str_replace('T', ' ', $_POST['date_captured'])) : null;
$slug = Asset::cleanSlug($_POST['slug']);
$asset->setKeyData(htmlspecialchars($_POST['title']), $slug, $date_captured, intval($_POST['priority']));
$asset->slug = Asset::cleanSlug($_POST['slug']);
$asset->title = htmlspecialchars($_POST['title']);
$asset->priority = intval($_POST['priority']);
$asset->save();
}
// Changing parent album?

View File

@ -46,6 +46,18 @@ class ViewPhoto extends HTMLController
if (isset($tag))
$page->setTag($tag);
// Keeping tabs on a filter?
if (isset($_GET['by']))
{
// Let's first verify that the filter is valid
$user = Member::fromSlug($_GET['by']);
if (!$user)
throw new UnexpectedValueException('Invalid filter for this album or tag.');
// Alright, let's run with it then
$page->setActiveFilter($user->getSlug());
}
$this->page->adopt($page);
$this->page->setCanonicalUrl($this->photo->getPageUrl());
}

View File

@ -26,60 +26,92 @@ class ViewPhotoAlbum extends HTMLController
$tag = Tag::fromSlug($_GET['tag']);
$id_tag = $tag->id_tag;
$title = $tag->tag;
$description = !empty($tag->description) ? $tag->description : '';
// Can we go up a level?
if ($tag->id_parent != 0)
{
$ptag = Tag::fromId($tag->id_parent);
$back_link = BASEURL . '/' . (!empty($ptag->slug) ? $ptag->slug . '/' : '');
$back_link_title = 'Back to &quot;' . $ptag->tag . '&quot;';
}
elseif ($tag->kind === 'Person')
{
$back_link = BASEURL . '/people/';
$back_link_title = 'Back to &quot;People&quot;';
$is_person = true;
}
$header_box = new AlbumHeaderBox($title, $description, $back_link, $back_link_title);
$header_box = $this->getHeaderBox($tag);
}
// View the album root.
else
{
$id_tag = 1;
$tag = Tag::fromId($id_tag);
$title = 'Albums';
}
// What page are we at?
$page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
$current_page = isset($_GET['page']) ? (int) $_GET['page'] : 1;
parent::__construct($title . ' - Page ' . $page . ' - ' . SITE_TITLE);
parent::__construct($title . ' - Page ' . $current_page . ' - ' . SITE_TITLE);
if (isset($header_box))
$this->page->adopt($header_box);
// Can we do fancy things here?
// !!! TODO: permission system?
$buttons = $this->getAlbumButtons($id_tag, $tag ?? null);
if (!empty($buttons))
$this->page->adopt(new AlbumButtonBox($buttons));
// Who contributed to this album?
$contributors = $tag->getContributorList();
// Enumerate possible filters
$filters = [];
if (!empty($contributors))
{
$filters[''] = ['id_user' => null, 'label' => '', 'caption' => 'All photos',
'link' => $tag->getUrl()];
foreach ($contributors as $contributor)
{
$filters[$contributor['slug']] = [
'id_user' => $contributor['id_user'],
'label' => $contributor['first_name'],
'caption' => sprintf('By %s (%s photos)',
$contributor['first_name'], $contributor['num_assets']),
'link' => $tag->getUrl() . '?by=' . $contributor['slug'],
];
}
}
// Limit to a particular uploader?
$active_filter = '';
$id_user_uploaded = null;
if (!empty($_GET['by']))
{
if (!isset($filters[$_GET['by']]))
throw new UnexpectedValueException('Invalid filter for this album or tag.');
$active_filter = $_GET['by'];
$id_user_uploaded = $filters[$active_filter]['id_user'];
$filters[$active_filter]['is_active'] = true;
}
// Add an interface to query and modify the album/tag
$buttons = $this->getAlbumButtons($tag, $active_filter);
$button_strip = new AlbumButtonBox($buttons, $filters, $active_filter);
$this->page->adopt($button_strip);
// Fetch subalbums, but only if we're on the first page.
if ($page === 1)
if ($current_page === 1)
{
$albums = $this->getAlbums($id_tag);
$index = new AlbumIndex($albums);
$this->page->adopt($index);
}
// Are we viewing a person tag?
$is_person = $tag->kind === 'Person';
// Load a photo mosaic for the current tag.
list($mosaic, $total_count) = $this->getPhotoMosaic($id_tag, $page, !isset($is_person));
list($mosaic, $total_count) = $this->getPhotoMosaic($id_tag, $id_user_uploaded, $current_page, !$is_person);
if (isset($mosaic))
{
$index = new PhotosIndex($mosaic, Registry::get('user')->isAdmin());
$this->page->adopt($index);
if ($id_tag > 1)
$index->setUrlSuffix('?in=' . $id_tag);
$url_params = [];
if (isset($tag) && $tag->id_parent != 0)
$url_params['in'] = $tag->id_tag;
if (!empty($active_filter))
$url_params['by'] = $active_filter;
$url_suffix = http_build_query($url_params);
$index->setUrlSuffix('?' . $url_suffix);
$menu_items = $this->getEditMenuItems('&' . $url_suffix);
$index->setEditMenuItems($menu_items);
}
// Make a page index as needed, while we're at it.
@ -88,24 +120,24 @@ class ViewPhotoAlbum extends HTMLController
$index = new PageIndex([
'recordCount' => $total_count,
'items_per_page' => self::PER_PAGE,
'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE,
'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''),
'page_slug' => 'page/%PAGE%/',
'start' => ($current_page - 1) * self::PER_PAGE,
'base_url' => $tag->getUrl(),
'page_slug' => 'page/%PAGE%/' . (!empty($active_filter) ? '?by=' . $active_filter : ''),
'index_class' => 'pagination-lg justify-content-around justify-content-lg-center',
]);
$this->page->adopt(new PageIndexWidget($index));
}
// Set the canonical url.
$this->page->setCanonicalUrl(BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : '') .
($page > 1 ? 'page/' . $page . '/' : ''));
$this->page->setCanonicalUrl($tag->getUrl() . ($current_page > 1 ? 'page/' . $current_page . '/' : ''));
}
public function getPhotoMosaic($id_tag, $page, $sort_linear)
public function getPhotoMosaic($id_tag, $id_user_uploaded, $page, $sort_linear)
{
// Create an iterator.
list($this->iterator, $total_count) = AssetIterator::getByOptions([
'id_tag' => $id_tag,
'id_user_uploaded' => $id_user_uploaded,
'order' => 'date_captured',
'direction' => $sort_linear ? 'asc' : 'desc',
'limit' => self::PER_PAGE,
@ -145,25 +177,26 @@ class ViewPhotoAlbum extends HTMLController
return $albums;
}
private function getAlbumButtons($id_tag, $tag)
private function getAlbumButtons(Tag $tag, $active_filter)
{
$buttons = [];
$user = Registry::get('user');
if ($user->isLoggedIn())
{
$suffix = !empty($active_filter) ? '&by=' . $active_filter : '';
$buttons[] = [
'url' => BASEURL . '/download/?tag=' . $id_tag,
'url' => BASEURL . '/download/?tag=' . $tag->id_tag . $suffix,
'caption' => 'Download album',
];
}
if (isset($tag))
if ($tag->id_parent != 0)
{
if ($tag->kind === 'Album')
{
$buttons[] = [
'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag,
'url' => BASEURL . '/uploadmedia/?tag=' . $tag->id_tag,
'caption' => 'Upload photos here',
];
}
@ -173,14 +206,14 @@ class ViewPhotoAlbum extends HTMLController
if ($tag->kind === 'Album')
{
$buttons[] = [
'url' => BASEURL . '/editalbum/?id=' . $id_tag,
'url' => BASEURL . '/editalbum/?id=' . $tag->id_tag,
'caption' => 'Edit album',
];
}
elseif ($tag->kind === 'Person')
{
$buttons[] = [
'url' => BASEURL . '/edittag/?id=' . $id_tag,
'url' => BASEURL . '/edittag/?id=' . $tag->id_tag,
'caption' => 'Edit tag',
];
}
@ -190,7 +223,7 @@ class ViewPhotoAlbum extends HTMLController
if ($user->isAdmin() && (!isset($tag) || $tag->kind === 'Album'))
{
$buttons[] = [
'url' => BASEURL . '/addalbum/?tag=' . $id_tag,
'url' => BASEURL . '/addalbum/?tag=' . $tag->id_tag,
'caption' => 'Create subalbum',
];
}
@ -198,6 +231,65 @@ class ViewPhotoAlbum extends HTMLController
return $buttons;
}
private function getEditMenuItems($url_suffix)
{
$items = [];
$sess = '&' . Session::getSessionTokenKey() . '=' . Session::getSessionToken();
if (Registry::get('user')->isLoggedIn())
{
$items[] = [
'label' => 'Edit image',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix,
];
$items[] = [
'label' => 'Delete image',
'uri' => fn($image) => $image->getDeleteUrl() . $url_suffix . $sess,
'onclick' => 'return confirm(\'Are you sure you want to delete this image?\');',
];
}
if (Registry::get('user')->isAdmin())
{
$items[] = [
'label' => 'Make album cover',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix . '&album_cover' . $sess,
];
$items[] = [
'label' => 'Increase priority',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix . '&inc_prio' . $sess,
];
$items[] = [
'label' => 'Decrease priority',
'uri' => fn($image) => $image->getEditUrl() . $url_suffix . '&dec_prio' . $sess,
];
}
return $items;
}
private function getHeaderBox(Tag $tag)
{
// Can we go up a level?
if ($tag->id_parent != 0)
{
$ptag = Tag::fromId($tag->id_parent);
$back_link = BASEURL . '/' . (!empty($ptag->slug) ? $ptag->slug . '/' : '');
$back_link_title = 'Back to &quot;' . $ptag->tag . '&quot;';
}
elseif ($tag->kind === 'Person')
{
$back_link = BASEURL . '/people/';
$back_link_title = 'Back to &quot;People&quot;';
}
$description = !empty($tag->description) ? $tag->description : '';
return new AlbumHeaderBox($tag->tag, $description, $back_link, $back_link_title);
}
public function __destruct()
{
if (isset($this->iterator))

View File

@ -8,17 +8,17 @@
class Asset
{
protected $id_asset;
protected $id_user_uploaded;
protected $subdir;
protected $filename;
protected $title;
protected $slug;
protected $mimetype;
protected $image_width;
protected $image_height;
protected $date_captured;
protected $priority;
public $id_asset;
public $id_user_uploaded;
public $subdir;
public $filename;
public $title;
public $slug;
public $mimetype;
public $image_width;
public $image_height;
public $date_captured;
public $priority;
protected $meta;
protected $tags;
@ -36,6 +36,11 @@ class Asset
$this->date_captured = new DateTime($data['date_captured']);
}
public function canBeEditedBy(User $user)
{
return $this->isOwnedBy($user) || $user->isAdmin();
}
public static function cleanSlug($slug)
{
// Only alphanumerical chars, underscores and forward slashes are allowed
@ -675,87 +680,101 @@ class Asset
FROM assets');
}
public function setKeyData($title, $slug, DateTime $date_captured = null, $priority)
public function save()
{
$params = [
'id_asset' => $this->id_asset,
'title' => $title,
'slug' => $slug,
'priority' => $priority,
];
if (isset($date_captured))
$params['date_captured'] = $date_captured->format('Y-m-d H:i:s');
if (empty($this->id_asset))
throw new UnexpectedValueException();
return Registry::get('db')->query('
UPDATE assets
SET title = {string:title},
slug = {string:slug},' . (isset($date_captured) ? '
date_captured = {datetime:date_captured},' : '') . '
SET id_asset = {int:id_asset},
id_user_uploaded = {int:id_user_uploaded},
subdir = {string:subdir},
filename = {string:filename},
title = {string:title},
slug = {string:slug},
mimetype = {string:mimetype},
image_width = {int:image_width},
image_height = {int:image_height},
date_captured = {datetime:date_captured},
priority = {int:priority}
WHERE id_asset = {int:id_asset}',
get_object_vars($this));
}
protected function getUrlForAdjacentInSet($prevNext, ?Tag $tag, $activeFilter)
{
$next = $prevNext === 'next';
$previous = !$next;
$where = [];
$params = [
'id_asset' => $this->id_asset,
'date_captured' => $this->date_captured,
];
// Direction depends on whether we're browsing a tag or timeline
if (isset($tag))
{
$where[] = 't.id_tag = {int:id_tag}';
$params['id_tag'] = $tag->id_tag;
$params['where_op'] = $previous ? '<' : '>';
$params['order_dir'] = $previous ? 'DESC' : 'ASC';
}
else
{
$params['where_op'] = $previous ? '>' : '<';
$params['order_dir'] = $previous ? 'ASC' : 'DESC';
}
// Take active filter into account as well
if (!empty($activeFilter) && ($user = Member::fromSlug($activeFilter)) !== false)
{
$where[] = 'id_user_uploaded = {int:id_user_uploaded}';
$params['id_user_uploaded'] = $user->getUserId();
}
// Use complete ordering when sorting the set
$where[] = '(a.date_captured, a.id_asset) {raw:where_op} ' .
'({datetime:date_captured}, {int:id_asset})';
// Stringify conditions together
$where = '(' . implode(') AND (', $where) . ')';
// Run query, leaving out tags table if not required
$row = Registry::get('db')->queryAssoc('
SELECT a.*
FROM assets AS a
' . (isset($tag) ? '
INNER JOIN assets_tags AS t ON a.id_asset = t.id_asset' : '') . '
WHERE ' . $where . '
ORDER BY a.date_captured {raw:order_dir}, a.id_asset {raw:order_dir}
LIMIT 1',
$params);
if (!$row)
return false;
$obj = self::byRow($row, 'object');
$urlParams = [];
if (isset($tag))
$urlParams['in'] = $tag->id_tag;
if (!empty($activeFilter))
$urlParams['by'] = $activeFilter;
$queryString = !empty($urlParams) ? '?' . http_build_query($urlParams) : '';
return $obj->getPageUrl() . $queryString;
}
public function getUrlForPreviousInSet(?Tag $tag)
public function getUrlForPreviousInSet(?Tag $tag, $activeFilter)
{
$row = Registry::get('db')->queryAssoc('
SELECT a.*
' . (isset($tag) ? '
FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND
(a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
ORDER BY a.date_captured DESC, a.id_asset DESC'
: '
FROM assets AS a
WHERE (a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
ORDER BY date_captured ASC, a.id_asset ASC')
. '
LIMIT 1',
[
'id_asset' => $this->id_asset,
'id_tag' => isset($tag) ? $tag->id_tag : null,
'date_captured' => $this->date_captured,
]);
if ($row)
{
$obj = self::byRow($row, 'object');
return $obj->getPageUrl() . ($tag ? '?in=' . $tag->id_tag : '');
}
else
return false;
return $this->getUrlForAdjacentInSet('previous', $tag, $activeFilter);
}
public function getUrlForNextInSet(?Tag $tag)
public function getUrlForNextInSet(?Tag $tag, $activeFilter)
{
$row = Registry::get('db')->queryAssoc('
SELECT a.*
' . (isset($tag) ? '
FROM assets_tags AS t
INNER JOIN assets AS a ON a.id_asset = t.id_asset
WHERE t.id_tag = {int:id_tag} AND
(a.date_captured, a.id_asset) > ({datetime:date_captured}, {int:id_asset})
ORDER BY a.date_captured ASC, a.id_asset ASC'
: '
FROM assets AS a
WHERE (a.date_captured, a.id_asset) < ({datetime:date_captured}, {int:id_asset})
ORDER BY date_captured DESC, a.id_asset DESC')
. '
LIMIT 1',
[
'id_asset' => $this->id_asset,
'id_tag' => isset($tag) ? $tag->id_tag : null,
'date_captured' => $this->date_captured,
]);
if ($row)
{
$obj = self::byRow($row, 'object');
return $obj->getPageUrl() . ($tag ? '?in=' . $tag->id_tag : '');
}
else
return false;
return $this->getUrlForAdjacentInSet('next', $tag, $activeFilter);
}
}

View File

@ -118,6 +118,11 @@ class AssetIterator extends Asset
else
$where[] = 'a.mimetype = {string:mime_type}';
}
if (isset($options['id_user_uploaded']))
{
$params['id_user_uploaded'] = $options['id_user_uploaded'];
$where[] = 'id_user_uploaded = {int:id_user_uploaded}';
}
if (isset($options['id_tag']))
{
$params['id_tag'] = $options['id_tag'];

View File

@ -155,24 +155,20 @@ class PageIndex
public function getLink($start = null, $order = null, $dir = null)
{
$url = $this->base_url;
$amp = strpos($this->base_url, '?') ? '&' : '?';
$page = !is_string($start) ? ($start / $this->items_per_page) + 1 : $start;
$url = $this->base_url . str_replace('%PAGE%', $page, $this->page_slug);
if (!empty($start))
{
$page = $start !== '%d' ? ($start / $this->items_per_page) + 1 : $start;
$url .= strtr($this->page_slug, ['%PAGE%' => $page, '%AMP%' => $amp]);
$amp = '&';
}
$urlParams = [];
if (!empty($order))
{
$url .= $amp . 'order=' . $order;
$amp = '&';
}
$urlParams['order'] = $order;
if (!empty($dir))
$urlParams['dir'] = $dir;
if (!empty($urlParams))
{
$url .= $amp . 'dir=' . $dir;
$amp = '&';
$queryString = (strpos($uri, '?') !== false ? '&' : '?');
$queryString .= http_build_query($urlParams);
$url .= $queryString;
}
return $url;

View File

@ -141,6 +141,21 @@ class Tag
return $rows;
}
public function getContributorList()
{
return Registry::get('db')->queryPairs('
SELECT u.id_user, u.first_name, u.surname, u.slug, COUNT(*) AS num_assets
FROM assets_tags AS at
LEFT JOIN assets AS a ON at.id_asset = a.id_asset
LEFT JOIN users AS u ON a.id_user_uploaded = u.id_user
WHERE at.id_tag = {int:id_tag}
GROUP BY a.id_user_uploaded
ORDER BY u.first_name, u.surname',
[
'id_tag' => $this->id_tag,
]);
}
public static function getPeople($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array')
{
$rows = Registry::get('db')->queryAssocs('

View File

@ -75,6 +75,11 @@ abstract class User
return $this->ip_address;
}
public function getSlug()
{
return $this->slug;
}
/**
* Returns whether user is logged in.
*/

View File

@ -1,27 +1,3 @@
/* Edit icon on tiled grids
-----------------------------*/
.polaroid {
position: relative;
}
.polaroid a.edit {
background: var(--bs-body-bg);
border-radius: 3px;
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
color: var(--bs-body-color);
opacity: 0;
left: 20px;
line-height: 1.5;
padding: 5px 10px;
position: absolute;
transition: 0.25s;
top: 20px;
z-index: 50;
}
.polaroid:hover > a.edit {
opacity: 1;
}
/* Crop editor
----------------*/
#crop_editor {

View File

@ -296,6 +296,34 @@ div.polaroid a {
}
/* Edit icon on tiled grids
-----------------------------*/
.polaroid {
position: relative;
}
.polaroid div.edit {
box-shadow: 1px 1px 2px rgba(0,0,0,0.3);
opacity: 0;
left: 20px;
position: absolute;
transition: 0.25s;
top: 20px;
z-index: 50;
}
.polaroid div.edit .dropdown-item {
line-height: 1.4;
}
.polaroid div.edit .dropdown-toggle {
line-height: 1.4;
padding: 0.25rem 0.5rem;
}
.polaroid div.edit .dropdown-toggle::after {
margin-left: 0;
}
.polaroid:hover > div.edit {
opacity: 1;
}
/* Album title boxes
----------------------*/
@ -339,9 +367,11 @@ div.polaroid a {
---------------------*/
.album_button_box {
float: right;
display: flex;
margin-bottom: 3rem;
}
.album_button_box > a {
.album_button_box > a,
.album_button_box .btn {
background: var(--bs-body-bg);
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
border-color: var( --bs-secondary-bg);
@ -349,7 +379,9 @@ div.polaroid a {
padding: 8px 10px;
margin-left: 12px;
}
.album_button_box > a:hover {
.album_button_box > a:hover,
.album_button_box .btn:hover,
.album_button_box .btn:focus {
background: var(--bs-secondary-bg);
border-color: var(--bs-tertiary-bg);
color: var(--bs-secondary-color);

View File

@ -8,11 +8,15 @@
class AlbumButtonBox extends Template
{
private $active_filter;
private $buttons;
private $filters;
public function __construct($buttons)
public function __construct(array $buttons, array $filters, $active_filter)
{
$this->active_filter = $active_filter;
$this->buttons = $buttons;
$this->filters = $filters;
}
public function html_main()
@ -24,6 +28,39 @@ class AlbumButtonBox extends Template
echo '
<a class="btn btn-light" href="', $button['url'], '">', $button['caption'], '</a>';
if (!empty($this->filters))
{
echo '
<div class="dropdown">
<button class="btn btn-light dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-filter"></i>';
if ($this->active_filter)
{
echo '
<span class="badge text-bg-danger">',
$this->filters[$this->active_filter]['label'], '</span>';
}
echo '
</button>
<ul class="dropdown-menu">';
foreach ($this->filters as $key => $filter)
{
$is_active = $key === $this->active_filter;
echo '
<li><a class="dropdown-item', $is_active ? ' active' : '',
'" href="', $filter['link'], '">',
$filter['caption'],
'</a></li>';
}
echo '
</ul>
</div>';
}
echo '
</div>';
}

View File

@ -8,6 +8,7 @@
class PhotoPage extends Template
{
private $activeFilter;
private $photo;
private $metaData;
private $tag;
@ -24,7 +25,15 @@ class PhotoPage extends Template
echo '
<div class="row mt-5">
<div class="col-lg-9">
<div class="col-lg">';
$this->photoMeta();
echo '
</div>
</div>
<div class="row mt-5">
<div class="col-lg">
<div id="sub_photo" class="content-box">';
$this->userActions();
@ -38,12 +47,6 @@ class PhotoPage extends Template
echo '
</div>
</div>
<div class="col-lg-3">';
$this->photoMeta();
echo '
</div>
</div>
<script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>';
}
@ -78,6 +81,11 @@ class PhotoPage extends Template
</a>';
}
public function setActiveFilter($filter)
{
$this->activeFilter = $filter;
}
public function setTag(Tag $tag)
{
$this->tag = $tag;
@ -85,14 +93,14 @@ class PhotoPage extends Template
private function photoNav()
{
if ($previousUrl = $this->photo->getUrlForPreviousInSet($this->tag))
if ($previousUrl = $this->photo->getUrlForPreviousInSet($this->tag, $this->activeFilter))
echo '
<a href="', $previousUrl, '#photo_frame" id="previous_photo"><i class="bi bi-arrow-left"></i></a>';
else
echo '
<span id="previous_photo"><i class="bi bi-arrow-left"></i></span>';
if ($nextUrl = $this->photo->getUrlForNextInSet($this->tag))
if ($nextUrl = $this->photo->getUrlForNextInSet($this->tag, $this->activeFilter))
echo '
<a href="', $nextUrl, '#photo_frame" id="next_photo"><i class="bi bi-arrow-right"></i></a>';
else
@ -103,12 +111,12 @@ class PhotoPage extends Template
private function photoMeta()
{
echo '
<ul class="list-group photo_meta">';
<ul class="list-group list-group-horizontal photo_meta">';
foreach ($this->metaData as $header => $body)
{
echo '
<li class="list-group-item">
<li class="list-group-item flex-fill">
<h4>', $header, '</h4>
', $body, '
</li>';

View File

@ -13,7 +13,9 @@ class PhotosIndex extends Template
protected $show_headers;
protected $show_labels;
protected $previous_header = '';
protected $url_suffix;
protected $edit_menu_items = [];
protected $photo_url_suffix;
const PANORAMA_WIDTH = 1256;
const PANORAMA_HEIGHT = null;
@ -81,6 +83,30 @@ class PhotosIndex extends Template
$this->previous_header = $header;
}
protected function editMenu(Image $image)
{
if (empty($this->edit_menu_items))
return;
echo '
<div class="edit dropdown">
<button class="btn btn-primary btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
</button>
<ul class="dropdown-menu">';
foreach ($this->edit_menu_items as $item)
{
echo '
<li><a class="dropdown-item" href="', $item['uri']($image), '"',
isset($item['onclick']) ? ' onclick="' . $item['onclick'] . '"' : '',
'>', $item['label'], '</a></li>';
}
echo '
</ul>
</div>';
}
protected function photo(Image $image, $className, $width, $height, $crop = true, $fit = true)
{
// Prefer thumbnail aspect ratio if available, otherwise use image aspect ratio.
@ -89,12 +115,11 @@ class PhotosIndex extends Template
echo '
<div class="polaroid ', $className, '" style="aspect-ratio: ', $aspectRatio, '">';
if ($this->show_edit_buttons)
echo '
<a class="edit" href="', BASEURL, '/editasset/?id=', $image->getId(), '">Edit</a>';
if ($this->show_edit_buttons && $image->canBeEditedBy(Registry::get('user')))
$this->editMenu($image);
echo '
<a href="', $image->getPageUrl(), $this->url_suffix, '#photo_frame">';
<a href="', $image->getPageUrl(), $this->photo_url_suffix, '#photo_frame">';
foreach (['normal-photo', 'blur-photo'] as $className)
@ -319,8 +344,13 @@ class PhotosIndex extends Template
$this->threePortraits($photos, $altLayout);
}
public function setEditMenuItems(array $items)
{
$this->edit_menu_items = $items;
}
public function setUrlSuffix($suffix)
{
$this->url_suffix = $suffix;
$this->photo_url_suffix = $suffix;
}
}