From 07bc784859c36bcc176b27c04b2996cdca5bb73f Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 12:58:30 +0100 Subject: [PATCH 01/79] Add bootstrap as a dependency --- composer.json | 5 ++++- public/vendor | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 120000 public/vendor diff --git a/composer.json b/composer.json index 2ad1e40..a4b52e4 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,9 @@ "ext-mysqli": "*", "ext-imagick": "*", "ext-gd": "*", - "ext-fileinfo": "*" + "ext-imagick": "*", + "ext-mysqli": "*", + "twbs/bootstrap": "^5.3", + "twbs/bootstrap-icons": "^1.10" } } diff --git a/public/vendor b/public/vendor new file mode 120000 index 0000000..42a408b --- /dev/null +++ b/public/vendor @@ -0,0 +1 @@ +../vendor/ \ No newline at end of file -- 2.46.0 From daf6b6b26408a6166831fde7ecd4b4e72df8528b Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:12:12 +0100 Subject: [PATCH 02/79] MainTemplate: clean up HTML head; remove unused inline CSS function --- templates/MainTemplate.php | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/templates/MainTemplate.php b/templates/MainTemplate.php index 87a1c0d..b5224fb 100644 --- a/templates/MainTemplate.php +++ b/templates/MainTemplate.php @@ -25,14 +25,22 @@ class MainTemplate extends Template echo '<!DOCTYPE html> <html lang="en"> <head> - <title>', $this->title, '</title>', !empty($this->canonical_url) ? ' - <link rel="canonical" href="' . $this->canonical_url . '">' : '', ' + <title>', $this->title, '</title>'; + + if (!empty($this->canonical_url)) + echo ' + <link rel="canonical" href="', $this->canonical_url, '">'; + + echo ' + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8">'; + + echo ' + <link rel="stylesheet" href="', BASEURL, '/vendor/twbs/bootstrap/dist/css/bootstrap.min.css"> + <link rel="stylesheet" href="', BASEURL, '/vendor/twbs/bootstrap-icons/font/bootstrap-icons.css"> <link type="text/css" rel="stylesheet" href="', BASEURL, '/css/default.css"> - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta http-equiv="Content-Type" content="text/html; charset=utf-8">', !empty($this->css) ? ' - <style type="text/css">' . $this->css . ' - </style>' : '', $this->header_html, ' - <script type="text/javascript" src="', BASEURL, '/js/main.js"></script> + <script type="text/javascript" src="', BASEURL, '/js/main.js"></script>' + , $this->header_html, ' </head> <body', !empty($this->classes) ? ' class="' . implode(' ', $this->classes) . '"' : '', '> <header> @@ -84,11 +92,6 @@ class MainTemplate extends Template </html>'; } - public function appendCss($css) - { - $this->css .= $css; - } - public function appendHeaderHtml($html) { $this->header_html .= "\n\t\t" . $html; -- 2.46.0 From f9eefe7b4192452834193064295eea7c5b13e5b3 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:20:59 +0100 Subject: [PATCH 03/79] Replace generic alert, form and table templates with new Bootstrap equivalents --- controllers/ManageAlbums.php | 4 +- controllers/ManageErrors.php | 4 +- controllers/ManageTags.php | 4 +- controllers/ManageUsers.php | 4 +- controllers/ViewPeople.php | 2 +- controllers/ViewPhotoAlbum.php | 2 +- controllers/ViewTimeline.php | 2 +- models/Form.php | 341 +++++++++++++++++++++++++-------- models/GenericTable.php | 14 +- public/css/admin.css | 139 -------------- public/css/default.css | 156 +-------------- templates/AlbumIndex.php | 2 +- templates/Alert.php | 20 +- templates/FormView.php | 261 ++++++++++++++++++------- templates/PageIndexWidget.php | 61 ++++++ templates/Pagination.php | 64 ------- templates/PhotosIndex.php | 2 +- templates/TabularData.php | 172 +++++++++++++---- 18 files changed, 681 insertions(+), 573 deletions(-) create mode 100644 templates/PageIndexWidget.php delete mode 100644 templates/Pagination.php diff --git a/controllers/ManageAlbums.php b/controllers/ManageAlbums.php index f6a1b59..f989e6a 100644 --- a/controllers/ManageAlbums.php +++ b/controllers/ManageAlbums.php @@ -18,7 +18,7 @@ class ManageAlbums extends HTMLController 'form' => [ 'action' => BASEURL . '/editalbum/', 'method' => 'get', - 'class' => 'floatright', + 'class' => 'float-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -60,7 +60,7 @@ class ManageAlbums extends HTMLController 'title' => 'Manage albums', 'no_items_label' => 'No albums meet the requirements of the current filter.', 'items_per_page' => 9999, - 'index_class' => 'floatleft', + 'index_class' => 'float-start', 'base_url' => BASEURL . '/managealbums/', 'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') { if (!in_array($order, ['id_tag', 'tag', 'slug', 'count'])) diff --git a/controllers/ManageErrors.php b/controllers/ManageErrors.php index d45d37f..9619753 100644 --- a/controllers/ManageErrors.php +++ b/controllers/ManageErrors.php @@ -29,7 +29,7 @@ class ManageErrors extends HTMLController 'form' => [ 'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(), 'method' => 'post', - 'class' => 'floatright', + 'class' => 'float-end', 'buttons' => [ 'flush' => [ 'type' => 'submit', @@ -99,7 +99,7 @@ class ManageErrors extends HTMLController 'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '', 'no_items_label' => "No errors to display -- we're all good!", 'items_per_page' => 20, - 'index_class' => 'floatleft', + 'index_class' => 'float-start', 'base_url' => BASEURL . '/manageerrors/', 'get_count' => 'ErrorLog::getCount', 'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') { diff --git a/controllers/ManageTags.php b/controllers/ManageTags.php index b600f2d..a49f9aa 100644 --- a/controllers/ManageTags.php +++ b/controllers/ManageTags.php @@ -18,7 +18,7 @@ class ManageTags extends HTMLController 'form' => [ 'action' => BASEURL . '/edittag/', 'method' => 'get', - 'class' => 'floatright', + 'class' => 'float-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -65,7 +65,7 @@ class ManageTags extends HTMLController 'title' => 'Manage tags', 'no_items_label' => 'No tags meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'floatleft', + 'index_class' => 'float-start', 'base_url' => BASEURL . '/managetags/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') { if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count'])) diff --git a/controllers/ManageUsers.php b/controllers/ManageUsers.php index d7fadbd..14ef97f 100644 --- a/controllers/ManageUsers.php +++ b/controllers/ManageUsers.php @@ -18,7 +18,7 @@ class ManageUsers extends HTMLController 'form' => [ 'action' => BASEURL . '/edituser/', 'method' => 'get', - 'class' => 'floatright', + 'class' => 'float-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -94,7 +94,7 @@ class ManageUsers extends HTMLController 'title' => 'Manage users', 'no_items_label' => 'No users meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'floatleft', + 'index_class' => 'float-start', 'base_url' => BASEURL . '/manageusers/', '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'])) diff --git a/controllers/ViewPeople.php b/controllers/ViewPeople.php index bf7d8ba..3829327 100644 --- a/controllers/ViewPeople.php +++ b/controllers/ViewPeople.php @@ -53,7 +53,7 @@ class ViewPeople extends HTMLController 'base_url' => BASEURL . '/people/', 'page_slug' => 'page/%PAGE%/', ]); - $this->page->adopt(new Pagination($pagination)); + $this->page->adopt(new PageIndexWidget($pagination)); $this->page->setCanonicalUrl(BASEURL . '/people/' . ($page > 1 ? 'page/' . $page . '/' : '')); } diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index aa0f10a..73c84e2 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -112,7 +112,7 @@ class ViewPhotoAlbum extends HTMLController 'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''), 'page_slug' => 'page/%PAGE%/', ]); - $this->page->adopt(new Pagination($index)); + $this->page->adopt(new PageIndexWidget($index)); } // Set the canonical url. diff --git a/controllers/ViewTimeline.php b/controllers/ViewTimeline.php index 3aa29b9..bf9d56e 100644 --- a/controllers/ViewTimeline.php +++ b/controllers/ViewTimeline.php @@ -47,7 +47,7 @@ class ViewTimeline extends HTMLController 'base_url' => BASEURL . '/timeline/', 'page_slug' => 'page/%PAGE%/', ]); - $this->page->adopt(new Pagination($index)); + $this->page->adopt(new PageIndexWidget($index)); } // Set the canonical url. diff --git a/models/Form.php b/models/Form.php index 62952ae..3558533 100644 --- a/models/Form.php +++ b/models/Form.php @@ -3,7 +3,8 @@ * Form.php * Contains key class Form. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 *****************************************************************************/ class Form @@ -12,9 +13,11 @@ class Form public $request_url; public $content_above; public $content_below; - private $fields; - private $data; - private $missing; + private $fields = []; + private $data = []; + private $missing = []; + private $submit_caption; + private $trim_inputs; // NOTE: this class does not verify the completeness of form options. public function __construct($options) @@ -24,9 +27,42 @@ class Form $this->fields = !empty($options['fields']) ? $options['fields'] : []; $this->content_below = !empty($options['content_below']) ? $options['content_below'] : null; $this->content_above = !empty($options['content_above']) ? $options['content_above'] : null; + $this->submit_caption = !empty($options['submit_caption']) ? $options['submit_caption'] : 'Save information'; + $this->trim_inputs = !empty($options['trim_inputs']); } - public function verify($post) + public function getFields() + { + return $this->fields; + } + + public function getData() + { + return $this->data; + } + + public function getSubmitButtonCaption() + { + return $this->submit_caption; + } + + public function getMissing() + { + return $this->missing; + } + + public function setData($data) + { + $this->verify($data, true); + $this->missing = []; + } + + public function setFieldAsMissing($field) + { + $this->missing[] = $field; + } + + public function verify($post, $initalisation = false) { $this->data = []; $this->missing = []; @@ -41,30 +77,43 @@ class Form } // No data present at all for this field? - if ((!isset($post[$field_id]) || $post[$field_id] == '') && empty($field['is_optional'])) + if ((!isset($post[$field_id]) || $post[$field_id] == '') && + $field['type'] !== 'captcha') { - $this->missing[] = $field_id; - $this->data[$field_id] = ''; + if (empty($field['is_optional'])) + $this->missing[] = $field_id; + + if ($field['type'] === 'select' && !empty($field['multiple'])) + $this->data[$field_id] = []; + else + $this->data[$field_id] = ''; + continue; } - // Verify data for all fields + // Should we trim this? + if ($this->trim_inputs && $field['type'] !== 'captcha' && empty($field['multiple'])) + $post[$field_id] = trim($post[$field_id]); + + // Using a custom validation function? + if (isset($field['validate']) && is_callable($field['validate'])) + { + // Validation functions can clean up the data if passed by reference + $this->data[$field_id] = $post[$field_id]; + + // Evaluate validation functions as boolean to see if data is missing + if (!$field['validate']($post[$field_id])) + $this->missing[] = $field_id; + + continue; + } + + // Verify data by field type switch ($field['type']) { case 'select': case 'radio': - // Skip validation? Dangerous territory! - if (isset($field['verify_options']) && $field['verify_options'] === false) - $this->data[$field_id] = $post[$field_id]; - // Check whether selected option is valid. - elseif (isset($post[$field_id]) && !isset($field['options'][$post[$field_id]])) - { - $this->missing[] = $field_id; - $this->data[$field_id] = ''; - continue 2; - } - else - $this->data[$field_id] = $post[$field_id]; + $this->validateSelect($field_id, $field, $post); break; case 'checkbox': @@ -73,61 +122,22 @@ class Form break; case 'color': - // Colors are stored as a string of length 3 or 6 (hex) - if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6)) - { - $this->missing[] = $field_id; - $this->data[$field_id] = ''; - continue 2; - } - else - $this->data[$field_id] = $post[$field_id]; + $this->validateColor($field_id, $field, $post); break; case 'file': - // Needs to be verified elsewhere! + // Asset needs to be processed out of POST! This is just a filename. + $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : ''; break; case 'numeric': - $data = isset($post[$field_id]) ? $post[$field_id] : ''; - // Do we need to check bounds? - if (isset($field['min_value']) && is_numeric($data)) - { - if (is_float($field['min_value']) && (float) $data < $field['min_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0.0; - } - elseif (is_int($field['min_value']) && (int) $data < $field['min_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0; - } - else - $this->data[$field_id] = $data; - } - elseif (isset($field['max_value']) && is_numeric($data)) - { - if (is_float($field['max_value']) && (float) $data > $field['max_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0.0; - } - elseif (is_int($field['max_value']) && (int) $data > $field['max_value']) - { - $this->missing[] = $field_id; - $this->data[$field_id] = 0; - } - else - $this->data[$field_id] = $data; - } - // Does it look numeric? - elseif (is_numeric($data)) - { - $this->data[$field_id] = $data; - } - // Let's consider it missing, then. - else + $this->validateNumeric($field_id, $field, $post); + break; + + case 'captcha': + if (isset($_POST['g-recaptcha-response']) && !$initalisation) + $this->validateCaptcha($field_id); + elseif (!$initalisation) { $this->missing[] = $field_id; $this->data[$field_id] = 0; @@ -137,29 +147,200 @@ class Form case 'text': case 'textarea': default: - $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : ''; + $this->validateText($field_id, $field, $post); } } } - public function setData($data) + private function validateCaptcha($field_id) { - $this->verify($data); - $this->missing = []; + $postdata = http_build_query([ + 'secret' => RECAPTCHA_API_SECRET, + 'response' => $_POST['g-recaptcha-response'], + ]); + + $opts = [ + 'http' => [ + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $postdata, + ] + ]; + + $context = stream_context_create($opts); + $result = file_get_contents('https://www.google.com/recaptcha/api/siteverify', false, $context); + $check = json_decode($result); + + if ($check->success) + { + $this->data[$field_id] = 1; + } + else + { + $this->data[$field_id] = 0; + $this->missing[] = $field_id; + } } - public function getFields() + private function validateColor($field_id, array $field, array $post) { - return $this->fields; + // Colors are stored as a string of length 3 or 6 (hex) + if (!isset($post[$field_id]) || (strlen($post[$field_id]) != 3 && strlen($post[$field_id]) != 6)) + { + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + } + else + $this->data[$field_id] = $post[$field_id]; } - public function getData() + private function validateNumeric($field_id, array $field, array $post) { - return $this->data; + $data = isset($post[$field_id]) ? $post[$field_id] : ''; + + // Sanity check: does this even look numeric? + if (!is_numeric($data)) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0; + return; + } + + // Do we need to a minimum bound? + if (isset($field['min_value'])) + { + if (is_float($field['min_value']) && (float) $data < $field['min_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0.0; + } + elseif (is_int($field['min_value']) && (int) $data < $field['min_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0; + } + } + + // What about a maximum bound? + if (isset($field['max_value'])) + { + if (is_float($field['max_value']) && (float) $data > $field['max_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0.0; + } + elseif (is_int($field['max_value']) && (int) $data > $field['max_value']) + { + $this->missing[] = $field_id; + $this->data[$field_id] = 0; + } + } + + $this->data[$field_id] = $data; } - public function getMissing() + private function validateSelect($field_id, array $field, array $post) { - return $this->missing; + // Skip validation? Dangerous territory! + if (isset($field['verify_options']) && $field['verify_options'] === false) + { + $this->data[$field_id] = $post[$field_id]; + return; + } + + // Check whether selected option is valid. + if (($field['type'] !== 'select' || empty($field['multiple'])) && empty($field['has_groups'])) + { + if (isset($post[$field_id]) && !isset($field['options'][$post[$field_id]])) + { + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + return; + } + else + $this->data[$field_id] = $post[$field_id]; + } + // Multiple selections involve a bit more work. + elseif (!empty($field['multiple']) && empty($field['has_groups'])) + { + $this->data[$field_id] = []; + if (!is_array($post[$field_id])) + { + if (isset($field['options'][$post[$field_id]])) + $this->data[$field_id][] = $post[$field_id]; + else + $this->missing[] = $field_id; + return; + } + + foreach ($post[$field_id] as $option) + { + if (isset($field['options'][$option])) + $this->data[$field_id][] = $option; + } + + if (empty($this->data[$field_id])) + $this->missing[] = $field_id; + } + // Any optgroups involved? + elseif (!empty($field['has_groups'])) + { + if (!isset($post[$field_id])) + { + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + return; + } + + // Expensive: iterate over all groups until the value selected has been found. + foreach ($field['options'] as $label => $options) + { + if (is_array($options)) + { + // Consider each of the options as a valid a value. + foreach ($options as $value => $label) + { + if ($post[$field_id] === $value) + { + $this->data[$field_id] = $options; + return; + } + } + } + else + { + // This is an ungrouped value in disguise! Treat it as such. + if ($post[$field_id] === $options) + { + $this->data[$field_id] = $options; + return; + } + else + continue; + } + } + + // If we've reached this point, we'll consider the data invalid. + $this->missing[] = $field_id; + $this->data[$field_id] = ''; + } + else + { + throw new UnexpectedValueException('Unexpected field configuration in validateSelect!'); + } + } + + private function validateText($field_id, array $field, array $post) + { + $this->data[$field_id] = isset($post[$field_id]) ? $post[$field_id] : ''; + + // Trim leading and trailing whitespace? + if (!empty($field['trim'])) + $this->data[$field_id] = trim($this->data[$field_id]); + + // Is there a length limit to enforce? + if (isset($field['maxlength']) && strlen($post[$field_id]) > $field['maxlength']) { + $post[$field_id] = substr($post[$field_id], 0, $field['maxlength']); + } } } diff --git a/models/GenericTable.php b/models/GenericTable.php index f2147f7..d98220a 100644 --- a/models/GenericTable.php +++ b/models/GenericTable.php @@ -3,7 +3,8 @@ * GenericTable.php * Contains key class GenericTable. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2021 *****************************************************************************/ class GenericTable @@ -19,7 +20,7 @@ class GenericTable public $form_above; public $form_below; - + private $table_class; private $sort_direction; private $sort_order; private $base_url; @@ -84,6 +85,8 @@ class GenericTable else $this->body = $options['no_items_label'] ?? ''; + $this->table_class = $options['table_class'] ?? ''; + // Got a title? $this->title = $options['title'] ?? ''; $this->title_class = $options['title_class'] ?? ''; @@ -105,6 +108,7 @@ class GenericTable $header = [ 'class' => isset($column['class']) ? $column['class'] : '', + 'cell_class' => isset($column['cell_class']) ? $column['cell_class'] : null, 'colspan' => !empty($column['header_colspan']) ? $column['header_colspan'] : 1, 'href' => $isSortable ? $this->getLink($this->start, $key, $sortDirection) : null, 'label' => $column['header'], @@ -168,6 +172,11 @@ class GenericTable return $this->pageIndex; } + public function getTableClass() + { + return $this->table_class; + } + public function getTitle() { return $this->title; @@ -196,6 +205,7 @@ class GenericTable // Append the cell to the row. $newRow['cells'][] = [ + 'class' => $column['cell_class'] ?? '', 'value' => $value, ]; } diff --git a/public/css/admin.css b/public/css/admin.css index f3f3b90..93007cc 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -11,13 +11,6 @@ margin: 0 0 0.2em; } -.floatleft { - float: left; -} -.floatright { - float: right; -} - /* Admin bar styles ---------------------*/ body { @@ -81,22 +74,6 @@ body { } -/* Edit user screen ----------------------*/ -.edituser dt { - clear: left; - float: left; - width: 150px; -} -.edituser dd { - float: left; - margin-bottom: 5px; -} -.edituser form div:last-child { - padding: 1em 0 0; -} - - /* Admin widgets ------------------*/ .widget { @@ -195,119 +172,3 @@ body { top: 400px; left: 300px; } - - -/* The pagination styles below are based on Bootstrap 2.3.2 --------------------------------------------------------------*/ - -.table_pagination, .table_form { - margin: 20px 0; -} - -.table_pagination ul { - display: inline-block; - margin: 0; - padding: 0; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.table_pagination ul > li { - display: inline; -} - -.table_pagination ul > li > a, -.table_pagination ul > li > span { - float: left; - padding: 4px 12px; - line-height: 20px; - text-decoration: none; - background-color: #ffffff; - border: 1px solid #dddddd; - border-left-width: 0; -} - -.table_pagination ul > li > a:hover, -.table_pagination ul > li > a:focus, -.table_pagination ul > .active > a, -.table_pagination ul > .active > span { - background-color: #f5f5f5; -} - -.table_pagination ul > .active > a, -.table_pagination ul > .active > span { - color: #999999; - cursor: default; -} - -.table_pagination ul > .disabled > span, -.table_pagination ul > .disabled > a, -.table_pagination ul > .disabled > a:hover, -.table_pagination ul > .disabled > a:focus { - color: #999999; - cursor: default; - background-color: transparent; -} - -.table_pagination ul > li:first-child > a, -.table_pagination ul > li:first-child > span { - border-left-width: 1px; -} - - -/* The table styles below were taken from Bootstrap 2.3.2 ------------------------------------------------------------*/ -table { - max-width: 100%; - background-color: transparent; - border-collapse: collapse; - border-spacing: 0; -} - -.table { - width: 100%; - margin-bottom: 20px; -} - -.table th, -.table td { - border-top: 1px solid #dddddd; - line-height: 20px; - padding: 8px; - text-align: left; - vertical-align: top; -} - -.table th { - font-weight: bold; -} - -.table thead th { - vertical-align: bottom; -} - -.table caption + thead tr:first-child th, -.table caption + thead tr:first-child td, -.table colgroup + thead tr:first-child th, -.table colgroup + thead tr:first-child td, -.table thead:first-child tr:first-child th, -.table thead:first-child tr:first-child td { - border-top: 0; -} - -.table tbody + tbody { - border-top: 2px solid #dddddd; -} - -.table .table { - background-color: #ffffff; -} - -.table-striped tbody > tr:nth-child(odd) > td, -.table-striped tbody > tr:nth-child(odd) > th { - background-color: #f9f9f9; -} - -.table-hover tbody tr:hover > td, -.table-hover tbody tr:hover > th { - background-color: #f5f5f5; -} diff --git a/public/css/default.css b/public/css/default.css index 0aadb89..f4ecab9 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -15,7 +15,7 @@ } body { - font: 13px/1.7 "Open Sans", sans-serif; + font-family: "Open Sans", sans-serif; padding: 0 0 3em; margin: 0; background: #aaa 0 -50% fixed; @@ -94,51 +94,6 @@ ul#nav li a:hover { } -/* Pagination ----------------*/ -.pagination { - clear: both; - text-align: center; -} -.pagination ul { - display: inline-block; - margin: 0; - padding: 0; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); -} -.pagination ul > li { - display: inline; -} -.pagination ul > li > a, .pagination ul > li > span { - float: left; - font: 300 18px/2.2 "Open Sans", sans-serif; - padding: 6px 22px; - text-decoration: none; - background-color: #fff; - border-right: 1px solid #ddd; -} - -.pagination ul > li > a:hover, .pagination ul > li > a:focus, -.pagination ul > .active > a, .pagination ul > .active > span { - background-color: #eee; -} - -.pagination ul > .active > a, .pagination ul > .active > span { - cursor: default; -} - -.pagination ul > .disabled > span, .pagination ul > .disabled > a, -.pagination ul > .disabled > a:hover, .pagination ul > .disabled > a:focus { - color: #999; - cursor: default; - background-color: transparent; -} - -.pagination .page-padding { - cursor: pointer; -} - - /* Tiled grid ---------------*/ .tiled_header { @@ -372,51 +327,6 @@ footer a { /* Input ----------*/ - -input, select, .btn { - background: #fff; - border: 1px solid #dbdbdb; - border-radius: 4px; - color: #000; - font: 13px/1.7 "Open Sans", "Helvetica", sans-serif; - padding: 3px; -} -textarea { - border: 1px solid #dbdbdb; - border-radius: 4px; - font: 14px/1.4 'Inconsolata', 'DejaVu Sans Mono', monospace; - padding: 0.75%; -} - -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: #eebbaa; border-color: #cc9988; @@ -474,70 +384,6 @@ input:active, select:active, button:active, .btn:active { } -/* Alert boxes -- styling borrowed from Bootstrap 2 ------------------------------------------------------*/ -.alert { - padding: 8px 35px 8px 14px; - margin-bottom: 20px; - text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); - background-color: #fcf8e3; - border: 1px solid #fbeed5; - -webkit-border-radius: 4px; - -moz-border-radius: 4px; - border-radius: 4px; -} -.alert, -.alert h4 { - color: #c09853; -} -.alert h4 { - margin: 0; -} -.alert .close { - position: relative; - top: -2px; - right: -21px; - line-height: 20px; -} -.alert-success { - background-color: #dff0d8; - border-color: #d6e9c6; - color: #468847; -} -.alert-success h4 { - color: #468847; -} -.alert-danger, -.alert-error { - background-color: #f2dede; - border-color: #eed3d7; - color: #b94a48; -} -.alert-danger h4, -.alert-error h4 { - color: #b94a48; -} -.alert-info { - background-color: #d9edf7; - border-color: #bce8f1; - color: #3a87ad; -} -.alert-info h4 { - color: #3a87ad; -} -.alert-block { - padding-top: 14px; - padding-bottom: 14px; -} -.alert-block > p, -.alert-block > ul { - margin-bottom: 0; -} -.alert-block p + p { - margin-top: 5px; -} - - /* Styling for the photo pages --------------------------------*/ #photo_frame { diff --git a/templates/AlbumIndex.php b/templates/AlbumIndex.php index 29a856b..7fdcf64 100644 --- a/templates/AlbumIndex.php +++ b/templates/AlbumIndex.php @@ -26,7 +26,7 @@ class AlbumIndex extends SubTemplate protected function html_content() { echo ' - <div class="tiled_grid">'; + <div class="tiled_grid clearfix">'; foreach (array_chunk($this->albums, 3) as $photos) { diff --git a/templates/Alert.php b/templates/Alert.php index d5a2091..4f24031 100644 --- a/templates/Alert.php +++ b/templates/Alert.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class Alert extends SubTemplate +class Alert extends Template { private $_type; private $_message; @@ -16,20 +16,20 @@ class Alert extends SubTemplate { $this->_title = $title; $this->_message = $message; - $this->_type = in_array($type, ['alert', 'error', 'success', 'info']) ? $type : 'alert'; + $this->_type = in_array($type, ['success', 'info', 'warning', 'danger']) ? $type : 'info'; } - protected function html_content() + public function html_main() { echo ' - <div class="alert', $this->_type != 'alert' ? ' alert-' . $this->_type : '', '">', (!empty($this->_title) ? ' - <strong>' . $this->_title . '</strong><br>' : ''), '<p>', $this->_message, '</p>'; - - $this->additional_alert_content(); - - echo '</div>'; + <div class="alert', $this->_type !== 'alert' ? ' alert-' . $this->_type : '', '">' + , !empty($this->_title) ? '<strong>' . $this->_title . '</strong><br>' : '', ' + ', $this->_message, + $this->additional_alert_content(), ' + </div>'; } protected function additional_alert_content() - {} + { + } } diff --git a/templates/FormView.php b/templates/FormView.php index 389fa11..5b775ef 100644 --- a/templates/FormView.php +++ b/templates/FormView.php @@ -3,52 +3,42 @@ * FormView.php * Contains the form template. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 *****************************************************************************/ class FormView extends SubTemplate { - private $content_below; - private $content_above; - private $data; - private $missing; - private $fields; - private $request_method; - private $request_url; + private $form; + private array $data; + private array $missing; private $title; public function __construct(Form $form, $title = '') { + $this->form = $form; $this->title = $title; - $this->request_url = $form->request_url; - $this->request_method = $form->request_method; - $this->fields = $form->getFields(); - $this->missing = $form->getMissing(); - $this->data = $form->getData(); - $this->content_above = $form->content_above; - $this->content_below = $form->content_below; } protected function html_content($exclude = [], $include = []) { if (!empty($this->title)) echo ' - <div class="admin_box"> - <h2>', htmlspecialchars($this->title), '</h2>'; + <h1>', $this->title, '</h1>'; foreach ($this->_subtemplates as $template) $template->html_main(); echo ' - <form action="', $this->request_url, '" method="', $this->request_method, '" enctype="multipart/form-data">'; + <form action="', $this->form->request_url, '" method="', $this->form->request_method, '" enctype="multipart/form-data">'; - if (isset($this->content_above)) - echo $this->content_above; + if (isset($this->form->content_above)) + echo $this->form->content_above; - echo ' - <dl>'; + $this->missing = $this->form->getMissing(); + $this->data = $this->form->getData(); - foreach ($this->fields as $field_id => $field) + foreach ($this->form->getFields() as $field_id => $field) { // Either we have a blacklist if (!empty($exclude) && in_array($field_id, $exclude)) @@ -62,107 +52,230 @@ class FormView extends SubTemplate } echo ' - </dl> <input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '"> - <div style="clear: both"> - <button type="submit" class="btn btn-primary">Save information</button>'; + <div class="form-group"> + <div class="offset-sm-2 col-sm-10"> + <button type="submit" name="submit" class="btn btn-primary">', $this->form->getSubmitButtonCaption(), '</button>'; - if (isset($this->content_below)) + if (isset($this->form->content_below)) echo ' - ', $this->content_below; + ', $this->form->content_below; echo ' + </div> </div> </form>'; - - if (!empty($this->title)) - echo ' - </div>'; } - protected function renderField($field_id, $field) + protected function renderField($field_id, array $field) { if (isset($field['before_html'])) - echo '</dl> - ', $field['before_html'], ' - <dl>'; - - if ($field['type'] != 'checkbox' && isset($field['label'])) echo ' - <dt class="cont_', $field_id, isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['label'], '</dt>'; - elseif ($field['type'] === 'checkbox' && isset($field['header'])) - echo ' - <dt class="cont_', $field_id, isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['header'], '</dt>'; + ', $field['before_html']; echo ' - <dd class="cont_', $field_id, isset($field['dd_class']) ? ' ' . $field['dd_class'] : '', isset($field['tab_class']) ? ' target target-' . $field['tab_class'] : '', '">'; + <div class="row mb-2">'; if (isset($field['before'])) echo $field['before']; + if ($field['type'] !== 'checkbox') + if (isset($field['label'])) + echo ' + <label class="col-sm-2 col-form-label" for="', $field_id, '"', in_array($field_id, $this->missing) ? ' style="color: red"' : '', '>', $field['label'], ':</label> + <div class="', isset($field['class']) ? $field['class'] : 'col-sm-6', '">'; + else + echo ' + <div class="offset-sm-2 ', isset($field['class']) ? $field['class'] : 'col-sm-6', '">'; + switch ($field['type']) { case 'select': - echo ' - <select name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>'; - - if (isset($field['placeholder'])) - echo ' - <option value="">', $field['placeholder'], '</option>'; - - foreach ($field['options'] as $value => $option) - echo ' - <option value="', $value, '"', $this->data[$field_id] == $value ? ' selected' : '', '>', htmlentities($option), '</option>'; - - echo ' - </select>'; + $this->renderSelect($field_id, $field); break; case 'radio': - foreach ($field['options'] as $value => $option) - echo ' - <input type="radio" name="', $field_id, '" value="', $value, '"', $this->data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> ', htmlentities($option); + $this->renderRadio($field_id, $field); break; case 'checkbox': - echo ' - <label><input type="checkbox"', $this->data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '"> ', htmlentities($field['label']), '</label>'; + $this->renderCheckbox($field_id, $field); break; case 'textarea': - echo ' - <textarea name="', $field_id, '" id="', $field_id, '" cols="', isset($field['columns']) ? $field['columns'] : 40, '" rows="', isset($field['rows']) ? $field['rows'] : 4, '"', !empty($field['disabled']) ? ' disabled' : '', '>', $this->data[$field_id], '</textarea>'; + $this->renderTextArea($field_id, $field); break; case 'color': - echo ' - <input type="color" name="', $field_id, '" id="', $field_id, '" value="', htmlentities($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>'; + $this->renderColor($field_id, $field); break; 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' : '', '>'; + $this->renderNumeric($field_id, $field); break; case 'file': - if (!empty($this->data[$field_id])) - echo '<img src="', $this->data[$field_id], '" alt=""><br>'; + $this->renderFile($field_id, $field); + break; - echo ' - <input type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>'; + case 'captcha': + $this->renderCaptcha($field_id, $field); break; case 'text': case 'password': default: - echo ' - <input type="', $field['type'], '" 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' : '', isset($field['trigger']) ? ' class="trigger-' . $field['trigger'] . '"' : '', '>'; + $this->renderText($field_id, $field); } if (isset($field['after'])) echo ' ', $field['after']; + if ($field['type'] !== 'checkbox') + echo ' + </div>'; + echo ' - </dd>'; + </div>'; + } + + private function renderCaptcha($field_id, array $field) + { + echo ' + <div class="g-recaptcha" data-sitekey="', RECAPTCHA_API_KEY, '"></div> + <script src="https://www.google.com/recaptcha/api.js"></script>'; + } + + private function renderCheckbox($field_id, array $field) + { + echo ' + <div class="offset-sm-2 col-sm-10"> + <div class="form-check"> + <input class="form-check-input" type="checkbox"', $this->data[$field_id] ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', ' name="', $field_id, '" id="check-', $field_id, '"> + <label class="form-check-label" for="check-', $field_id, '"> + ', $field['label'], ' + </label> + </div> + </div>'; + } + + private function renderColor($field_id, array $field) + { + echo ' + <input class="form-control" type="color" name="', $field_id, '" id="', $field_id, '" value="', htmlspecialchars($this->data[$field_id]), '"', !empty($field['disabled']) ? ' disabled' : '', '>'; + } + + private function renderFile($field_id, array $field) + { + if (!empty($this->data[$field_id])) + echo 'Currently using asset <tt>', $this->data[$field_id], '</tt>. Upload to overwrite.<br>'; + + echo ' + <input class="form-control" type="file" name="', $field_id, '" id="', $field_id, '"', !empty($field['disabled']) ? ' disabled' : '', '>'; + } + + private function renderNumeric($field_id, array $field) + { + echo ' + <input class="form-control" 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="', htmlspecialchars($this->data[$field_id]), '"', + !empty($field['disabled']) ? ' disabled' : '', '>'; + } + + private function renderRadio($field_id, array $field) + { + foreach ($field['options'] as $value => $option) + echo ' + <div class="form-check"> + <input class="form-check-input" type="radio" name="', $field_id, '" id="radio-', $field_id, '-', $value, '" value="', $value, '"', $this->data[$field_id] == $value ? ' checked' : '', !empty($field['disabled']) ? ' disabled' : '', '> + <label class="form-check-label" for="radio-', $field_id, '-', $value, '"> + ', htmlspecialchars($option), ' + </label> + </div>'; + } + + private function renderSelect($field_id, array $field) + { + echo ' + <select class="form-select" name="', $field_id, !empty($field['multiple']) ? '[]' : '', + '" id="', $field_id, '"', + !empty($field['disabled']) ? ' disabled' : '', + !empty($field['multiple']) ? ' multiple' : '', + !empty($field['size']) ? ' size="' . $field['size'] . '"' : '', + '>'; + + if (isset($field['placeholder'])) + echo ' + <option value="">', $field['placeholder'], '</option>'; + + foreach ($field['options'] as $key => $value) + { + if (is_array($value)) + { + assert(empty($field['multiple'])); + $this->renderSelectOptionGroup($field_id, $key, $value); + } + else + $this->renderSelectOption($field_id, $value, $key, !empty($field['multiple'])); + } + + echo ' + </select>'; + } + + private function renderSelectOption($field_id, $label, $value, $multiple = false) + { + echo ' + <option value="', $value, '"', + !$multiple && $this->data[$field_id] == $value ? ' selected' : '', + $multiple && in_array($value, $this->data[$field_id]) ? ' selected' : '', + '>', htmlspecialchars($label), '</option>'; + } + + private function renderSelectOptionGroup($field_id, $label, $options) + { + echo ' + <optgroup label="', $label, '">'; + + foreach ($options as $value => $option) + $this->renderSelectOption($field_id, $option, $value); + + echo ' + </optgroup>'; + } + + private function renderText($field_id, array $field) + { + echo ' + <input class="form-control" ', + 'type="', $field['type'], '" ', + 'name="', $field_id, '" ', + 'id="', $field_id, '"', + isset($field['size']) ? ' size="' . $field['size'] . '"' : '', + isset($field['maxlength']) ? ' maxlength="' . $field['maxlength'] . '"' : '', + isset($this->data[$field_id]) ? ' value="' . htmlspecialchars($this->data[$field_id]) . '"' : '', + isset($field['placeholder']) ? ' placeholder="' . $field['placeholder'] . '"' : '', + !empty($field['disabled']) ? ' disabled' : '', + isset($field['trigger']) ? ' class="trigger-' . $field['trigger'] . '"' : '', + '>'; + } + + private function renderTextArea($field_id, array $field) + { + echo ' + <textarea class="form-control' . + '" name="', $field_id, + '" id="', $field_id, + '" cols="', isset($field['columns']) ? $field['columns'] : 40, + '" rows="', isset($field['rows']) ? $field['rows'] : 4, '"', + isset($field['placeholder']) ? ' placeholder="' . $field['placeholder'] . '"' : '', + '"', !empty($field['disabled']) ? ' disabled' : '', + '>', $this->data[$field_id], '</textarea>'; } } diff --git a/templates/PageIndexWidget.php b/templates/PageIndexWidget.php new file mode 100644 index 0000000..a10070e --- /dev/null +++ b/templates/PageIndexWidget.php @@ -0,0 +1,61 @@ +<?php +/***************************************************************************** + * PageIndexWidget.php + * Contains the template that displays a page index. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +class PageIndexWidget extends Template +{ + private $index; + private string $class; + + public function __construct(PageIndex $index) + { + $this->index = $index; + $this->class = $index->getPageIndexClass(); + } + + public function html_main() + { + self::paginate($this->index, $this->class); + } + + public static function paginate(PageIndex $index, $class = null) + { + $page_index = $index->getPageIndex(); + if (empty($page_index) || count($page_index) == 1) + return; + + if (!isset($class)) + $class = $index->getPageIndexClass(); + + echo ' + <ul class="pagination', $class ? ' ' . $class : '', '"> + <li class="page-item', empty($page_index['previous']) ? ' disabled' : '', '">', + '<a class="page-link"', !empty($page_index['previous']) ? ' href="' . $page_index['previous']['href'] . '"' : '', '>', + '« previous</a></li>'; + + foreach ($page_index as $key => $page) + { + if (!is_numeric($key)) + continue; + + if (!is_array($page)) + echo ' + <li class="page-item disabled"><a class="page-link">...</a></li>'; + else + echo ' + <li class="page-item', $page['is_selected'] ? ' active" aria-current="page' : '', '">', + '<a class="page-link" href="', $page['href'], '">', $page['index'], '</a></li>'; + } + + echo ' + <li class="page-item', empty($page_index['next']) ? ' disabled' : '', '">', + '<a class="page-link"', !empty($page_index['next']) ? ' href="' . $page_index['next']['href'] . '"' : '', '>', + 'next »</a></li> + </ul>'; + } +} diff --git a/templates/Pagination.php b/templates/Pagination.php deleted file mode 100644 index fea1df4..0000000 --- a/templates/Pagination.php +++ /dev/null @@ -1,64 +0,0 @@ -<?php -/***************************************************************************** - * Pagination.php - * Contains the pagination template. - * - * Kabuki CMS (C) 2013-2016, Aaron van Geffen - *****************************************************************************/ - -class Pagination extends SubTemplate -{ - private $index; - private static $unique_index_count = 0; - private string $class; - - public function __construct(PageIndex $index) - { - $this->index = $index; - $this->class = $index->getPageIndexClass(); - } - - protected function html_content() - { - $index = $this->index->getPageIndex(); - - echo ' - <div class="table_pagination', !empty($this->class) ? ' ' . $this->class : '', '"> - <ul> - <li class="first"><', !empty($index['previous']) ? 'a href="' . $index['previous']['href'] . '"' : 'span', '>« previous</', !empty($index['previous']) ? 'a' : 'span', '></li>'; - - $num_wildcards = 0; - foreach ($index as $key => $page) - { - if (!is_numeric($key)) - continue; - - if (!is_array($page)) - { - $num_wildcards++; - echo ' - <li class="page-padding" onclick="javascript:promptGoToPage(', self::$unique_index_count, ')"><span>...</span></li>'; - } - else - echo ' - <li class="page-number', $page['is_selected'] ? ' active' : '', '"><a href="', $page['href'], '">', $page['index'], '</a></li>'; - } - - echo ' - <li class="last"><', !empty($index['next']) ? 'a href="' . $index['next']['href'] . '"' : 'span', '>next »</', !empty($index['next']) ? 'a' : 'span', '></li> - </ul> - </div>'; - - if ($num_wildcards) - { - echo ' - <script type="text/javascript"> - var page_index_', self::$unique_index_count++, ' = { - wildcard_url: "', $this->index->getLink("%d"), '", - num_pages: ', $this->index->getNumberOfPages(), ', - per_page: ', $this->index->getItemsPerPage(), ' - }; - </script>'; - } - } -} diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index 2ec9795..c988e54 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -45,7 +45,7 @@ class PhotosIndex extends SubTemplate protected function html_content() { echo ' - <div class="tiled_grid">'; + <div class="tiled_grid clearfix">'; for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--) { diff --git a/templates/TabularData.php b/templates/TabularData.php index 587d4f0..86f17de 100644 --- a/templates/TabularData.php +++ b/templates/TabularData.php @@ -3,56 +3,73 @@ * TabularData.php * Contains the template that displays tabular data. * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 *****************************************************************************/ class TabularData extends SubTemplate { - private Pagination $pager; private GenericTable $_t; public function __construct(GenericTable $table) { $this->_t = $table; - - $pageIndex = $table->getPageIndex(); - if ($pageIndex) - $this->pager = new Pagination($pageIndex); } protected function html_content() { - echo ' - <div class="admin_box">'; - $title = $this->_t->getTitle(); if (!empty($title)) + { + $titleclass = $this->_t->getTitleClass(); echo ' - <h2>', $title, '</h2>'; + <div class="generic-table', !empty($titleclass) ? ' ' . $titleclass : '', '"> + <h1>', htmlspecialchars($title), '</h1>'; + } - // Showing a page index? - if (isset($this->pager)) - $this->pager->html_content(); + foreach ($this->_subtemplates as $template) + $template->html_main(); - // Maybe even a small form? - if (isset($this->_t->form_above)) - $this->showForm($this->_t->form_above); + // Showing an inline form? + $pager = $this->_t->getPageIndex(); + if (!empty($pager) || isset($this->_t->form_above)) + { + echo ' + <div class="row clearfix justify-content-end">'; + + // Page index? + if (!empty($pager)) + PageIndexWidget::paginate($pager); + + // Form controls? + if (isset($this->_t->form_above)) + $this->showForm($this->_t->form_above); + + echo ' + </div>'; + } + + $tableClass = $this->_t->getTableClass(); + if ($tableClass) + echo ' + <div class="', $tableClass, '">'; // Build the table! echo ' - <table class="table table-striped"> + <table class="table table-striped table-condensed"> <thead> <tr>'; - // Show the table's headers. - foreach ($this->_t->getHeader() as $th) + // Show all headers in their full glory! + $header = $this->_t->getHeader(); + foreach ($header as $th) { echo ' <th', (!empty($th['width']) ? ' width="' . $th['width'] . '"' : ''), (!empty($th['class']) ? ' class="' . $th['class'] . '"' : ''), ($th['colspan'] > 1 ? ' colspan="' . $th['colspan'] . '"' : ''), ' scope="', $th['scope'], '">', $th['href'] ? '<a href="' . $th['href'] . '">' . $th['label'] . '</a>' : $th['label']; - if ($th['sort_mode'] ) - echo ' ', $th['sort_mode'] === 'up' ? '↑' : '↓'; + if ($th['sort_mode']) + echo ' <i class="bi bi-caret-' . ($th['sort_mode'] === 'down' ? 'down' : 'up') . '-fill"></i>'; echo '</th>'; } @@ -62,7 +79,7 @@ class TabularData extends SubTemplate </thead> <tbody>'; - // Show the table's body. + // The body is what we came to see! $body = $this->_t->getBody(); if (is_array($body)) { @@ -72,51 +89,134 @@ class TabularData extends SubTemplate <tr', (!empty($tr['class']) ? ' class="' . $tr['class'] . '"' : ''), '>'; foreach ($tr['cells'] as $td) + { echo ' - <td', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>', $td['value'], '</td>'; + <td', (!empty($td['width']) ? ' width="' . $td['width'] . '"' : ''), '>'; + + if (!empty($td['class'])) + echo '<span class="', $td['class'], '">', $td['value'], '</span>'; + else + echo $td['value']; + + echo '</td>'; + } echo ' </tr>'; } } + // !!! Sum colspan! else echo ' <tr> - <td colspan="', count($this->_t->getHeader()), '">', $body, '</td> + <td colspan="', count($header), '" class="fullwidth">', $body, '</td> </tr>'; echo ' </tbody> </table>'; - // Maybe another small form? - if (isset($this->_t->form_below)) - $this->showForm($this->_t->form_below); + if ($tableClass) + echo ' + </div>'; - // Showing a page index? - if (isset($this->pager)) - $this->pager->html_content(); + // Showing an inline form? + if (!empty($pager) || isset($this->_t->form_below)) + { + echo ' + <div class="row clearfix justify-content-end">'; - echo ' + // Page index? + if (!empty($pager)) + PageIndexWidget::paginate($pager); + + // Form controls? + if (isset($this->_t->form_below)) + $this->showForm($this->_t->form_below); + + echo ' + </div>'; + } + + if (!empty($title)) + echo ' </div>'; } protected function showForm($form) { echo ' - <form action="', $form['action'], '" method="', $form['method'], '" class="table_form ', $form['class'], '">'; + <form action="', $form['action'], '" method="', $form['method'], '" class="', $form['class'], '">'; + + if (!empty($form['is_group'])) + echo ' + <div class="input-group">'; if (!empty($form['fields'])) + { foreach ($form['fields'] as $name => $field) - echo ' - <input name="', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '"', isset($field['class']) ? ' class="' . $field['class'] . '"' : '', isset($field['value']) ? ' value="' . $field['value'] . '"' : '', '>'; + { + if ($field['type'] === 'select') + { + echo ' + <select class="form-select" name="', $name, '"', (isset($field['onchange']) ? ' onchange="' . $field['onchange'] . '"' : ''), '>'; + + foreach ($field['values'] as $value => $caption) + { + if (!is_array($caption)) + { + echo ' + <option value="', $value, '"', $value === $field['selected'] ? ' selected' : '', '>', $caption, '</option>'; + } + else + { + $label = $value; + $options = $caption; + + echo ' + <optgroup label="', $label, '">'; + + foreach ($options as $value => $caption) + { + echo ' + <option value="', $value, '"', $value === $field['selected'] ? ' selected' : '', '>', $caption, '</option>'; + } + + echo ' + </optgroup>'; + } + } + + echo ' + </select>'; + } + else + echo ' + <input name="', $name, '" id="field_', $name, '" type="', $field['type'], '" placeholder="', $field['placeholder'], '" class="form-control', isset($field['class']) ? ' ' . $field['class'] : '', '"', isset($field['value']) ? ' value="' . htmlspecialchars($field['value']) . '"' : '', '>'; + + if (isset($field['html_after'])) + echo $field['html_after']; + } + } + + echo ' + <input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '">'; if (!empty($form['buttons'])) foreach ($form['buttons'] as $name => $button) + { echo ' - <input name="', $name, '" type="', $button['type'], '" value="', $button['caption'], '" class="btn', isset($button['class']) ? ' ' . $button['class'] . '' : '', '">'; + <button class="btn ', isset($button['class']) ? $button['class'] : 'btn-primary', '" type="', $button['type'], '" name="', $name, '">', $button['caption'], '</button>'; + + if (isset($button['html_after'])) + echo $button['html_after']; + } + + if (!empty($form['is_group'])) + echo ' + </div>'; echo ' - </form>'; + </form>'; } } -- 2.46.0 From 0366df9b5fa55682c1eedf2484557a76349fb580 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:30:02 +0100 Subject: [PATCH 04/79] Alerts: replace 'error' class with 'danger' --- controllers/EditAlbum.php | 4 ++-- controllers/EditTag.php | 4 ++-- controllers/EditUser.php | 12 ++++++------ controllers/Login.php | 2 +- controllers/ResetPassword.php | 4 ++-- models/Dispatcher.php | 2 +- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index 26e2c6a..5888876 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -123,7 +123,7 @@ class EditAlbum extends HTMLController // Anything missing? if (!empty($form->getMissing())) - return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error')); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); @@ -140,7 +140,7 @@ class EditAlbum extends HTMLController $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')); + return $formview->adopt(new Alert('Cannot create this album', 'Something went wrong while creating the album...', 'danger')); if (isset($_POST['submit_and_new'])) { diff --git a/controllers/EditTag.php b/controllers/EditTag.php index 24126c3..4b42926 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -115,7 +115,7 @@ class EditTag extends HTMLController // Anything missing? if (!empty($form->getMissing())) - return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error')); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); @@ -127,7 +127,7 @@ class EditTag extends HTMLController { $return = Tag::createNew($data); if ($return === false) - return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'error')); + return $formview->adopt(new Alert('Cannot create this tag', 'Something went wrong while creating the tag...', 'danger')); if (isset($_POST['submit_and_new'])) { diff --git a/controllers/EditUser.php b/controllers/EditUser.php index bbce589..c5f6578 100644 --- a/controllers/EditUser.php +++ b/controllers/EditUser.php @@ -129,7 +129,7 @@ class EditUser extends HTMLController // Anything missing? if (!empty($form->getMissing())) - return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'error')); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); @@ -150,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 Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'error')); + return $formview->adopt(new Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'danger')); // 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 Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'error')); + return $formview->adopt(new Alert('Email address already in use', 'Another account is already using the e-mail address you entered.', 'danger')); // 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 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')); + 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).', 'danger')); elseif ($data['password1'] !== $data['password2']) - return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'error')); + return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'danger')); else $data['password'] = $data['password1']; @@ -173,7 +173,7 @@ class EditUser extends HTMLController { $return = Member::createNew($data); if ($return === false) - return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'error')); + return $formview->adopt(new Alert('Cannot create this user', 'Something went wrong while creating the user...', 'danger')); if (isset($_POST['submit_and_new'])) { diff --git a/controllers/Login.php b/controllers/Login.php index 46e5da6..d91785d 100644 --- a/controllers/Login.php +++ b/controllers/Login.php @@ -44,7 +44,7 @@ class Login extends HTMLController parent::__construct('Log in - ' . SITE_TITLE); $form = new LogInForm('Log in'); if ($login_error) - $form->adopt(new Alert('', 'Invalid email address or password.', 'error')); + $form->adopt(new Alert('', 'Invalid email address or password.', 'danger')); // Tried anything? Be helpful, at least. if (isset($_POST['emailaddress'])) diff --git a/controllers/ResetPassword.php b/controllers/ResetPassword.php index 7153498..24fa7b6 100644 --- a/controllers/ResetPassword.php +++ b/controllers/ResetPassword.php @@ -48,7 +48,7 @@ class ResetPassword extends HTMLController exit; } else - $form->adopt(new Alert('Some fields require your attention', '<ul><li>' . implode('</li><li>', $missing) . '</li></ul>', 'error')); + $form->adopt(new Alert('Some fields require your attention', '<ul><li>' . implode('</li><li>', $missing) . '</li></ul>', 'danger')); } } else @@ -63,7 +63,7 @@ class ResetPassword extends HTMLController $id_user = Authentication::getUserid(trim($_POST['emailaddress'])); if ($id_user === false) { - $form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'error')); + $form->adopt(new Alert('Invalid email address', 'The email address you provided could not be found in our system. Please try again.', 'danger')); return; } diff --git a/models/Dispatcher.php b/models/Dispatcher.php index d5688ab..8280797 100644 --- a/models/Dispatcher.php +++ b/models/Dispatcher.php @@ -50,7 +50,7 @@ class Dispatcher public static function kickGuest($title = null, $message = null) { $form = new LogInForm('Log in'); - $form->adopt(new Alert($title ?? '', $message ?? '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.', 'danger')); $form->setRedirectUrl($_SERVER['REQUEST_URI']); $page = new MainTemplate('Login required'); -- 2.46.0 From 307d34430a18118273873904b6154261175ca60a Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:37:59 +0100 Subject: [PATCH 05/79] SubTemplate: use SubTemplates for boxed content only --- public/css/default.css | 22 +++++++++++++++------- templates/AdminBar.php | 4 ++-- templates/AlbumButtonBox.php | 4 ++-- templates/AlbumHeaderBox.php | 4 ++-- templates/AlbumIndex.php | 4 ++-- templates/MediaUploader.php | 6 +++--- templates/PhotoPage.php | 4 ++-- templates/PhotosIndex.php | 4 ++-- templates/SubTemplate.php | 24 +++++++++++++++++++++++- 9 files changed, 53 insertions(+), 23 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index f4ecab9..510683d 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -8,18 +8,15 @@ @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-family: "Open Sans", sans-serif; - padding: 0 0 3em; - margin: 0; background: #aaa 0 -50% fixed; - background-image: radial-gradient(ellipse at top, #ccc 0%, #aaa 55%, #333 100%); } #wrapper, header { @@ -94,6 +91,17 @@ ul#nav li a:hover { } +/* Content boxes +------------------*/ +.content-box { + background-color: #fff; + margin: 0 auto 2rem; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1); +} + + /* Tiled grid ---------------*/ .tiled_header { diff --git a/templates/AdminBar.php b/templates/AdminBar.php index 65662fe..daa81a8 100644 --- a/templates/AdminBar.php +++ b/templates/AdminBar.php @@ -6,11 +6,11 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class AdminBar extends SubTemplate +class AdminBar extends Template { private $extra_items = []; - protected function html_content() + public function html_main() { echo ' <div id="admin_bar"> diff --git a/templates/AlbumButtonBox.php b/templates/AlbumButtonBox.php index d18ef31..4126b72 100644 --- a/templates/AlbumButtonBox.php +++ b/templates/AlbumButtonBox.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ -class AlbumButtonBox extends SubTemplate +class AlbumButtonBox extends Template { private $buttons; @@ -15,7 +15,7 @@ class AlbumButtonBox extends SubTemplate $this->buttons = $buttons; } - protected function html_content() + public function html_main() { echo ' <div class="album_button_box">'; diff --git a/templates/AlbumHeaderBox.php b/templates/AlbumHeaderBox.php index 0de991d..7dfccf1 100644 --- a/templates/AlbumHeaderBox.php +++ b/templates/AlbumHeaderBox.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ -class AlbumHeaderBox extends SubTemplate +class AlbumHeaderBox extends Template { private $back_link_title; private $back_link; @@ -21,7 +21,7 @@ class AlbumHeaderBox extends SubTemplate $this->back_link_title = $back_link_title; } - protected function html_content() + public function html_main() { echo ' <div class="album_title_box"> diff --git a/templates/AlbumIndex.php b/templates/AlbumIndex.php index 7fdcf64..56635a4 100644 --- a/templates/AlbumIndex.php +++ b/templates/AlbumIndex.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class AlbumIndex extends SubTemplate +class AlbumIndex extends Template { protected $albums; protected $show_edit_buttons; @@ -23,7 +23,7 @@ class AlbumIndex extends SubTemplate $this->show_labels = $show_labels; } - protected function html_content() + public function html_main() { echo ' <div class="tiled_grid clearfix">'; diff --git a/templates/MediaUploader.php b/templates/MediaUploader.php index 008cfc9..d577ec7 100644 --- a/templates/MediaUploader.php +++ b/templates/MediaUploader.php @@ -18,14 +18,14 @@ class MediaUploader extends SubTemplate protected function html_content() { echo ' - <form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" class="boxed_content" method="post" enctype="multipart/form-data"> + <form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" method="post" enctype="multipart/form-data"> <h2>Upload new photos to "', $this->tag->tag, '"</h2> <div> <h3>Select files</h3> - <input type="file" id="upload_queue" name="uploads[]" multiple> + <input class="form-control" type="file" id="upload_queue" name="uploads[]" multiple> </div> <div> - <input name="save" id="photo_submit" type="submit" value="Upload the lot"> + <input class="btn btn-primary" name="save" id="photo_submit" type="submit" value="Upload the lot"> </div> <div id="upload_preview_area"> </div> diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 3ce2b55..ded285f 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2016, Aaron van Geffen *****************************************************************************/ -class PhotoPage extends SubTemplate +class PhotoPage extends Template { protected $photo; private $exif; @@ -34,7 +34,7 @@ class PhotoPage extends SubTemplate $this->is_asset_owner = $flag; } - protected function html_content() + public function html_main() { $this->photoNav(); $this->photo(); diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index c988e54..25255c5 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class PhotosIndex extends SubTemplate +class PhotosIndex extends Template { protected $mosaic; protected $show_edit_buttons; @@ -42,7 +42,7 @@ class PhotosIndex extends SubTemplate $this->show_labels = $show_labels; } - protected function html_content() + public function html_main() { echo ' <div class="tiled_grid clearfix">'; diff --git a/templates/SubTemplate.php b/templates/SubTemplate.php index 75030de..79f0ed0 100644 --- a/templates/SubTemplate.php +++ b/templates/SubTemplate.php @@ -8,10 +8,32 @@ abstract class SubTemplate extends Template { + protected $_class = 'content-box container'; + protected $_id; + protected $_title; + + public function __construct($title = '') + { + $this->_title = $title; + } + public function html_main() { - echo $this->html_content(); + echo ' + <div class="', $this->_class, '"', isset($this->_id) ? ' id="' . $this->_id . '"' : '', '>', + $this->html_content(), ' + </div>'; } abstract protected function html_content(); + + public function setClassName($className) + { + $this->_class = $className; + } + + public function setDOMId($id) + { + $this->_id = $id; + } } -- 2.46.0 From 2d1a299fe0874d331a325be721850a289353fe49 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:44:36 +0100 Subject: [PATCH 06/79] Replace login and password reset templates --- templates/ForgotPasswordForm.php | 24 +++++++++------ templates/LogInForm.php | 52 ++++++++++++++++++++------------ templates/PasswordResetForm.php | 36 ++++++++++++---------- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/templates/ForgotPasswordForm.php b/templates/ForgotPasswordForm.php index 7a50660..447ebeb 100644 --- a/templates/ForgotPasswordForm.php +++ b/templates/ForgotPasswordForm.php @@ -11,19 +11,25 @@ class ForgotPasswordForm extends SubTemplate protected function html_content() { echo ' - <div class="boxed_content"> - <h2>Password reset procedure</h2>'; + <h1>Password reset procedure</h1>'; foreach ($this->_subtemplates as $template) $template->html_main(); echo ' - <p>Please fill in the email address you used to sign up in the form below. You will be sent a reset link to your email address.</p> - <form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=1" method="post"> - <label class="control-label" for="field_emailaddress">E-mail address:</label><br> - <input type="text" id="field_emailaddress" name="emailaddress"> - <button type="submit" class="btn btn-primary">Send mail</button> - </form> - </div>'; + <p class="mt-3">Please fill in the email address you used to sign up in the form below. We will send a reset link to your email address.</p> + <form action="', BASEURL, '/resetpassword/?step=1" method="post"> + <div class="row"> + <label class="col-sm-2 col-form-label" for="field_emailaddress">E-mail address:</label> + <div class="col-sm-4"> + <input type="text" class="form-control" id="field_emailaddress" name="emailaddress"> + </div> + </div> + <div class="row mt-3"> + <div class="offset-sm-2 col-sm-2"> + <button type="submit" class="btn btn-primary">Send mail</button> + </div> + </div> + </form>'; } } diff --git a/templates/LogInForm.php b/templates/LogInForm.php index c2b8835..0c28e70 100644 --- a/templates/LogInForm.php +++ b/templates/LogInForm.php @@ -11,45 +11,57 @@ class LogInForm extends SubTemplate private $redirect_url = ''; private $emailaddress = ''; + protected $_class = 'content-box container w-50'; + public function setRedirectUrl($url) { - $_SESSION['login_url'] = $url; $this->redirect_url = $url; } public function setEmail($addr) { - $this->emailaddress = htmlentities($addr); + $this->emailaddress = htmlspecialchars($addr); } protected function html_content() { - echo ' - <form action="', BASEURL, '/login/" method="post" id="login"> - <h3>Log in</h3>'; + if (!empty($this->_title)) + echo ' + <h1 class="mb-4">Log in to your account</h1>'; - foreach ($this->_subtemplates as $template) - $template->html_main(); + if (!empty($this->_subtemplates)) + { + foreach ($this->_subtemplates as $template) + $template->html_main(); + } echo ' - <dl> - <dt><label for="field_emailaddress">E-mail address:</label></dt> - <dd><input type="text" id="field_emailaddress" name="emailaddress" tabindex="1" value="', $this->emailaddress, '" autofocus></dd> - - <dt><label for="field_password">Password:</label></dt> - <dd><input type="password" id="field_password" name="password" tabindex="2"></dd> - </dl>'; + <form class="mt-4" action="', BASEURL, '/login/" method="post"> + <div class="row"> + <label class="col-sm-3 col-form-label" for="field_emailaddress">E-mail address:</label> + <div class="col-sm"> + <input type="text" class="form-control" id="field_emailaddress" name="emailaddress" value="', $this->emailaddress, '"> + </div> + </div> + <div class="row mt-3"> + <label class="col-sm-3 col-form-label" for="field_password">Password:</label> + <div class="col-sm"> + <input type="password" class="form-control" id="field_password" name="password"> + </div> + </div>'; // Throw in a redirect url if asked for. if (!empty($this->redirect_url)) echo ' - <input type="hidden" name="redirect_url" value="', base64_encode($this->redirect_url), '">'; + <input type="hidden" name="redirect_url" value="', base64_encode($this->redirect_url), '">'; echo ' - <a href="', BASEURL, '/resetpassword/">Forgotten your password?</a> - <div class="buttonstrip"> - <button type="submit" class="btn btn-primary" id="field_login" name="login" tabindex="3">Log in</button> - </div> - </form>'; + <div class="mt-4"> + <div class="offset-sm-3 col-sm-9"> + <button type="submit" class="btn btn-primary">Sign in</button> + <a class="btn btn-light" href="', BASEURL, '/resetpassword/" style="margin-left: 1em">Forgotten your password?</a> + </div> + </div> + </form>'; } } diff --git a/templates/PasswordResetForm.php b/templates/PasswordResetForm.php index 4bc9c5e..2393bd5 100644 --- a/templates/PasswordResetForm.php +++ b/templates/PasswordResetForm.php @@ -20,27 +20,31 @@ class PasswordResetForm extends SubTemplate protected function html_content() { echo ' - <div class="boxed_content"> - <h2>Password reset procedure</h2>'; + <h1 class="mb-4">Password reset procedure</h1>'; foreach ($this->_subtemplates as $template) $template->html_main(); echo ' - <p>You have successfully confirmed your identify. Please use the form below to set a new password.</p> - <form class="form-horizontal" action="', BASEURL, '/resetpassword/?step=2&email=', rawurlencode($this->email), '&key=', $this->key, '" method="post"> - <p> - <label class="control-label" for="field_password1">New password:</label> - <input type="password" id="field_password1" name="password1"> - </p> - - <p> - <label class="control-label" for="field_password2">Repeat new password:</label> - <input type="password" id="field_password2" name="password2"> - </p> - + <p>You have successfully confirmed your identify. Please use the form below to set a new password.</p> + <form action="', BASEURL, '/resetpassword/?step=2&email=', rawurlencode($this->email), '&key=', $this->key, '" method="post"> + <div class="row mt-3"> + <label class="col-sm-2 col-form-label" for="field_password1">New password:</label> + <div class="col-sm-3"> + <input type="password" class="form-control" id="field_password1" name="password1"> + </div> + </div> + <div class="row mt-3"> + <label class="col-sm-2 col-form-label" for="field_password2">Repeat new password:</label> + <div class="col-sm-3"> + <input type="password" class="form-control" id="field_password2" name="password2"> + </div> + </div> + <div class="row mt-3"> + <div class="offset-sm-2 col-sm-2"> <button type="submit" class="btn btn-primary">Reset password</button> - </form> - </div>'; + </div> + </div> + </form>'; } } -- 2.46.0 From cf31f0af074933ddff62161ccfb09d2f1c4a5d60 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:51:12 +0100 Subject: [PATCH 07/79] Replace more custom button classes with Bootstrap counterparts --- public/css/default.css | 32 -------------------------------- templates/AlbumButtonBox.php | 2 +- templates/PhotoPage.php | 2 +- 3 files changed, 2 insertions(+), 34 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 510683d..b92ea4b 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -280,29 +280,12 @@ ul#nav li a:hover { margin-bottom: 20px; } .album_button_box > a { - background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - display: inline-block; - float: left; - font-size: 1em; padding: 8px 10px; margin-left: 12px; } -/* Generic boxed content ---------------------------*/ -.boxed_content { - background: #fff; - padding: 25px; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); -} -.boxed_content h2 { - font: 300 24px "Open Sans", sans-serif; - margin: 0 0 0.2em; -} - - /* Error pages ----------------*/ .errormsg p { @@ -333,21 +316,6 @@ footer a { } -/* Input -----------*/ -.btn-red { - 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 ---------------------*/ #login { diff --git a/templates/AlbumButtonBox.php b/templates/AlbumButtonBox.php index 4126b72..3956ffb 100644 --- a/templates/AlbumButtonBox.php +++ b/templates/AlbumButtonBox.php @@ -22,7 +22,7 @@ class AlbumButtonBox extends Template foreach ($this->buttons as $button) echo ' - <a href="', $button['url'], '">', $button['caption'], '</a>'; + <a class="btn btn-light" href="', $button['url'], '">', $button['caption'], '</a>'; echo ' </div>'; diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index ded285f..dc24134 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -242,7 +242,7 @@ class PhotoPage extends Template echo ' <div id=user_actions_box> <h3>Actions</h3> - <a class="btn btn-red" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete</a> + <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete</a> </div>'; } } -- 2.46.0 From a9a2c64d81be6eea7f2050f43232cedd84e5083c Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 13:57:57 +0100 Subject: [PATCH 08/79] PhotoPage: replace custom sub-photo boxes with generic equivalents --- public/css/default.css | 26 -------------------------- templates/PhotoPage.php | 22 +++++++++++++++------- 2 files changed, 15 insertions(+), 33 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index b92ea4b..541f3e6 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -421,14 +421,6 @@ a#previous_photo:hover, a#next_photo:hover { font-size: 16px; } -#sub_photo { - background: #fff; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - float: left; - padding: 2%; - margin: 25px 3.5% 25px 0; - width: 68.5%; -} #sub_photo #tag_list { list-style: none; margin: 1em 0; @@ -446,15 +438,6 @@ a#previous_photo:hover, a#next_photo:hover { } -#photo_exif_box { - background: #fff; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - margin: 25px 0 25px 0; - overflow: auto; - padding: 2%; - float: right; - width: 20%; -} #photo_exif_box dt { font-weight: bold; float: left; @@ -469,15 +452,6 @@ 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 ---------------------------------------*/ diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index dc24134..1c0bd5c 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -40,21 +40,27 @@ class PhotoPage extends Template $this->photo(); echo ' - <div id="sub_photo"> - <h2 class="entry-title">', $this->photo->getTitle(), '</h2>'; + <div class="row mt-5"> + <div class="col-8"> + <div id="sub_photo" class="content-box"> + <h2 class="entry-title">', $this->photo->getTitle(), '</h2>'; $this->taggedPeople(); $this->linkNewTags(); echo ' - </div>'; + </div> + </div> + <div class="col-4">'; $this->photoMeta(); - if($this->is_asset_owner) + if ($this->is_asset_owner) $this->addUserActions(); echo ' + </div> + </div> <script type="text/javascript" src="', BASEURL, '/js/photonav.js"></script>'; } @@ -94,7 +100,7 @@ class PhotoPage extends Template private function photoMeta() { echo ' - <div id="photo_exif_box"> + <div id="photo_exif_box" class="content-box clearfix"> <h3>EXIF</h3> <dl class="photo_meta">'; @@ -171,7 +177,9 @@ class PhotoPage extends Template echo ' <div> <h3>Link tags</h3> - <p style="position: relative"><input type="text" id="new_tag" placeholder="Type to link a new tag"></p> + <p style="position: relative"> + <input class="form-control w-auto" type="text" id="new_tag" placeholder="Type to link a new tag"> + </p> </div> <script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script> <script type="text/javascript" src="', BASEURL, '/js/autosuggest.js"></script> @@ -240,7 +248,7 @@ class PhotoPage extends Template public function addUserActions() { echo ' - <div id=user_actions_box> + <div id="user_actions_box" class="content-box"> <h3>Actions</h3> <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete</a> </div>'; -- 2.46.0 From 021df2df93a2172389927458567b44240a613424 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 14:12:56 +0100 Subject: [PATCH 09/79] Pagination: use larger page indices on photo and album index pages --- controllers/ViewPeople.php | 1 + controllers/ViewPhotoAlbum.php | 1 + controllers/ViewTimeline.php | 1 + 3 files changed, 3 insertions(+) diff --git a/controllers/ViewPeople.php b/controllers/ViewPeople.php index 3829327..014ebcb 100644 --- a/controllers/ViewPeople.php +++ b/controllers/ViewPeople.php @@ -52,6 +52,7 @@ class ViewPeople extends HTMLController 'start' => $start, 'base_url' => BASEURL . '/people/', 'page_slug' => 'page/%PAGE%/', + 'index_class' => 'pagination-lg justify-content-center', ]); $this->page->adopt(new PageIndexWidget($pagination)); diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index 73c84e2..aa3d632 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -111,6 +111,7 @@ class ViewPhotoAlbum extends HTMLController 'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE, 'base_url' => BASEURL . '/' . (isset($_GET['tag']) ? $_GET['tag'] . '/' : ''), 'page_slug' => 'page/%PAGE%/', + 'index_class' => 'pagination-lg justify-content-center', ]); $this->page->adopt(new PageIndexWidget($index)); } diff --git a/controllers/ViewTimeline.php b/controllers/ViewTimeline.php index bf9d56e..72dfd76 100644 --- a/controllers/ViewTimeline.php +++ b/controllers/ViewTimeline.php @@ -46,6 +46,7 @@ class ViewTimeline extends HTMLController 'start' => (isset($_GET['page']) ? $_GET['page'] - 1 : 0) * self::PER_PAGE, 'base_url' => BASEURL . '/timeline/', 'page_slug' => 'page/%PAGE%/', + 'index_class' => 'pagination-lg justify-content-center', ]); $this->page->adopt(new PageIndexWidget($index)); } -- 2.46.0 From 812c7a4f205c6fbb6f7b8eb38af022809d47028d Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 14:13:29 +0100 Subject: [PATCH 10/79] PhotoPage: change previous/next icons --- public/css/default.css | 10 ++++------ templates/PhotoPage.php | 8 ++++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 541f3e6..e88e043 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -401,17 +401,15 @@ a#previous_photo:hover, a#next_photo:hover { color: #000; } #previous_photo { + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; left: 0; } -#previous_photo:before { - content: '←'; -} #next_photo { + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; right: 0; } -#next_photo:before { - content: '→'; -} #sub_photo h2, #sub_photo h3, #photo_exif_box h3, #user_actions_box h3 { font: 600 20px/30px "Open Sans", sans-serif; diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 1c0bd5c..772a4f9 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -84,17 +84,17 @@ class PhotoPage extends Template { if ($this->previous_photo_url) echo ' - <a href="', $this->previous_photo_url, '" id="previous_photo"><em>Previous photo</em></a>'; + <a href="', $this->previous_photo_url, '" id="previous_photo"><i class="bi bi-arrow-left"></i></a>'; else echo ' - <span id="previous_photo"><em>Previous photo</em></span>'; + <span id="previous_photo"><i class="bi bi-arrow-left"></i></span>'; if ($this->next_photo_url) echo ' - <a href="', $this->next_photo_url, '" id="next_photo"><em>Next photo</em></a>'; + <a href="', $this->next_photo_url, '" id="next_photo"><i class="bi bi-arrow-right"></i></a>'; else echo ' - <span id="next_photo"><em>Next photo</em></span>'; + <span id="next_photo"><i class="bi bi-arrow-right"></i></span>'; } private function photoMeta() -- 2.46.0 From b9bd2bf4994e3f12d7d54bd70b2e82ee708ca26c Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 14:17:38 +0100 Subject: [PATCH 11/79] AlbumHeaderBox: apply some border radius to tag headers --- public/css/default.css | 7 ++++++- templates/AlbumHeaderBox.php | 4 +++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index e88e043..450d770 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -107,6 +107,7 @@ ul#nav li a:hover { .tiled_header { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + border-radius: 0.5rem; color: #000; clear: both; float: left; @@ -248,15 +249,19 @@ ul#nav li a:hover { .album_title_box > a { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + border-top-left-radius: 0.5rem; + border-bottom-left-radius: 0.5rem; display: inline-block; float: left; font-size: 2em; line-height: 1; - padding: 8px 10px 14px; + padding: 8px 10px; } .album_title_box > div { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + border-top-right-radius: 0.5rem; + border-bottom-right-radius: 0.5rem; float: left; font: inherit; padding: 6px 22px; diff --git a/templates/AlbumHeaderBox.php b/templates/AlbumHeaderBox.php index 7dfccf1..1d82818 100644 --- a/templates/AlbumHeaderBox.php +++ b/templates/AlbumHeaderBox.php @@ -25,7 +25,9 @@ class AlbumHeaderBox extends Template { echo ' <div class="album_title_box"> - <a class="back_button" href="', $this->back_link, '" title="', $this->back_link_title, '">←</a> + <a class="back_button" href="', $this->back_link, '" title="', $this->back_link_title, '"> + <i class="bi bi-arrow-left"></i> + </a> <div> <h2>', $this->title, '</h2>'; -- 2.46.0 From a6fd8d27644894f3ecc3750f0c3544ebab1d2f83 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 14:24:17 +0100 Subject: [PATCH 12/79] Admin controllers: apply new column classes --- controllers/ManageAlbums.php | 4 ++-- controllers/ManageAssets.php | 1 - controllers/ManageErrors.php | 5 +++-- controllers/ManageTags.php | 4 ++-- controllers/ManageUsers.php | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/controllers/ManageAlbums.php b/controllers/ManageAlbums.php index f989e6a..6f578b1 100644 --- a/controllers/ManageAlbums.php +++ b/controllers/ManageAlbums.php @@ -18,7 +18,7 @@ class ManageAlbums extends HTMLController 'form' => [ 'action' => BASEURL . '/editalbum/', 'method' => 'get', - 'class' => 'float-end', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -60,7 +60,7 @@ class ManageAlbums extends HTMLController 'title' => 'Manage albums', 'no_items_label' => 'No albums meet the requirements of the current filter.', 'items_per_page' => 9999, - 'index_class' => 'float-start', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/managealbums/', 'get_data' => function($offset = 0, $limit = 9999, $order = '', $direction = 'up') { if (!in_array($order, ['id_tag', 'tag', 'slug', 'count'])) diff --git a/controllers/ManageAssets.php b/controllers/ManageAssets.php index 1afc85b..463ede5 100644 --- a/controllers/ManageAssets.php +++ b/controllers/ManageAssets.php @@ -67,7 +67,6 @@ class ManageAssets extends HTMLController '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'])) diff --git a/controllers/ManageErrors.php b/controllers/ManageErrors.php index 9619753..f510c7b 100644 --- a/controllers/ManageErrors.php +++ b/controllers/ManageErrors.php @@ -29,11 +29,12 @@ class ManageErrors extends HTMLController 'form' => [ 'action' => BASEURL . '/manageerrors/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(), 'method' => 'post', - 'class' => 'float-end', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'flush' => [ 'type' => 'submit', 'caption' => 'Delete all', + 'class' => 'btn-danger', ], ], ], @@ -99,7 +100,7 @@ class ManageErrors extends HTMLController 'sort_direction' => !empty($_GET['dir']) ? $_GET['dir'] : '', 'no_items_label' => "No errors to display -- we're all good!", 'items_per_page' => 20, - 'index_class' => 'float-start', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/manageerrors/', 'get_count' => 'ErrorLog::getCount', 'get_data' => function($offset = 0, $limit = 20, $order = '', $direction = 'down') { diff --git a/controllers/ManageTags.php b/controllers/ManageTags.php index a49f9aa..ff49780 100644 --- a/controllers/ManageTags.php +++ b/controllers/ManageTags.php @@ -18,7 +18,7 @@ class ManageTags extends HTMLController 'form' => [ 'action' => BASEURL . '/edittag/', 'method' => 'get', - 'class' => 'float-end', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -65,7 +65,7 @@ class ManageTags extends HTMLController 'title' => 'Manage tags', 'no_items_label' => 'No tags meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'float-start', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/managetags/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'up') { if (!in_array($order, ['id_tag', 'tag', 'slug', 'kind', 'count'])) diff --git a/controllers/ManageUsers.php b/controllers/ManageUsers.php index 14ef97f..cf53e24 100644 --- a/controllers/ManageUsers.php +++ b/controllers/ManageUsers.php @@ -18,7 +18,7 @@ class ManageUsers extends HTMLController 'form' => [ 'action' => BASEURL . '/edituser/', 'method' => 'get', - 'class' => 'float-end', + 'class' => 'col-md-6 text-end', 'buttons' => [ 'add' => [ 'type' => 'submit', @@ -94,7 +94,7 @@ class ManageUsers extends HTMLController 'title' => 'Manage users', 'no_items_label' => 'No users meet the requirements of the current filter.', 'items_per_page' => 30, - 'index_class' => 'float-start', + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/manageusers/', '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'])) -- 2.46.0 From 5bb8c020bd16a80f4815c6ac8e60ac989b871065 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 14:31:44 +0100 Subject: [PATCH 13/79] EditAssetForm: replace widget class with generic content box --- public/css/admin.css | 26 +------------------------- public/js/crop_editor.js | 10 +++++++--- templates/DummyBox.php | 6 +++--- templates/EditAssetForm.php | 30 ++++++++++++++++-------------- 4 files changed, 27 insertions(+), 45 deletions(-) diff --git a/public/css/admin.css b/public/css/admin.css index 93007cc..8dd510d 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -74,30 +74,6 @@ body { } -/* Admin widgets -------------------*/ -.widget { - background: #fff; - padding: 25px; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); -} -.widget h3 { - margin: 0 0 1em; - font: 400 18px "Raleway", sans-serif; -} -.widget p, .errormsg p { - margin: 0; -} -.widget ul { - margin: 0; - list-style: none; - padding: 0; -} -.widget li { - line-height: 1.7em; -} - - /* Edit icon on tiled grids -----------------------------*/ .tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama { @@ -134,7 +110,7 @@ body { color: #fff; } #crop_editor input[type=number] { - width: 50px; + width: 75px; background: #555; color: #fff; } diff --git a/public/js/crop_editor.js b/public/js/crop_editor.js index 555ec83..286f1da 100644 --- a/public/js/crop_editor.js +++ b/public/js/crop_editor.js @@ -3,7 +3,7 @@ class CropEditor { this.opt = opt; this.edit_crop_button = document.createElement("span"); - this.edit_crop_button.className = "btn"; + this.edit_crop_button.className = "btn btn-light"; this.edit_crop_button.textContent = "Edit crop"; this.edit_crop_button.addEventListener('click', this.show.bind(this)); @@ -34,6 +34,7 @@ class CropEditor { this.position.appendChild(source_x_label); this.source_x = document.createElement("input"); + this.source_x.className = 'form-control d-inline'; this.source_x.type = 'number'; this.source_x.addEventListener("change", this.positionBoundary.bind(this)); this.source_x.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -43,6 +44,7 @@ class CropEditor { this.position.appendChild(source_y_label); this.source_y = document.createElement("input"); + this.source_y.className = 'form-control d-inline'; this.source_y.type = 'number'; this.source_y.addEventListener("change", this.positionBoundary.bind(this)); this.source_y.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -52,6 +54,7 @@ class CropEditor { this.position.appendChild(crop_width_label); this.crop_width = document.createElement("input"); + this.crop_width.className = 'form-control d-inline'; this.crop_width.type = 'number'; this.crop_width.addEventListener("change", this.positionBoundary.bind(this)); this.crop_width.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -61,6 +64,7 @@ class CropEditor { this.position.appendChild(crop_height_label); this.crop_height = document.createElement("input"); + this.crop_height.className = 'form-control d-inline'; this.crop_height.type = 'number'; this.crop_height.addEventListener("change", this.positionBoundary.bind(this)); this.crop_height.addEventListener("keyup", this.positionBoundary.bind(this)); @@ -78,13 +82,13 @@ class CropEditor { this.crop_constrain_label.appendChild(this.crop_constrain_text); this.save_button = document.createElement("span"); - this.save_button.className = "btn"; + this.save_button.className = "btn btn-light"; this.save_button.textContent = "Save"; this.save_button.addEventListener('click', this.save.bind(this)); this.position.appendChild(this.save_button); this.abort_button = document.createElement("span"); - this.abort_button.className = "btn btn-red"; + this.abort_button.className = "btn btn-danger"; this.abort_button.textContent = "Abort"; this.abort_button.addEventListener('click', this.hide.bind(this)); this.position.appendChild(this.abort_button); diff --git a/templates/DummyBox.php b/templates/DummyBox.php index 2632384..23dc086 100644 --- a/templates/DummyBox.php +++ b/templates/DummyBox.php @@ -8,9 +8,9 @@ class DummyBox extends SubTemplate { - private $_class; - private $_content; - private $_title; + protected $_class; + protected $_content; + protected $_title; public function __construct($title = '', $content = '', $class = '') { diff --git a/templates/EditAssetForm.php b/templates/EditAssetForm.php index 490b0d2..8bdcdaf 100644 --- a/templates/EditAssetForm.php +++ b/templates/EditAssetForm.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class EditAssetForm extends SubTemplate +class EditAssetForm extends Template { private $asset; private $thumbs; @@ -17,14 +17,14 @@ class EditAssetForm extends SubTemplate $this->thumbs = $thumbs; } - protected function html_content() + public function html_main() { echo ' <form id="asset_form" action="" method="post" enctype="multipart/form-data"> - <div class="boxed_content" style="margin-bottom: 2%"> + <div class="content-box"> <div style="float: right"> - <a class="btn btn-red" href="', BASEURL, '/', $this->asset->getSlug(), '?delete_confirmed">Delete asset</a> - <input type="submit" value="Save asset data"> + <a class="btn btn-danger" href="', BASEURL, '/', $this->asset->getSlug(), '?delete_confirmed">Delete asset</a> + <input class="btn btn-primary" type="submit" value="Save asset data"> </div> <h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2> </div>'; @@ -32,14 +32,15 @@ class EditAssetForm extends SubTemplate $this->section_replace(); echo ' - <div style="float: left; width: 60%; margin-right: 2%">'; + <div class="row"> + <div class="col-md-8">'; $this->section_key_info(); $this->section_asset_meta(); echo ' - </div> - <div style="float: left; width: 38%;">'; + </div> + <div class="col-md-4">'; if (!empty($this->thumbs)) $this->section_thumbnails(); @@ -47,11 +48,12 @@ class EditAssetForm extends SubTemplate $this->section_linked_tags(); echo ' - </div>'; + </div>'; $this->section_crop_editor(); echo ' + </div> </form>'; } @@ -59,7 +61,7 @@ class EditAssetForm extends SubTemplate { $date_captured = $this->asset->getDateCaptured(); echo ' - <div class="widget key_info"> + <div class="content-box key_info"> <h3>Key info</h3> <dl> <dt>Title</dt> @@ -81,7 +83,7 @@ class EditAssetForm extends SubTemplate protected function section_linked_tags() { echo ' - <div class="widget linked_tags" style="margin-top: 2%"> + <div class="content-box linked_tags"> <h3>Linked tags</h3> <ul id="tag_list">'; @@ -134,7 +136,7 @@ class EditAssetForm extends SubTemplate protected function section_thumbnails() { echo ' - <div class="widget linked_thumbs"> + <div class="content-box linked_thumbs"> <h3>Thumbnails</h3> View: <select id="thumbnail_src">'; @@ -218,7 +220,7 @@ class EditAssetForm extends SubTemplate protected function section_asset_meta() { echo ' - <div class="widget asset_meta" style="margin-top: 2%"> + <div class="content-box asset_meta" style="margin-top: 2%"> <h3>Asset meta data</h3> <ul>'; @@ -246,7 +248,7 @@ class EditAssetForm extends SubTemplate protected function section_replace() { echo ' - <div class="widget replace_asset" style="margin-bottom: 2%; display: block"> + <div class="content-box replace_asset" style="margin-bottom: 2%; display: block"> <h3>Replace asset</h3> File: <input type="file" name="replacement"> Target: <select name="replacement_target"> -- 2.46.0 From b1378a3b595267a5a7291430dc9426d5340ab5ea Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 14:38:49 +0100 Subject: [PATCH 14/79] DummyBox: fix SubTemplate inheritance --- templates/DummyBox.php | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/templates/DummyBox.php b/templates/DummyBox.php index 23dc086..0ef5f24 100644 --- a/templates/DummyBox.php +++ b/templates/DummyBox.php @@ -8,28 +8,26 @@ class DummyBox extends SubTemplate { - protected $_class; protected $_content; - protected $_title; - public function __construct($title = '', $content = '', $class = '') + public function __construct($title = '', $content = '', $class = null) { - $this->_title = $title; + parent::__construct($title); $this->_content = $content; - $this->_class = $class; + + if (isset($class)) + $this->_class .= $class; } protected function html_content() { - echo ' - <div class="boxed_content', $this->_class ? ' ' . $this->_class : '', '">', $this->_title ? ' - <h2>' . $this->_title . '</h2>' : '', ' - ', $this->_content; + if ($this->_title) + echo ' + <h2>', $this->_title, '</h2>'; + + echo $this->_content; foreach ($this->_subtemplates as $template) $template->html_main(); - - echo ' - </div>'; } } -- 2.46.0 From 277611e0ac6c237bdc6abcafe3afd696bb993722 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 15:14:05 +0100 Subject: [PATCH 15/79] Introduce new menu classes and navigation templates --- controllers/HTMLController.php | 2 -- models/AdminMenu.php | 58 +++++++++++++++++++++++++++++++ models/Dispatcher.php | 1 - models/ErrorHandler.php | 1 - models/MainMenu.php | 41 ++++++++++++++++++++++ models/Menu.php | 18 ++++++++++ models/UserMenu.php | 60 ++++++++++++++++++++++++++++++++ public/css/admin.css | 38 --------------------- public/css/default.css | 31 +++-------------- templates/AdminBar.php | 38 --------------------- templates/MainNavBar.php | 51 ++++++++++++++++++++++++++++ templates/MainTemplate.php | 16 ++++----- templates/NavBar.php | 62 ++++++++++++++++++++++++++++++++++ 13 files changed, 302 insertions(+), 115 deletions(-) create mode 100644 models/AdminMenu.php create mode 100644 models/MainMenu.php create mode 100644 models/Menu.php create mode 100644 models/UserMenu.php delete mode 100644 templates/AdminBar.php create mode 100644 templates/MainNavBar.php create mode 100644 templates/NavBar.php diff --git a/controllers/HTMLController.php b/controllers/HTMLController.php index d03bc50..a5d5236 100644 --- a/controllers/HTMLController.php +++ b/controllers/HTMLController.php @@ -22,8 +22,6 @@ abstract class HTMLController if (Registry::get('user')->isAdmin()) { $this->page->appendStylesheet(BASEURL . '/css/admin.css'); - $this->admin_bar = new AdminBar(); - $this->page->adopt($this->admin_bar); } } diff --git a/models/AdminMenu.php b/models/AdminMenu.php new file mode 100644 index 0000000..aae9e0d --- /dev/null +++ b/models/AdminMenu.php @@ -0,0 +1,58 @@ +<?php +/***************************************************************************** + * AdminMenu.php + * Contains the admin navigation logic. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +class AdminMenu extends Menu +{ + public function __construct() + { + $user = Registry::has('user') ? Registry::get('user') : new Guest(); + if (!$user->isAdmin()) + return; + + $this->items[] = [ + 'label' => 'Admin', + 'icon' => 'gear', + 'subs' => [ + [ + 'uri' => '/managealbums/', + 'label' => 'Albums', + ], + [ + 'uri' => '/manageassets/', + 'label' => 'Assets', + ], + [ + 'uri' => '/managetags/', + 'label' => 'Tags', + ], + [ + 'uri' => '/manageusers/', + 'label' => 'Users', + ], + [ + 'uri' => '/manageerrors/', + 'label' => 'Errors', + 'badge' => ErrorLog::getCount(), + ], + ], + ]; + + foreach ($this->items as $i => $item) + { + if (isset($item['uri'])) + $this->items[$i]['url'] = BASEURL . $item['uri']; + + if (!isset($item['subs'])) + continue; + + foreach ($item['subs'] as $j => $subitem) + $this->items[$i]['subs'][$j]['url'] = BASEURL . $subitem['uri']; + } + } +} diff --git a/models/Dispatcher.php b/models/Dispatcher.php index 8280797..3a6b529 100644 --- a/models/Dispatcher.php +++ b/models/Dispatcher.php @@ -86,7 +86,6 @@ class Dispatcher if (Registry::has('user') && Registry::get('user')->isAdmin()) { $page->appendStylesheet(BASEURL . '/css/admin.css'); - $page->adopt(new AdminBar()); } $page->adopt(new DummyBox('Well, this is a bit embarrassing!', '<p>The page you requested could not be found. Don\'t worry, it\'s probably not your fault. You\'re welcome to browse the website, though!</p>', 'errormsg')); diff --git a/models/ErrorHandler.php b/models/ErrorHandler.php index da650e9..3139444 100644 --- a/models/ErrorHandler.php +++ b/models/ErrorHandler.php @@ -168,7 +168,6 @@ class ErrorHandler if ($is_admin) { $page->appendStylesheet(BASEURL . '/css/admin.css'); - $page->adopt(new AdminBar()); } } elseif (!$is_sensitive) diff --git a/models/MainMenu.php b/models/MainMenu.php new file mode 100644 index 0000000..e1ccb05 --- /dev/null +++ b/models/MainMenu.php @@ -0,0 +1,41 @@ +<?php +/***************************************************************************** + * MainMenu.php + * Contains the main navigation logic. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +class MainMenu extends Menu +{ + public function __construct() + { + $this->items = [ + [ + 'uri' => '/', + 'label' => 'Albums', + ], + [ + 'uri' => '/people/', + 'label' => 'People', + ], + [ + 'uri' => '/timeline/', + 'label' => 'Timeline', + ], + ]; + + foreach ($this->items as $i => $item) + { + if (isset($item['uri'])) + $this->items[$i]['url'] = BASEURL . $item['uri']; + + if (!isset($item['subs'])) + continue; + + foreach ($item['subs'] as $j => $subitem) + $this->items[$i]['subs'][$j]['url'] = BASEURL . $subitem['uri']; + } + } +} diff --git a/models/Menu.php b/models/Menu.php new file mode 100644 index 0000000..cde4de0 --- /dev/null +++ b/models/Menu.php @@ -0,0 +1,18 @@ +<?php +/***************************************************************************** + * Menu.php + * Contains all navigational menus. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +abstract class Menu +{ + protected $items = []; + + public function getItems() + { + return $this->items; + } +} diff --git a/models/UserMenu.php b/models/UserMenu.php new file mode 100644 index 0000000..0204322 --- /dev/null +++ b/models/UserMenu.php @@ -0,0 +1,60 @@ +<?php +/***************************************************************************** + * UserMenu.php + * Contains the user navigation logic. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +class UserMenu extends Menu +{ + public function __construct() + { + $user = Registry::has('user') ? Registry::get('user') : new Guest(); + if ($user->isLoggedIn()) + { + $this->items[] = [ + 'label' => $user->getFirstName(), + 'icon' => 'person-circle', + 'subs' => [ + + [ + 'label' => 'Settings', + 'uri' => '/accountsettings/', + ], + [ + 'label' => 'Log out', + 'uri' => '/logout/', + ], + ], + ]; + } + else + { + $this->items[] = [ + 'label' => 'Log in', + 'icon' => 'person-circle', + 'uri' => '/login/', + ]; + } + + $this->items[] = [ + 'label' => 'Home', + 'icon' => 'house-door', + 'uri' => '/', + ]; + + foreach ($this->items as $i => $item) + { + if (isset($item['uri'])) + $this->items[$i]['url'] = BASEURL . $item['uri']; + + if (!isset($item['subs'])) + continue; + + foreach ($item['subs'] as $j => $subitem) + $this->items[$i]['subs'][$j]['url'] = BASEURL . $subitem['uri']; + } + } +} diff --git a/public/css/admin.css b/public/css/admin.css index 8dd510d..0316b80 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -11,44 +11,6 @@ margin: 0 0 0.2em; } -/* Admin bar styles ----------------------*/ -body { - padding-top: 30px; -} -#admin_bar { - background: #333; - color: #ccc; - left: 0; - position: fixed; - top: 0; - width: 100%; - z-index: 100; -} -#admin_bar ul { - list-style: none; - margin: 0 auto; - max-width: 1280px; - min-width: 900px; - padding: 2px; - width: 95%; -} -#admin_bar ul > li { - display: inline; - border-right: 1px solid #aaa; -} -#admin_bar ul > li:last-child { - border-right: none; -} -#admin_bar li > a { - color: inherit; - display: inline-block; - padding: 4px 6px; -} -#admin_bar li a:hover { - text-decoration: underline; -} - /* (Tag) autosuggest ----------------------*/ diff --git a/public/css/default.css b/public/css/default.css index 450d770..8cd0846 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -19,17 +19,13 @@ body { background: #aaa 0 -50% fixed; } -#wrapper, header { +#wrapper, header .container { width: 95%; min-width: 900px; max-width: 1280px; margin: 0 auto; } -header { - overflow: auto; -} - a { color: #963626; text-decoration: none; @@ -67,27 +63,10 @@ a:hover h1#logo, a:hover h1#logo:before { /* Navigation ---------------*/ -ul#nav { - margin: 55px 10px 0 0; - padding: 0; - float: right; - list-style: none; -} -ul#nav li { - float: left; -} -ul#nav li a { - color: #fff; - display: block; - float: left; - font: 200 20px 'Press Start 2P', sans-serif; - margin: 0 0 0 32px; - padding: 10px 0; - text-decoration: none; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.7); -} -ul#nav li a:hover { - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); +.nav-divider { + height: 2.5rem; + border-left: .1rem solid rgba(255,255,255, 0.2); + margin: 0 0.5rem; } diff --git a/templates/AdminBar.php b/templates/AdminBar.php deleted file mode 100644 index daa81a8..0000000 --- a/templates/AdminBar.php +++ /dev/null @@ -1,38 +0,0 @@ -<?php -/***************************************************************************** - * AdminBar.php - * Defines the AdminBar class. - * - * Kabuki CMS (C) 2013-2015, Aaron van Geffen - *****************************************************************************/ - -class AdminBar extends Template -{ - private $extra_items = []; - - public function html_main() - { - 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>'; - - foreach ($this->extra_items as $item) - echo ' - <li><a href="', $item[0], '">', $item[1], '</a></li>'; - - echo ' - <li><a href="', BASEURL, '/logout/">Log out [', Registry::get('user')->getFullName(), ']</a></li> - </ul> - </div>'; - } - - public function appendItem($url, $caption) - { - $this->extra_items[] = [$url, $caption]; - } -} diff --git a/templates/MainNavBar.php b/templates/MainNavBar.php new file mode 100644 index 0000000..ded5fbb --- /dev/null +++ b/templates/MainNavBar.php @@ -0,0 +1,51 @@ +<?php +/***************************************************************************** + * MainNavBar.php + * Contains the primary navigational menu template. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +class MainNavBar extends NavBar +{ + protected $outerMenuId = 'mainNav'; + protected $innerMenuId = 'mainNavigation'; + protected $ariaLabel = 'Main navigation'; + protected $navBarClasses = 'navbar-dark bg-dark sticky-top mb-4'; + protected $primaryBadgeClasses = 'bg-light text-dark'; + protected $secondaryBadgeClasses = 'bg-dark text-light'; + + public function html_main() + { + echo ' + <nav id="', $this->outerMenuId, '" class="navbar navbar-expand-lg ', $this->navBarClasses, '" aria-label="', $this->ariaLabel, '"> + <div class="container"> + <a class="navbar-brand flex-grow-1" href="', BASEURL, '/"> + HashRU Pics + </a> + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#', $this->innerMenuId, '" aria-controls="', $this->innerMenuId, '" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + <div class="collapse navbar-collapse justify-content-end" id="', $this->innerMenuId, '"> + <ul class="navbar-nav mb-2 mb-lg-0">'; + + $mainMenu = new MainMenu(); + $this->renderMenuItems($mainMenu->getItems()); + + echo ' + <li class="nav-divider d-none d-lg-inline"></li>'; + + $adminMenu = new AdminMenu(); + $this->renderMenuItems($adminMenu->getItems()); + + $userMenu = new UserMenu(); + $this->renderMenuItems($userMenu->getItems()); + + echo ' + </ul> + </div> + </div> + </nav>'; + } +} diff --git a/templates/MainTemplate.php b/templates/MainTemplate.php index b5224fb..d23df69 100644 --- a/templates/MainTemplate.php +++ b/templates/MainTemplate.php @@ -43,15 +43,12 @@ class MainTemplate extends Template , $this->header_html, ' </head> <body', !empty($this->classes) ? ' class="' . implode(' ', $this->classes) . '"' : '', '> - <header> - <a href="', BASEURL, '/"> - <h1 id="logo">#pics</h1> - </a> - <ul id="nav"> - <li><a href="', BASEURL, '/">albums</a></li> - <li><a href="', BASEURL, '/people/">people</a></li> - <li><a href="', BASEURL, '/timeline/">timeline</a></li> - </ul> + <header>'; + + $bar = new MainNavBar(); + $bar->html_main(); + + echo ' </header> <div id="wrapper">'; @@ -88,6 +85,7 @@ class MainTemplate extends Template echo '<pre>', strtr($query, "\t", " "), '</pre>'; echo ' + <script type="text/javascript" src="', BASEURL, '/vendor/twbs/bootstrap/dist/js/bootstrap.bundle.min.js"></script> </body> </html>'; } diff --git a/templates/NavBar.php b/templates/NavBar.php new file mode 100644 index 0000000..c625c60 --- /dev/null +++ b/templates/NavBar.php @@ -0,0 +1,62 @@ +<?php +/***************************************************************************** + * NavBar.php + * Contains the navigational menu template. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2022 + *****************************************************************************/ + +abstract class NavBar extends Template +{ + protected $primaryBadgeClasses = 'bg-dark text-light'; + protected $secondaryBadgeClasses = 'bg-light text-dark'; + + public function renderMenu(array $items, $navBarClasses = '') + { + echo ' + <ul class="navbar-nav ', $navBarClasses, '">'; + + $this->renderMenuItems($items, $navBarClasses); + + echo ' + </ul>'; + } + + public function renderMenuItems(array $items) + { + foreach ($items as $menuId => $item) + { + if (isset($item['icon'])) + $item['label'] = '<i class="bi bi-' . $item['icon'] . '"></i> ' . $item['label']; + + if (isset($item['badge'])) + $item['label'] .= ' <span class="badge ' . $this->primaryBadgeClasses . '">' . $item['badge'] . '</span>'; + + if (empty($item['subs'])) + { + echo ' + <li class="nav-item"><a class="nav-link" href="', $item['url'], '">', $item['label'], '</a></li>'; + continue; + } + + echo ' + <li class="nav-item dropdown"> + <a class="nav-link dropdown-toggle" href="#" id="menu', $menuId, '" data-bs-toggle="dropdown" aria-expanded="false">', $item['label'], '</a> + <ul class="dropdown-menu" aria-labelledby="menu', $menuId, '">'; + + foreach ($item['subs'] as $subitem) + { + if (isset($subitem['badge'])) + $subitem['label'] .= ' <span class="badge ' . $this->secondaryBadgeClasses . '">' . $subitem['badge'] . '</span>'; + + echo ' + <li><a class="dropdown-item" href="', $subitem['url'], '">', $subitem['label'], '</a></li>'; + } + + echo ' + </ul> + </li>'; + } + } +} -- 2.46.0 From c6902150f050e042ea1cf091bef3e3d55682ba0e Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 15:17:36 +0100 Subject: [PATCH 16/79] PhotoPage: move edit button from old admin bar to widget --- controllers/ViewPhoto.php | 4 ---- templates/PhotoPage.php | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/controllers/ViewPhoto.php b/controllers/ViewPhoto.php index cff5ed6..e46929d 100644 --- a/controllers/ViewPhoto.php +++ b/controllers/ViewPhoto.php @@ -27,10 +27,6 @@ class ViewPhoto extends HTMLController $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) diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 772a4f9..490b2cc 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -250,7 +250,8 @@ class PhotoPage extends Template echo ' <div id="user_actions_box" class="content-box"> <h3>Actions</h3> - <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete</a> + <a class="btn btn-primary" href="', BASEURL, '/editasset/?id=', $this->photo->getId(), '?confirm_delete">Edit photo</a> + <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete photo</a> </div>'; } } -- 2.46.0 From 87df775c5152518eec0d16ce690bc5c4994b9a5e Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 15:27:15 +0100 Subject: [PATCH 17/79] MainNavBar: re-introduce the space invader --- public/css/default.css | 50 ++++++++++++++++++---------------------- templates/MainNavBar.php | 3 ++- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 8cd0846..4a00c3b 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -4,7 +4,6 @@ * DO NOT COPY OR RE-USE WITHOUT EXPLICIT WRITTEN PERMISSION. THANK YOU. */ -@import url(//fonts.googleapis.com/css?family=Press+Start+2P); @import url(//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic); @font-face { @@ -17,6 +16,7 @@ body { font-family: "Open Sans", sans-serif; background: #aaa 0 -50% fixed; + padding: 0 0 3rem; } #wrapper, header .container { @@ -34,40 +34,34 @@ a:hover { color: #262626; } -/* Logo ----------*/ -h1#logo { - color: #fff; - float: left; - font: 200 50px 'Press Start 2P', sans-serif; - margin: 40px 0 50px 10px; - padding: 0; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6); -} -h1#logo:before { - color: #fff; - content: 'B'; - float: left; - font: 75px 'Invaders'; - margin: -4px 20px 0 0; - padding: 0; - text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.6); -} -a h1#logo { - text-decoration: none; -} -a:hover h1#logo, a:hover h1#logo:before { - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); -} - /* Navigation ---------------*/ +#mainNav { + margin-bottom: 4rem; +} .nav-divider { height: 2.5rem; border-left: .1rem solid rgba(255,255,255, 0.2); margin: 0 0.5rem; } +.navbar-brand { + padding-left: 80px; + position: relative; +} +i.space-invader::before { + color: #fff; + content: 'B'; + display: inline-block; + font: 85px 'Invaders'; + height: 85px; + left: -25px; + position: absolute; + text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); + top: -5px; + transform: rotate(-5deg); + width: 85px; +} /* Content boxes @@ -261,7 +255,7 @@ a:hover h1#logo, a:hover h1#logo:before { ---------------------*/ .album_button_box { float: right; - margin-bottom: 20px; + margin-bottom: 3rem; } .album_button_box > a { box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); diff --git a/templates/MainNavBar.php b/templates/MainNavBar.php index ded5fbb..6442673 100644 --- a/templates/MainNavBar.php +++ b/templates/MainNavBar.php @@ -12,7 +12,7 @@ class MainNavBar extends NavBar protected $outerMenuId = 'mainNav'; protected $innerMenuId = 'mainNavigation'; protected $ariaLabel = 'Main navigation'; - protected $navBarClasses = 'navbar-dark bg-dark sticky-top mb-4'; + protected $navBarClasses = 'navbar-dark bg-dark sticky-top'; protected $primaryBadgeClasses = 'bg-light text-dark'; protected $secondaryBadgeClasses = 'bg-dark text-light'; @@ -22,6 +22,7 @@ class MainNavBar extends NavBar <nav id="', $this->outerMenuId, '" class="navbar navbar-expand-lg ', $this->navBarClasses, '" aria-label="', $this->ariaLabel, '"> <div class="container"> <a class="navbar-brand flex-grow-1" href="', BASEURL, '/"> + <i class="space-invader"></i> HashRU Pics </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#', $this->innerMenuId, '" aria-controls="', $this->innerMenuId, '" aria-expanded="false" aria-label="Toggle navigation"> -- 2.46.0 From 02b43035f3375d2e222c4a0163ef0ff318500b62 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 15:32:07 +0100 Subject: [PATCH 18/79] AccountSettings: allow users to change their personal details --- controllers/AccountSettings.php | 130 ++++++++++++++++++++++++++++++++ models/Member.php | 5 +- models/Router.php | 1 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 controllers/AccountSettings.php diff --git a/controllers/AccountSettings.php b/controllers/AccountSettings.php new file mode 100644 index 0000000..4bbb056 --- /dev/null +++ b/controllers/AccountSettings.php @@ -0,0 +1,130 @@ +<?php +/***************************************************************************** + * AccountSettings.php + * Contains the account settings controller. + * + * Global Data Lab code (C) Radboud University Nijmegen + * Programming (C) Aaron van Geffen, 2015-2023 + *****************************************************************************/ + +class AccountSettings extends HTMLController +{ + public function __construct() + { + // Not logged in yet? + if (!Registry::get('user')->isLoggedIn()) + throw new NotAllowedException('You need to be logged in to view this page.'); + + parent::__construct('Account settings'); + $form_title = 'Account settings'; + + // Session checking! + if (empty($_POST)) + Session::resetSessionToken(); + else + Session::validateSession(); + + $fields = [ + 'first_name' => [ + 'type' => 'text', + 'label' => 'First name', + 'size' => 50, + 'maxlength' => 255, + ], + 'surname' => [ + 'type' => 'text', + 'label' => 'Family name', + 'size' => 50, + 'maxlength' => 255, + ], + 'emailaddress' => [ + 'type' => 'text', + 'label' => 'Email address', + 'size' => 50, + 'maxlength' => 255, + ], + 'password1' => [ + 'before_html' => '<div class="offset-sm-2 mt-4"><p>To change your password, please fill out the fields below.</p></div>', + 'type' => 'password', + 'label' => 'Password', + 'size' => 50, + 'maxlength' => 255, + 'is_optional' => true, + ], + 'password2' => [ + 'type' => 'password', + 'label' => 'Password (repeat)', + 'size' => 50, + 'maxlength' => 255, + 'is_optional' => true, + ], + ]; + + $form = new Form([ + 'request_url' => BASEURL . '/' . $_GET['action'] . '/', + 'fields' => $fields, + 'submit_caption' => 'Save details', + ]); + + $user = Registry::get('user'); + + // Create the form, add in default values. + $form->setData(empty($_POST) ? $user->getProps() : $_POST); + $formview = new FormView($form, $form_title); + $this->page->adopt($formview); + + // Left a message? + if (isset($_SESSION['account_msg'])) + { + $alert = $_SESSION['account_msg']; + $formview->adopt(new Alert($alert[0], $alert[1], $alert[2])); + unset($_SESSION['account_msg']); + } + + // Just updating account settings? + if (!empty($_POST)) + { + $form->verify($_POST); + + // Anything missing? + if (!empty($form->getMissing())) + { + $missingFields = array_intersect_key($fields, array_flip($form->getMissing())); + $missingFields = array_map(function($field) { return strtolower($field['label']); }, $missingFields); + return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $missingFields), 'danger')); + } + + $data = $form->getData(); + + // Just to be on the safe side. + $data['first_name'] = htmlspecialchars(trim($data['first_name'])); + $data['surname'] = htmlspecialchars(trim($data['surname'])); + $data['emailaddress'] = trim($data['emailaddress']); + + // If it looks like an e-mail address... + if (!empty($data['emailaddress']) && !preg_match('~^[^ ]+@[^ ]+\.[a-z]+$~', $data['emailaddress'])) + return $formview->adopt(new Alert('Email addresses invalid', 'The email address you entered is not a valid email address.', 'danger')); + // 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']) && $user->getEmailAddress() !== $data['emailaddress'] && Member::exists($data['emailaddress'])) + return $formview->adopt(new Alert('Email address already in use', 'Another account is already using this e-mail address.', 'danger')); + + // Changing passwords? + if (!empty($data['password1']) && !empty($data['password2'])) + { + if (strlen($data['password1']) < 6 || !preg_match('~[^A-z]~', $data['password1'])) + return $formview->adopt(new Alert('Password not acceptable', 'Please use a password that is at least six characters long and contains at least one non-alphabetic character (e.g. a number or symbol).', 'danger')); + elseif ($data['password1'] !== $data['password2']) + return $formview->adopt(new Alert('Passwords do not match', 'The passwords you entered do not match. Please try again.', 'danger')); + + // Keep just the one. + $data['password'] = $data['password1']; + unset($data['password1'], $data['password2']); + $formview->adopt(new Alert('Your password has been changed', 'Next time you log in, you can use your new password to authenticate yourself.', 'success')); + } + else + $formview->adopt(new Alert('Your account settings have been saved', 'Thank you for keeping your information current.', 'success')); + + $user->update($data); + } + } +} diff --git a/models/Member.php b/models/Member.php index 9fc21f7..6675dac 100644 --- a/models/Member.php +++ b/models/Member.php @@ -110,6 +110,9 @@ class Member extends User $this->is_admin = $value == 1 ? 1 : 0; } + $params = get_object_vars($this); + $params['is_admin'] = $this->is_admin ? 1 : 0; + return Registry::get('db')->query(' UPDATE users SET @@ -120,7 +123,7 @@ class Member extends User password_hash = {string:password_hash}, is_admin = {int:is_admin} WHERE id_user = {int:id_user}', - get_object_vars($this)); + $params); } /** diff --git a/models/Router.php b/models/Router.php index ba54a61..da37499 100644 --- a/models/Router.php +++ b/models/Router.php @@ -11,6 +11,7 @@ class Router public static function route() { $possibleActions = [ + 'accountsettings' => 'AccountSettings', 'addalbum' => 'EditAlbum', 'albums' => 'ViewPhotoAlbums', 'editalbum' => 'EditAlbum', -- 2.46.0 From 0a8da104cc0899f6ad5f3cc8c6ef6a9c6aeca1cf Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 16:38:03 +0100 Subject: [PATCH 19/79] MainNavBar: randomize space invader; add Coda font --- public/css/default.css | 41 ++++++++++++++++++++++++++++++++++++++-- templates/MainNavBar.php | 6 +++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 4a00c3b..2cb4faf 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -5,6 +5,7 @@ */ @import url(//fonts.googleapis.com/css?family=Open+Sans:400,400italic,700,700italic); +@import url('//fonts.googleapis.com/css2?family=Coda&display=swap'); @font-face { font-family: 'Invaders'; @@ -21,7 +22,6 @@ body { #wrapper, header .container { width: 95%; - min-width: 900px; max-width: 1280px; margin: 0 auto; } @@ -38,6 +38,7 @@ a:hover { /* Navigation ---------------*/ #mainNav { + font-family: 'Coda', sans-serif; margin-bottom: 4rem; } .nav-divider { @@ -57,10 +58,43 @@ i.space-invader::before { height: 85px; left: -25px; position: absolute; + text-align: center; text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); top: -5px; transform: rotate(-5deg); - width: 85px; + width: 110px; +} +i.space-invader.alt-1::before { + content: 'C'; +} +i.space-invader.alt-2::before { + content: 'D'; +} +i.space-invader.alt-3::before { + content: 'E'; +} +i.space-invader.alt-4::before { + content: 'H'; +} +i.space-invader.alt-5::before { + content: 'I'; +} +i.space-invader.alt-6::before { + content: 'N'; +} +i.space-invader.alt-7::before { + content: 'O'; +} +@media (max-width: 991px) { + .navbar-brand { + padding-left: 60px; + } + i.space-invader::before { + font-size: 50px; + left: -10px; + top: -7px; + width: 70px; + } } @@ -73,6 +107,9 @@ i.space-invader::before { border-radius: 0.5rem; box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1); } +.content-box h1 { + font-family: 'Coda', sans-serif; +} /* Tiled grid diff --git a/templates/MainNavBar.php b/templates/MainNavBar.php index 6442673..133c205 100644 --- a/templates/MainNavBar.php +++ b/templates/MainNavBar.php @@ -18,11 +18,15 @@ class MainNavBar extends NavBar public function html_main() { + // Select a random space invader, with a bias towards the mascot + $rnd = rand(0, 100); + $alt = $rnd > 50 ? ' alt-' . ($rnd % 6 + 1) : ''; + echo ' <nav id="', $this->outerMenuId, '" class="navbar navbar-expand-lg ', $this->navBarClasses, '" aria-label="', $this->ariaLabel, '"> <div class="container"> <a class="navbar-brand flex-grow-1" href="', BASEURL, '/"> - <i class="space-invader"></i> + <i class="space-invader', $alt, '"></i> HashRU Pics </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#', $this->innerMenuId, '" aria-controls="', $this->innerMenuId, '" aria-expanded="false" aria-label="Toggle navigation"> -- 2.46.0 From febe7bb405468f9ee350a927d11daf14e8c94dbb Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 16:39:30 +0100 Subject: [PATCH 20/79] MainNavBar: hide navigation when not logged in --- templates/MainNavBar.php | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/templates/MainNavBar.php b/templates/MainNavBar.php index 133c205..a83a356 100644 --- a/templates/MainNavBar.php +++ b/templates/MainNavBar.php @@ -31,25 +31,32 @@ class MainNavBar extends NavBar </a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#', $this->innerMenuId, '" aria-controls="', $this->innerMenuId, '" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> - </button> + </button>'; + + if (Registry::get('user')->isLoggedIn()) + { + echo ' <div class="collapse navbar-collapse justify-content-end" id="', $this->innerMenuId, '"> <ul class="navbar-nav mb-2 mb-lg-0">'; - $mainMenu = new MainMenu(); - $this->renderMenuItems($mainMenu->getItems()); + $mainMenu = new MainMenu(); + $this->renderMenuItems($mainMenu->getItems()); - echo ' + echo ' <li class="nav-divider d-none d-lg-inline"></li>'; - $adminMenu = new AdminMenu(); - $this->renderMenuItems($adminMenu->getItems()); + $adminMenu = new AdminMenu(); + $this->renderMenuItems($adminMenu->getItems()); - $userMenu = new UserMenu(); - $this->renderMenuItems($userMenu->getItems()); + $userMenu = new UserMenu(); + $this->renderMenuItems($userMenu->getItems()); + + echo ' + </ul> + </div>'; + } echo ' - </ul> - </div> </div> </nav>'; } -- 2.46.0 From 556bbb275310ea8e72a5c066867c563a3f02df85 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 16:43:53 +0100 Subject: [PATCH 21/79] Use Coda font for buttons and headers --- public/css/default.css | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 2cb4faf..ecc227b 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -34,6 +34,10 @@ a:hover { color: #262626; } +.btn { + font-family: 'Coda', 'sans-serif'; +} + /* Navigation ---------------*/ @@ -107,8 +111,11 @@ i.space-invader.alt-7::before { border-radius: 0.5rem; box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1); } -.content-box h1 { +.content-box h1, +.content-box h2, +.content-box h3 { font-family: 'Coda', sans-serif; + margin-bottom: 0.5em; } @@ -122,7 +129,7 @@ i.space-invader.alt-7::before { clear: both; float: left; margin: 0 0 1.5% 0; - font: 400 18px/2.2 "Open Sans", sans-serif; + font: 400 18px/2.2 'Coda', sans-serif; padding: 6px 22px; text-overflow: ellipsis; white-space: nowrap; @@ -280,7 +287,7 @@ i.space-invader.alt-7::before { } .album_title_box h2 { color: #262626; - font: 400 18px/2 "Open Sans", sans-serif !important; + font: 400 18px/2 'Coda', sans-serif !important; margin: 0; } .album_title_box p { @@ -427,13 +434,8 @@ a#previous_photo:hover, a#next_photo:hover { } #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; + margin-bottom: 1rem; } -#sub_photo h3 { - font-size: 16px; -} - #sub_photo #tag_list { list-style: none; margin: 1em 0; -- 2.46.0 From 326c8f11ee867dcd89f7de0ba77ab9f2e2783303 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 16:55:22 +0100 Subject: [PATCH 22/79] Change colours for buttons and page indices --- public/css/default.css | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/public/css/default.css b/public/css/default.css index ecc227b..032ba30 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -34,9 +34,32 @@ a:hover { color: #262626; } +.page-link { + color: #b50707; +} +.page-link:hover { + color: #a40d0d; +} +.active > .page-link, .page-link.active { + background-color: #990b0b; + border-color: #a40d0d; +} + .btn { font-family: 'Coda', 'sans-serif'; } +.btn-primary { + --bs-btn-bg: #6c757d; + --bs-btn-border-color: #6c757d; + --bs-btn-hover-bg: #5c636a; + --bs-btn-hover-border-color: #565e64; + --bs-btn-focus-shadow-rgb: 130, 138, 145; + --bs-btn-active-bg: #565e64; + --bs-btn-active-border-color: #51585e; + --bs-btn-active-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + --bs-btn-disabled-bg: #6c757d; + --bs-btn-disabled-border-color: #6c757d; +} /* Navigation -- 2.46.0 From 7d19cf823df57df92f3bb12bcd229f1f518429d8 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:04:30 +0100 Subject: [PATCH 23/79] Pass aspect ratio into photo thumbnails --- templates/PhotosIndex.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index 25255c5..7fbdf55 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -100,7 +100,7 @@ class PhotosIndex extends Template else echo ' srcset="', $image->getThumbnailUrl($image->width(), $image->height(), true), ' 2x"'; - echo ' alt="" title="', $image->getTitle(), '">'; + echo ' alt="" title="', $image->getTitle(), '" style="aspect-ratio: ', $image->ratio(), '">'; if ($this->show_labels) echo ' -- 2.46.0 From e6f7476037cd1a47750f104ac10fc5d890475a61 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:15:59 +0100 Subject: [PATCH 24/79] MainNavBar: let space invader rotate on hover --- public/css/default.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/css/default.css b/public/css/default.css index 032ba30..8e85c9c 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -89,8 +89,12 @@ i.space-invader::before { text-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6); top: -5px; transform: rotate(-5deg); + transition: 0.25s; width: 110px; } +.navbar-brand:hover i.space-invader::before { + transform: rotate(5deg); +} i.space-invader.alt-1::before { content: 'C'; } -- 2.46.0 From 54c4294d08efc2660cddb0051bc1a11c455f1677 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:16:53 +0100 Subject: [PATCH 25/79] Add 'no thumb' vector image for use while loading --- public/css/default.css | 1 + public/images/nothumb.png | Bin 3694 -> 0 bytes public/images/nothumb.svg | 10 ++++++++++ templates/AlbumIndex.php | 5 +++-- templates/PhotosIndex.php | 2 +- 5 files changed, 15 insertions(+), 3 deletions(-) delete mode 100644 public/images/nothumb.png create mode 100644 public/images/nothumb.svg diff --git a/public/css/default.css b/public/css/default.css index 8e85c9c..3c60ef9 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -180,6 +180,7 @@ i.space-invader.alt-7::before { width: 31%; } .tiled_grid div img { + background: url('../images/nothumb.svg') center no-repeat; border: none; width: 100%; } diff --git a/public/images/nothumb.png b/public/images/nothumb.png deleted file mode 100644 index ce6a2f2d8c7db907ef15baf54f579481e35a5e7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3694 zcma)93pmqzA766I<>)wBa(NqalE&OG3mt8WG?&6g$7Rhw)@)-NyCAQlbWZE-2oVlm zbkyt8mP^WQ#n~YfMNK85NEsQqgz)~SQ|Eo2_w79Io;~0F{=eV%^Z8!?zvsK3e7p{* ztLUgeAP{vAcl2QhWJ4ObZdaBAr3VvM3;t|lxZxN_sK*&hJPm-jhEtCLFb^_50yqrd z!`U%+0VfD#;|`)9j)B7*LK3KCbG%H)oJEcX(GZB!URE@oKmr)BV?YFv;%qq8ATWdx z!<`MWwipXcGzy3$x^rm25ssH1fkPrVh8ym6fjO~|AORU*;9)Fs6ormtIUBC?BEhw6 z8(|1rS7DHx4L>@C!}!2ZR2l%YHMce+SXf%Z?Ci`fZEWrAEa5OK3rj161^Bfyv$R3l z+99p%VSjuKL2I<|<H*D4{eRd3pPUUN8H{Ko0>NZ5&6(EbR9XbW($UdT#$jb;25Okm z*%StzWk#VJePTcZbOMbS%^*@KFc~BM7&VsRYzTV#F$8k-XIcvVk2HY^L$L7C2upJd zSxBE0F_^zrC6hmE(;0_>KmGlm#B@J)G=Mk^(5bOB0$8}?Mlw^;NE8jgGpIB_DmCg; z7JVYA3@Sa68Vy4ov4fdl@B|`7wy|p+fx#d>D0Bv%LI6C_&W0d|IguESw6sE5Ik?)R z?H%`9TUxqVyY92Mb#y>G*t()n_Et9gK4H;RLM$1eFg{_!|HQg|5i4s3ax`ce4bX^j zK=^(dl?+=C8cF=JFZ;g8_Xjro%f6t#h(&<TAY`Tek5Yfy0{cg{{M@+U!{^oqC}6+S zz{Wli9A^rFs0?|aUHw>3CkDtz`o&O1AAx(7dU6~xV|guUU?siZP9aLfX=;lbhq%gV z_NPmOT5Ap!ol;)L7fzN`w}tj3$Xm#NlMUI7gUG3_i|@iV$f<%t8NMzc$qLGFP{8;f zg*+5o$wU8sTW(!!#(}HPU~a~8eGtMv(AU3~<ce@J-w<zbxvKHz(O-vG*m}mkHr$sx za=msEwkjpN2{M6;uY{{B(ysZHxjJd?&KPAL3z>87^j10U-`u$`M%S7(cn^?`0{0g} z_$C1Blz-M-9-1FWt?JvmQ`FkuJfT7ovh_@Pkw;m#5|rUxrDc*t&8AvCRa>k}Q0Wu* zR@G!-jutTYj^v?^kcW<CzFNG{d_EPVKcW<nwS9%;LS|uj>8!r@RgvhhY1NK0m#9bw z%woW~nPWMW{cCoOEmfA8!8#$z@If<#s8nNRtS(RbVD@>?8yxBPJu=R=U=8zhR^F}{ zhIOHTY<{-?kCpK5pZ(7hSq6Mo;iSH!`qBNUAM^$_wC=-mQ|o-RPaB5^Wac?(ti8P6 z;wf`b(ouFoC7(Sz(@H>YL!_HDK5EeA*csfYY8;g`)=l(Ci|@Yo%xzG~j=FVswK23g zEg)ugu`aLf47y2jnq1{W*RLF&n^?}FW?x;-pKI|vEp$8aG+C>uI(v(YV~$sbaQ1l2 zja2NG@&<WmHbGDtKaDcja&k$udS7a~kEI2$&!%FP#ivuVZ@<tSxo41$ZevGH=!C?u z_5`;|Tw5i_UgLvnmgingjgGgitqv-Rz5U{z>x2Z}aJf_9ua3}yZudE|araLp87`ti z`^K{;@(XVa+y1O)Y{1v9$}$3=F$p)QgnsGSE5jHtT(^?OMfV(5KVs@TC3W6pc~KR~ zVwP{-*XPfv3ljK*7*CJ&d#^0INI02Tosg@1^T(9^UV~Ek^YL?)scP(}Qd6cnZ(plt zSj?`nS4O~bkEHEd4PbPG;{&0?YU(^b&5yYWjubOm>8#@4dx>j;wQ06)2>u?c;Zpo# zWqAFA*`=Y)p)R^q>W*A=?+7RBm*Qcq7754Y-KDQ_jFLgGeAeRqX11or5^ug@_@WlR zmUSw%4?c4UJRO-%8yoBHeE-sE$G0!1S0OE-QLM=`795Zlc@&-4+(Zo0Jo%g8EyA`j z-K2*xCHlF?1#ZqHeyAL-O$7MHxAxscXbPVC<L*!LBva}<c6$YasiCeVepsuCbBYnf zE9A%ZRga+aigWiXk#%wNX)5pnu-!AhQu?5MEj^r(I~%E^Z!?>X*{jV<ynVGIZ(*fP z&NIxlaK!DU$&<&aipdI1Jyr+BiKpaLIV1ll3I7^*zbR|7<flC}dFbrHBO6J~=+ish z1@9Gln04I6UkBO)`sVsZm!guMneOQ_36zKS+jUH|#0B-0Ja{5UVv>eMTH=<k;GX+l z_i?1NGQMcNK>9|}4vS=km8!vTL^k%i5dd;~h)kZ``e;RD5_q6P8UFeShyUjMFAlT= z=WqXTlBj2#chl$q=xS!Gp7Cz}kfEqN%eU$qb$GHuVvpUyfVG2Ru1N_)zEyJEBA1;c zY>{ifNg#7%_EO=!Gx4SYbF^v?X3Bh=5%)^rtLmI!H*a#TE-uwc72Xw|Ke`~9_1P2k z{4N)$zZKk>)DZHzQ}6Qm%<DTY9}Nt2@hWe7D}J*#!U<nC#U16ucf96q4|<$z%y($Z zh_+F=@X+dsMR-6!)|FMevt{FJwH@VP-&W)+KqFY1SwOF3R;k=(PwY?jKO!j^w)T9z zT}FI|l&XZL8sbw^c8%r~zE%x?f7x*Ny^yacx;paEh{KQcr|>sJuSf=vq}scjlSN{g zqxD*tP-^p`omZA(&IpHwW9y_|g$qf5><GEFAnM)wix^Yvm2Il^p=vIJo)?b0Q4yI2 z-QZRD0oL}ie6eEm8)yup_;2^{mMm#n=}O~48FJ}GFB}Xx^n;H{^Xkn-#@VIe>0{Ec z{86_jZQ^)IT7<5S;%%=54KPJM?wIU1ucS3<z8BFCCLev+BFT^7-r4CU{&sog#9*DI z7da9bwh7+#mXr1Uy>zRGnz7D0KNsKK<n6f_WBI;mrsG|G!9wM(hpfxzWQ7jiMh&$| zSU9#<{#iruq)<3?=f+&YSd&h{Guq|Oi~6{YMQ4NozW!?s@h0U<lisVP!l4C!eEpj~ z4ximluK?4pot6~Pzr1;^F^P1%7OuFRC*VBdP`Aw_)a##6hM$M;5Yd=<`1T6HU|QfL z_s$?R@E@oGBUFI`bffR1Sd;ALz2%{ziaHRfW$QM!pr*f?3o6+1<)NsUI?l;c0rAJ1 z087^9=9zD&gx8odV9Hh~lPTF6_p3c1?%$g79D7<4GQoL1B1>gzRMM-^-UCUU6-+b5 z?MeC8dV23Um3?W|))LobAq;LGxSW&JZ6yij-f8nnj2<cvN_8TE39CosXYuq8Y8AAW z+c%zCFsmM4+6hg0zBN6#Zt$p`jrNQaJgz?JyS&NsWkCCnd9U8-&g=39Z<a1k6^0%V zSd3bHa4pJgc>C7jRH<T?LANPL$eZuqvU(CXz_Z6}nz^QbSm2Ton?o6YYwq5A!AhJW z$9*d*M@5WJjjHDOQ|qjLiT6YQ*xpzoZjkj$b2e6~q4Qv&xnge(x5-ySLWyhAoq_YU zzhlptREy8t3vder*n@W$0!|&i!u^QBXJVB?nK}MbD%WaCD#wPqgMqQG4zWSE4z87; zzII1c-j*}hO&gd@Oe|;6ojtn(`)a;o&kteo4+Jyg(_;)$KL^qElw%LJkY(dc+1{>% zp+miCW50F!CA$@h$_q?7W+umSZ>Vlq>1#FpQ2j>rh8kOZTgODi8hvyNtb)@T<8ts+ zQAmGw)VqO)LMha{^vpeptSp&W!T6TSg@I1)v6}Cvy7x5a8V^7JcyK^D+h@7ciyk$& zYI|osUNCUM`g;|^sJ~LZvm_C^>|EJWtP1!~#@nOLJ@pD$f79xh7ct+OMYKyx{<VV5 zI%Tm1>+!-+8SYRw?cLN9(O!M~2fPIs!<{f~Fk@j3T3PxyIn~q_+0D9f7f)}Ob;3M; zZh(^k2!WN5e|gi>tLFZB`^CsxMb;Q5bT>R1@K8oAj!w95OIAo3lF8Oh6*;okMeVm| z=k&lw_uaBSR67k;N>#B{wv~ckH+!k=!EgGGpfTSN;q{xEI14o$(6nM}S_G)Fi}&n; z5kE}(m-{gOKera&9eK3+5PrRAn6Pp`ST1$xalV(!^OI%c^(N&u-Mw({cQ1%+K*kzU z`StJMz?E9pFKnzBuP_k@hGYWD2$>7BSmmpHcQxu<q+gc5ZCUU);6-d7*Zlf`+0|gV z{~rFNo>6*}7uXYR8_2(h_B`{4z&CC9%Rx{!K>i<7<X?=EGKTHqH3e-5Kg9y2<QaBJ Q_7Bj*%?o{PU)T@-1_Re;XaE2J diff --git a/public/images/nothumb.svg b/public/images/nothumb.svg new file mode 100644 index 0000000..339274a --- /dev/null +++ b/public/images/nothumb.svg @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 150"> +<defs><style>.cls-2{fill:#cc9d9d;}</style></defs> +<g> + <path class="cls-2" d="m221.34,135.39c-13.69,0-27.38-.09-41.07.06-3.43.04-4.94-.61-4.91-4.56.17-27.21.14-54.42.02-81.63-.02-3.41.9-4.57,4.45-4.56,27.69.13,55.38.12,83.07,0,3.36-.01,4.19,1.18,4.18,4.34-.1,27.37-.12,54.73.02,82.1.02,3.73-1.44,4.34-4.69,4.3-13.69-.14-27.38-.06-41.07-.06Zm-.11-27.1c11.37,0,22.74-.1,34.1.06,3.26.05,4.3-.97,4.28-4.25-.14-16.19-.14-32.38,0-48.56.03-3.28-1.01-4.27-4.27-4.25-22.74.12-45.47.12-68.21,0-3.26-.02-4.3.97-4.27,4.25.14,16.19.14,32.38,0,48.56-.03,3.28,1.01,4.3,4.27,4.26,11.37-.16,22.74-.06,34.1-.06Z"/> + <path class="cls-2" d="m271.69,111.12c.4-3.72-.27-8.33-.9-12.95-.4-2.96.59-3.73,3.62-3.01,6.71,1.61,6.75,1.45,8.74-5.81,3.66-13.3,7.37-26.59,10.95-39.91,1.64-6.09,1.55-6.23-4.53-7.87-20.8-5.63-41.65-11.12-62.43-16.82-3.48-.95-5.32-.26-6.11,3.33-.73,3.33-1.85,6.57-2.55,9.9-.71,3.39-3,4.22-5.87,3.73-3.34-.57-2.27-2.94-1.71-5.06,1.7-6.44,3.31-12.91,5.03-19.34.47-1.74.7-3.35,3.66-2.54,27.36,7.52,54.77,14.85,82.2,22.1,2.71.72,3.31,1.43,2.52,4.29-7.26,26.45-14.3,52.97-21.49,79.44-.5,1.84-.24,5.23-3.51,4.25-3.05-.92-8.22.3-7.68-5.77.21-2.32.03-4.67.03-7.96Z"/> + <path class="cls-2" d="m237.89,68.65c3.58,9.04,7.13,18.07,10.74,27.08.87,2.17.4,3.25-2.07,3.25-16.63-.01-33.25,0-49.88-.01-2.63,0-2.8-1.35-1.8-3.33.7-1.39,1.37-2.79,2.07-4.17,2.84-5.69,2.92-5.78,8.04-1.6,1.77,1.44,2.44,1.1,3.45-.67,1.69-2.95,3.7-5.72,5.45-8.64,1.39-2.31,2.67-2.5,4.73-.62,2.11,1.93,3.79,5.97,6.49,5.2,2.2-.63,3.51-4.41,5.19-6.81,2.13-3.04,4.23-6.1,6.37-9.13.15-.21.54-.25,1.23-.54Z"/> + <path class="cls-2" d="m201.38,75.62c-3.33.17-5.32-1.1-5.41-4.73-.09-3.64,1.37-6.17,5.12-6.38,3.38-.19,5.57,1.83,6,5.22.4,3.09-2.39,5.81-5.72,5.89Z"/> +</g> +</svg> diff --git a/templates/AlbumIndex.php b/templates/AlbumIndex.php index 56635a4..3205b81 100644 --- a/templates/AlbumIndex.php +++ b/templates/AlbumIndex.php @@ -55,11 +55,12 @@ class AlbumIndex extends Template echo ' <img src="', $thumbs[1], '"' . (isset($thumbs[2]) ? ' srcset="' . $thumbs[2] . ' 2x"' : '') . - ' alt="">'; + ' alt="" style="width: ', static::TILE_WIDTH, + 'px; height: ', static::TILE_HEIGHT, 'px">'; } else echo ' - <img src="', BASEURL, '/images/nothumb.png" alt="">'; + <img src="', BASEURL, '/images/nothumb.svg" alt="">'; if ($this->show_labels) echo ' diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index 7fbdf55..29fa06a 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -100,7 +100,7 @@ class PhotosIndex extends Template else echo ' srcset="', $image->getThumbnailUrl($image->width(), $image->height(), true), ' 2x"'; - echo ' alt="" title="', $image->getTitle(), '" style="aspect-ratio: ', $image->ratio(), '">'; + echo ' alt="" title="', $image->getTitle(), '" style="width: ', $width, 'px; height: ', $height, 'px">'; if ($this->show_labels) echo ' -- 2.46.0 From 4d47696dcd9050bedb2998ea0302bcbe3a836821 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:20:22 +0100 Subject: [PATCH 26/79] Use Coda font for page links, too --- public/css/default.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/css/default.css b/public/css/default.css index 3c60ef9..4967e99 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -36,6 +36,7 @@ a:hover { .page-link { color: #b50707; + font-family: 'Coda', sans-serif; } .page-link:hover { color: #a40d0d; @@ -46,7 +47,7 @@ a:hover { } .btn { - font-family: 'Coda', 'sans-serif'; + font-family: 'Coda', sans-serif; } .btn-primary { --bs-btn-bg: #6c757d; -- 2.46.0 From 4033a8813cc8f58a6d15a89c6d1a974aafc1d51f Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:23:44 +0100 Subject: [PATCH 27/79] EditTag: hide option for assigning parent --- controllers/EditTag.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index 4b42926..adec592 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -65,10 +65,6 @@ class EditTag extends HTMLController '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', @@ -118,6 +114,7 @@ class EditTag extends HTMLController return $formview->adopt(new Alert('Some data missing', 'Please fill out the following fields: ' . implode(', ', $form->getMissing()), 'danger')); $data = $form->getData(); + $data['id_parent'] = 0; // Quick stripping. $data['slug'] = strtr($data['slug'], [' ' => '-', '--' => '-', '&' => 'and', '=>' => '', "'" => "", ":"=> "", '/' => '-', '\\' => '-']); -- 2.46.0 From 4684482d6719ba0b2b2c147597ff606daf705f52 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:28:21 +0100 Subject: [PATCH 28/79] ManageAlbums: move hierarchy logic to PhotoAlbum model --- controllers/ManageAlbums.php | 61 +---------------------------- models/PhotoAlbum.php | 76 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 60 deletions(-) create mode 100644 models/PhotoAlbum.php diff --git a/controllers/ManageAlbums.php b/controllers/ManageAlbums.php index 6f578b1..662714b 100644 --- a/controllers/ManageAlbums.php +++ b/controllers/ManageAlbums.php @@ -68,28 +68,7 @@ class ManageAlbums extends HTMLController 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); + $rows = PhotoAlbum::getHierarchy($order, $direction); return [ 'rows' => $rows, @@ -106,42 +85,4 @@ class ManageAlbums extends HTMLController 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; - } } diff --git a/models/PhotoAlbum.php b/models/PhotoAlbum.php new file mode 100644 index 0000000..efdc41f --- /dev/null +++ b/models/PhotoAlbum.php @@ -0,0 +1,76 @@ +<?php +/***************************************************************************** + * PhotoAlbum.php + * Contains key class PhotoAlbum. + * + * Kabuki CMS (C) 2013-2015, Aaron van Geffen + *****************************************************************************/ + +class PhotoAlbum extends Tag +{ + public static function getHierarchy($order, $direction) + { + $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; + } + + 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; + } +} -- 2.46.0 From 54df35073daf6efce123ca30f5e06539fb0e4295 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:35:47 +0100 Subject: [PATCH 29/79] EditAlbum: make parent selection more intuitive --- controllers/EditAlbum.php | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index 5888876..e2aa20c 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -61,13 +61,24 @@ class EditAlbum extends HTMLController elseif (!$id_tag) $after_form = '<button name="submit_and_new" class="btn">Save and add another</button>'; + // Gather possible parents for this album to be filed into + $parentChoices = [0 => '-root-']; + foreach (PhotoAlbum::getHierarchy('tag', 'up') as $parent) + { + if (!empty($id_tag) && $parent['id_tag'] == $id_tag) + continue; + + $parentChoices[$parent['id_tag']] = $parent['tag']; + } + $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', + 'type' => 'select', + 'label' => 'Parent album', + 'options' => $parentChoices, ], 'id_asset_thumb' => [ 'type' => 'numeric', @@ -127,6 +138,12 @@ class EditAlbum extends HTMLController $data = $form->getData(); + // Sanity check: don't let an album be its own parent + if ($data['id_parent'] == $id_tag) + { + return $formview->adopt(new Alert('Invalid parent', 'An album cannot be its own parent.', 'danger')); + } + // Quick stripping. $data['tag'] = htmlentities($data['tag']); $data['description'] = htmlentities($data['description']); -- 2.46.0 From fa01bf8961cba7a59872a550a8f371b6b1b746c7 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:53:53 +0100 Subject: [PATCH 30/79] ManageAssets: trade filename for user uploaded field --- controllers/ManageAssets.php | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/controllers/ManageAssets.php b/controllers/ManageAssets.php index 463ede5..4598c8e 100644 --- a/controllers/ManageAssets.php +++ b/controllers/ManageAssets.php @@ -38,13 +38,17 @@ class ManageAssets extends HTMLController 'data' => 'filename', ], ], - 'title' => [ - 'header' => 'Title', + 'id_user_uploaded' => [ + 'header' => 'User uploaded', 'is_sortable' => true, 'parse' => [ - 'type' => 'value', - 'link' => BASEURL . '/editasset/?id={ID_ASSET}', - 'data' => 'title', + 'type' => 'function', + 'data' => function($row) { + if (!empty($row['first_name'])) + return $row['first_name'] . ' ' . $row['surname']; + else + return 'n/a'; + }, ], ], 'dimensions' => [ @@ -69,12 +73,15 @@ class ManageAssets extends HTMLController 'items_per_page' => 30, 'base_url' => BASEURL . '/manageassets/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') { - if (!in_array($order, ['id_asset', 'title', 'subdir', 'filename'])) + if (!in_array($order, ['id_asset', 'id_user_uploaded', 'title', 'subdir', 'filename'])) $order = 'id_asset'; $data = Registry::get('db')->queryAssocs(' - SELECT id_asset, title, subdir, filename, image_width, image_height - FROM assets + SELECT a.id_asset, a.subdir, a.filename, + a.image_width, a.image_height, + u.id_user, u.first_name, u.surname + FROM assets AS a + LEFT JOIN users AS u ON a.id_user_uploaded = u.id_user ORDER BY {raw:order} LIMIT {int:offset}, {int:limit}', [ -- 2.46.0 From a9a347c638b0f870102db5788a83bd5a5c2b7962 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 17:59:57 +0100 Subject: [PATCH 31/79] Adjust dropdown focus colours --- public/css/default.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/public/css/default.css b/public/css/default.css index 4967e99..df3e65c 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -62,6 +62,10 @@ a:hover { --bs-btn-disabled-border-color: #6c757d; } +.dropdown-item.active, .dropdown-item:active { + background-color: #990b0b; +} + /* Navigation ---------------*/ -- 2.46.0 From edc857f6fd91d5c52c9e0b01c4f3c7b0516d7164 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 18:22:27 +0100 Subject: [PATCH 32/79] EditTag: introduce featured thumbnail manager --- controllers/EditTag.php | 11 +++++++ public/css/default.css | 30 ++++++++++++++++++ templates/FeaturedThumbnailManager.php | 42 ++++++++++++++++++++++++++ 3 files changed, 83 insertions(+) create mode 100644 templates/FeaturedThumbnailManager.php diff --git a/controllers/EditTag.php b/controllers/EditTag.php index adec592..c3d1382 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -105,6 +105,17 @@ class EditTag extends HTMLController $formview = new FormView($form, $form_title ?? ''); $this->page->adopt($formview); + if (!empty($id_tag)) + { + list($assets, $num_assets) = AssetIterator::getByOptions([ + 'direction' => 'desc', + 'limit' => 500, + 'id_tag' => $id_tag, + ], true); + + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0)); + } + if (!empty($_POST)) { $form->verify($_POST); diff --git a/public/css/default.css b/public/css/default.css index df3e65c..1732722 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -355,6 +355,36 @@ i.space-invader.alt-7::before { } +/* Featured thumbnail selection +---------------------------------*/ +#featuredThumbnail { + list-style: none; + margin: 2.5% 0 0; + padding: 0; + clear: both; + overflow: auto; +} +#featuredThumbnail li { + float: left; + width: 18%; + line-height: 0; + margin: 0 1% 2%; + min-width: 200px; + height: 149px; + position: relative; +} +#featuredThumbnail input { + position: absolute; + top: 0.5rem; + right: 0.5rem; + z-index: 100; +} +#featuredThumbnail img { + width: 100%; + box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.2); +} + + /* Footer -----------*/ footer { diff --git a/templates/FeaturedThumbnailManager.php b/templates/FeaturedThumbnailManager.php new file mode 100644 index 0000000..910ecdf --- /dev/null +++ b/templates/FeaturedThumbnailManager.php @@ -0,0 +1,42 @@ +<?php +/***************************************************************************** + * FeaturedThumbnailManager.php + * Contains the featured thumbnail manager template. + * + * Kabuki CMS (C) 2013-2021, Aaron van Geffen + *****************************************************************************/ + +class FeaturedThumbnailManager extends SubTemplate +{ + private $assets; + private $currentThumbnailId; + + public function __construct(AssetIterator $assets, $currentThumbnailId) + { + $this->assets = $assets; + $this->currentThumbnailId = $currentThumbnailId; + } + + protected function html_content() + { + echo ' + <h2>Select thumbnail</h2> + <ul id="featuredThumbnail">'; + + while ($asset = $this->assets->next()) + { + $image = $asset->getImage(); + echo ' + <li> + <input class="form-check-input" type="radio" name="featuredThumbnail" value="', $image->getId(), '"', + $this->currentThumbnailId == $image->getId() ? ' checked' : '', '> + <img src="', $image->getThumbnailUrl(150, 100, 'top'), '" alt="" title="', $image->getTitle(), '" onclick="this.parentNode.children[0].checked = true"> + </li>'; + } + + $this->assets->clean(); + + echo ' + </ul>'; + } +} -- 2.46.0 From cf0b9ebaf9decf8e9bd823fd760c48054b4f69b0 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 19:34:01 +0100 Subject: [PATCH 33/79] LogInForm: change title to something #RU-like --- templates/LogInForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/LogInForm.php b/templates/LogInForm.php index 0c28e70..cafb51e 100644 --- a/templates/LogInForm.php +++ b/templates/LogInForm.php @@ -27,7 +27,7 @@ class LogInForm extends SubTemplate { if (!empty($this->_title)) echo ' - <h1 class="mb-4">Log in to your account</h1>'; + <h1 class="mb-4">Press #RU to continue</h1>'; if (!empty($this->_subtemplates)) { -- 2.46.0 From a06902335bb839e4909fb5d57d870a314f53c018 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 19:34:52 +0100 Subject: [PATCH 34/79] Manage{Tags,Users}: add call to resetSessionToken --- controllers/ManageTags.php | 2 ++ controllers/ManageUsers.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/controllers/ManageTags.php b/controllers/ManageTags.php index ff49780..26f2b49 100644 --- a/controllers/ManageTags.php +++ b/controllers/ManageTags.php @@ -14,6 +14,8 @@ class ManageTags extends HTMLController if (!Registry::get('user')->isAdmin()) throw new NotAllowedException(); + Session::resetSessionToken(); + $options = [ 'form' => [ 'action' => BASEURL . '/edittag/', diff --git a/controllers/ManageUsers.php b/controllers/ManageUsers.php index cf53e24..1be66a5 100644 --- a/controllers/ManageUsers.php +++ b/controllers/ManageUsers.php @@ -14,6 +14,8 @@ class ManageUsers extends HTMLController if (!Registry::get('user')->isAdmin()) throw new NotAllowedException(); + Session::resetSessionToken(); + $options = [ 'form' => [ 'action' => BASEURL . '/edituser/', -- 2.46.0 From 6d0aef4df659b7eddbed9d9e98c7e53ddb51c6e1 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 19:49:17 +0100 Subject: [PATCH 35/79] EditTag: allow updating the thumbnail visually --- controllers/EditTag.php | 39 ++++++++++++++++++-------- models/Tag.php | 4 ++- templates/FeaturedThumbnailManager.php | 6 +++- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index c3d1382..a26107f 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -10,14 +10,18 @@ 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.'); + if (!empty($id_tag)) + $tag = Tag::fromId($id_tag); + + // Are we allowed to edit this tag? + $user = Registry::get('user'); + if (!($user->isAdmin() || $user->getUserId() == $tag->id_user_owner)) + throw new NotAllowedException(); + // Adding an tag? if (isset($_GET['add'])) { @@ -29,7 +33,6 @@ class EditTag extends HTMLController 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/'); @@ -41,7 +44,6 @@ class EditTag extends HTMLController // 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); @@ -65,11 +67,6 @@ class EditTag extends HTMLController 'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'content_below' => $after_form, 'fields' => [ - 'id_asset_thumb' => [ - 'type' => 'numeric', - 'label' => 'Thumbnail asset ID', - 'is_optional' => true, - ], 'kind' => [ 'type' => 'select', 'label' => 'Kind of tag', @@ -116,6 +113,26 @@ class EditTag extends HTMLController $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0)); } + if (isset($_POST['changeThumbnail'])) + $this->processThumbnail($tag); + elseif (!empty($_POST)) + $this->processTagDetails($form, $id_tag, $tag); + } + + private function processThumbnail($tag) + { + if (empty($_POST)) + return; + + $tag->id_asset_thumb = $_POST['featuredThumbnail']; + $tag->save(); + + header('Location: ' . BASEURL . '/edittag/?id=' . $tag->id_tag); + exit; + } + + private function processTagDetails($form, $id_tag, $tag) + { if (!empty($_POST)) { $form->verify($_POST); diff --git a/models/Tag.php b/models/Tag.php index e6b6f13..d5bbccc 100644 --- a/models/Tag.php +++ b/models/Tag.php @@ -11,6 +11,7 @@ class Tag public $id_tag; public $id_parent; public $id_asset_thumb; + public $id_user_owner; public $tag; public $slug; public $description; @@ -258,7 +259,8 @@ class Tag UPDATE tags SET id_parent = {int:id_parent}, - id_asset_thumb = {int:id_asset_thumb}, + id_asset_thumb = {int:id_asset_thumb},' . (isset($this->id_user_owner) ? ' + id_user_owner = {int:id_user_owner},' : '') . ' tag = {string:tag}, slug = {string:slug}, description = {string:description}, diff --git a/templates/FeaturedThumbnailManager.php b/templates/FeaturedThumbnailManager.php index 910ecdf..ffbe1c0 100644 --- a/templates/FeaturedThumbnailManager.php +++ b/templates/FeaturedThumbnailManager.php @@ -20,6 +20,8 @@ class FeaturedThumbnailManager extends SubTemplate protected function html_content() { echo ' + <form action="" method="post"> + <button class="btn btn-primary float-end" type="submit" name="changeThumbnail">Save thumbnail selection</button> <h2>Select thumbnail</h2> <ul id="featuredThumbnail">'; @@ -37,6 +39,8 @@ class FeaturedThumbnailManager extends SubTemplate $this->assets->clean(); echo ' - </ul>'; + </ul> + <input type="hidden" name="', Session::getSessionTokenKey(), '" value="', Session::getSessionToken(), '"> + </form>'; } } -- 2.46.0 From 59b1fa7a72a1bcbb16cd7f367573a5f93dec06f8 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 19:52:30 +0100 Subject: [PATCH 36/79] EditAlbum: allow updating the thumbnail visually --- controllers/EditAlbum.php | 41 ++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index e2aa20c..76d549d 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -18,6 +18,9 @@ class EditAlbum extends HTMLController if (empty($id_tag) && !isset($_GET['add']) && $_GET['action'] !== 'addalbum') throw new UnexpectedValueException('Requested album not found or not requesting a new album.'); + if (!empty($id_tag)) + $album = Tag::fromId($id_tag); + // Adding an album? if (isset($_GET['add']) || $_GET['action'] === 'addalbum') { @@ -29,7 +32,6 @@ class EditAlbum extends HTMLController 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/'); @@ -41,7 +43,6 @@ class EditAlbum extends HTMLController // 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); @@ -80,11 +81,6 @@ class EditAlbum extends HTMLController 'label' => 'Parent album', 'options' => $parentChoices, ], - 'id_asset_thumb' => [ - 'type' => 'numeric', - 'label' => 'Thumbnail asset ID', - 'is_optional' => true, - ], 'tag' => [ 'type' => 'text', 'label' => 'Album title', @@ -128,6 +124,37 @@ class EditAlbum extends HTMLController $formview = new FormView($form, $form_title ?? ''); $this->page->adopt($formview); + if (!empty($id_tag)) + { + list($assets, $num_assets) = AssetIterator::getByOptions([ + 'direction' => 'desc', + 'limit' => 500, + 'id_tag' => $id_tag, + ], true); + + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0)); + } + + if (isset($_POST['changeThumbnail'])) + $this->processThumbnail($album); + elseif (!empty($_POST)) + $this->processTagDetails($form, $id_tag, $album); + } + + private function processThumbnail($tag) + { + if (empty($_POST)) + return; + + $tag->id_asset_thumb = $_POST['featuredThumbnail']; + $tag->save(); + + header('Location: ' . BASEURL . '/editalbum/?id=' . $tag->id_tag); + exit; + } + + private function processTagDetails($form, $id_tag, $album) + { if (!empty($_POST)) { $form->verify($_POST); -- 2.46.0 From ad816f10a36e6a6825d4fe89cc98c7c24866e867 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 19:57:19 +0100 Subject: [PATCH 37/79] EditTag: allow designating a tag owner --- controllers/EditTag.php | 5 +++++ models/Member.php | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index a26107f..d752223 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -75,6 +75,11 @@ class EditTag extends HTMLController 'Person' => 'Person', ], ], + 'id_user_owner' => [ + 'type' => 'select', + 'label' => 'Owner', + 'options' => [0 => '(nobody)'] + Member::getMemberMap(), + ], 'tag' => [ 'type' => 'text', 'label' => 'Tag title', diff --git a/models/Member.php b/models/Member.php index 6675dac..84912a4 100644 --- a/models/Member.php +++ b/models/Member.php @@ -192,4 +192,15 @@ class Member extends User // We should probably phase out the use of this function, or refactor the access levels of member properties... return get_object_vars($this); } + + public static function getMemberMap() + { + return Registry::get('db')->queryPair(' + SELECT id_user, CONCAT(first_name, {string:blank}, surname) AS full_name + FROM users + ORDER BY first_name, surname', + [ + 'blank' => ' ', + ]); + } } -- 2.46.0 From 27f69b0a74728f48ebceb9b62b679b58742755de Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:01:25 +0100 Subject: [PATCH 38/79] EditTag: disallow users to disown their own tags --- controllers/EditTag.php | 76 +++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index d752223..12de092 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -63,43 +63,51 @@ class EditTag extends HTMLController elseif (!$id_tag) $after_form = '<button name="submit_and_new" class="btn">Save and add another</button>'; + $fields = [ + 'kind' => [ + 'type' => 'select', + 'label' => 'Kind of tag', + 'options' => [ + 'Location' => 'Location', + 'Person' => 'Person', + ], + ], + 'id_user_owner' => [ + 'type' => 'select', + 'label' => 'Owner', + 'options' => [0 => '(nobody)'] + Member::getMemberMap(), + ], + '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, + ], + ]; + + if (!$user->isAdmin()) + { + unset($fields['kind']); + unset($fields['id_user_owner']); + } + $form = new Form([ 'request_url' => BASEURL . '/edittag/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'content_below' => $after_form, - 'fields' => [ - 'kind' => [ - 'type' => 'select', - 'label' => 'Kind of tag', - 'options' => [ - 'Location' => 'Location', - 'Person' => 'Person', - ], - ], - 'id_user_owner' => [ - 'type' => 'select', - 'label' => 'Owner', - 'options' => [0 => '(nobody)'] + Member::getMemberMap(), - ], - '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, - ], - ], + 'fields' => $fields, ]); // Create the form, add in default values. -- 2.46.0 From daa8b051c57c3853ad5ae3c51bdf959239ec97da Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:03:09 +0100 Subject: [PATCH 39/79] EditTag: on saving, redirect users to a page they can see --- controllers/EditTag.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index 12de092..fcb0e96 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -182,8 +182,11 @@ class EditTag extends HTMLController $tag->save(); } - // Redirect to the tag management page. - header('Location: ' . BASEURL . '/managetags/'); + // Redirect to a clean page + if (Registry::get('user')->isAdmin()) + header('Location: ' . BASEURL . '/managetags/'); + else + header('Location: ' . BASEURL . '/edittag/?id=' . $id_tag); exit; } } -- 2.46.0 From a76dde927b799dc5370601745d5bb583a2dfca86 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:27:09 +0100 Subject: [PATCH 40/79] AccountSettings: list tags owned by current user --- controllers/AccountSettings.php | 5 +++++ models/Tag.php | 19 +++++++++++++++++++ templates/MyTagsView.php | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 templates/MyTagsView.php diff --git a/controllers/AccountSettings.php b/controllers/AccountSettings.php index 4bbb056..4f4a431 100644 --- a/controllers/AccountSettings.php +++ b/controllers/AccountSettings.php @@ -73,6 +73,11 @@ class AccountSettings extends HTMLController $formview = new FormView($form, $form_title); $this->page->adopt($formview); + // Fetch user tags + $tags = Tag::getAllByOwner($user->getUserId()); + if (!empty($tags)) + $this->page->adopt(new MyTagsView($tags)); + // Left a message? if (isset($_SESSION['account_msg'])) { diff --git a/models/Tag.php b/models/Tag.php index d5bbccc..13af5c7 100644 --- a/models/Tag.php +++ b/models/Tag.php @@ -96,6 +96,25 @@ class Tag return $rows; } + public static function getAllByOwner($id_user_owner) + { + $db = Registry::get('db'); + $res = $db->query(' + SELECT * + FROM tags + WHERE id_user_owner = {int:id_user_owner} + ORDER BY tag', + [ + 'id_user_owner' => $id_user_owner, + ]); + + $objects = []; + while ($row = $db->fetch_assoc($res)) + $objects[$row['id_tag']] = new Tag($row); + + return $objects; + } + public static function getAlbums($id_parent = 0, $offset = 0, $limit = 24, $return_format = 'array') { $rows = Registry::get('db')->queryAssocs(' diff --git a/templates/MyTagsView.php b/templates/MyTagsView.php new file mode 100644 index 0000000..2b06fd9 --- /dev/null +++ b/templates/MyTagsView.php @@ -0,0 +1,32 @@ +<?php +/***************************************************************************** + * MyTagsView.php + * Contains the user tag list. + * + * Kabuki CMS (C) 2013-2015, Aaron van Geffen + *****************************************************************************/ + +class MyTagsView extends SubTemplate +{ + private $tags; + + public function __construct(array $tags) + { + $this->tags = $tags; + } + + protected function html_content() + { + echo ' + <h2>Tags you can edit</h2> + <p>You can currently edit the tags below. Click a tag to edit it.</p> + <ul>'; + + foreach ($this->tags as $tag) + echo ' + <li><a href="', BASEURL, '/edittag/?id=', $tag->id_tag, '">', $tag->tag, '</a></li>'; + + echo ' + </ul>'; + } +} -- 2.46.0 From d9fd2ae20dc08b7e85dc8b4cde47c526684dd8fb Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:27:45 +0100 Subject: [PATCH 41/79] Add upgrade script for new tag ownership --- upgrade.sql | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 upgrade.sql diff --git a/upgrade.sql b/upgrade.sql new file mode 100644 index 0000000..7e07e9c --- /dev/null +++ b/upgrade.sql @@ -0,0 +1,15 @@ +/* 2023-03-11 Allow designating an owner for each tag */ +ALTER TABLE `tags` ADD `id_user_owner` INT NULL DEFAULT NULL AFTER `id_asset_thumb`; + +/* 2023-03-11 Try to assign tag owners automagically */ +UPDATE tags AS t +SET id_user_owner = ( + SELECT id_user + FROM users AS u + WHERE LOWER(u.first_name) = LOWER(t.slug) OR + LOWER(u.first_name) = LOWER(t.tag) OR + LOWER(u.slug) = LOWER(t.slug) OR + LOWER(u.slug) = LOWER(t.tag) + ) +WHERE t.kind = 'Person' AND + (t.id_user_owner = 0 OR t.id_user_owner IS NULL); -- 2.46.0 From 167a50cb9281794d49df7bb4fb08ab3485279734 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:34:58 +0100 Subject: [PATCH 42/79] ViewPhotoAlbum: tweak album buttons to be more useful --- controllers/ViewPhotoAlbum.php | 75 ++++++++++++++++++++++++---------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index aa3d632..d45588b 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -60,27 +60,7 @@ 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, - 'caption' => 'Create new subalbum here', - ]; - - // Enough actions for a button box? + $buttons = $this->getAlbumButtons($id_tag, $tag ?? null); if (!empty($buttons)) $this->page->adopt(new AlbumButtonBox($buttons)); @@ -164,6 +144,59 @@ class ViewPhotoAlbum extends HTMLController return $albums; } + private function getAlbumButtons($id_tag, $tag) + { + $buttons = []; + $user = Registry::get('user'); + + if ($user->isLoggedIn()) + { + $buttons[] = [ + 'url' => BASEURL . '/download/?tag=' . $id_tag, + 'caption' => 'Download album', + ]; + } + + if (isset($tag)) + { + if ($tag->kind === 'Album') + { + $buttons[] = [ + 'url' => BASEURL . '/uploadmedia/?tag=' . $id_tag, + 'caption' => 'Upload photos here', + ]; + } + + if ($user->isAdmin()) + { + if ($tag->kind === 'Album') + { + $buttons[] = [ + 'url' => BASEURL . '/editalbum/?id=' . $id_tag, + 'caption' => 'Edit album', + ]; + } + elseif ($tag->kind === 'Person') + { + $buttons[] = [ + 'url' => BASEURL . '/edittag/?id=' . $id_tag, + 'caption' => 'Edit tag', + ]; + } + } + } + + if ($user->isAdmin() && (!isset($tag) || $tag->kind === 'Album')) + { + $buttons[] = [ + 'url' => BASEURL . '/addalbum/?tag=' . $id_tag, + 'caption' => 'Create subalbum', + ]; + } + + return $buttons; + } + public function __destruct() { if (isset($this->iterator)) -- 2.46.0 From 310fe7c3d6ddc5d50bc5b2da165c5ebd13fb635e Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:37:39 +0100 Subject: [PATCH 43/79] Hide thumbnail selection when none available --- controllers/EditAlbum.php | 3 ++- controllers/EditTag.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index 76d549d..c24787d 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -132,7 +132,8 @@ class EditAlbum extends HTMLController 'id_tag' => $id_tag, ], true); - $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0)); + if ($num_assets > 0) + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0)); } if (isset($_POST['changeThumbnail'])) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index fcb0e96..c96ef0e 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -123,7 +123,8 @@ class EditTag extends HTMLController 'id_tag' => $id_tag, ], true); - $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0)); + if ($num_assets > 0) + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $tag->id_asset_thumb : 0)); } if (isset($_POST['changeThumbnail'])) -- 2.46.0 From 5cff62836e28dca61079c5e43e7a7a0c8daea59d Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:39:55 +0100 Subject: [PATCH 44/79] ManageTags: display owning user in table --- controllers/ManageTags.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/controllers/ManageTags.php b/controllers/ManageTags.php index 26f2b49..cb1ffd2 100644 --- a/controllers/ManageTags.php +++ b/controllers/ManageTags.php @@ -50,10 +50,18 @@ class ManageTags extends HTMLController 'data' => 'slug', ], ], - 'kind' => [ - 'header' => 'Kind', + 'id_user_owner' => [ + 'header' => 'Owning user', 'is_sortable' => true, - 'value' => 'kind', + 'parse' => [ + 'type' => 'function', + 'data' => function($row) { + if (!empty($row['first_name'])) + return $row['first_name'] . ' ' . $row['surname']; + else + return 'n/a'; + }, + ], ], 'count' => [ 'header' => 'Cardinality', @@ -76,8 +84,9 @@ class ManageTags extends HTMLController $direction = 'up'; $data = Registry::get('db')->queryAssocs(' - SELECT * - FROM tags + SELECT t.*, u.id_user, u.first_name, u.surname + FROM tags AS t + LEFT JOIN users AS u ON t.id_user_owner = u.id_user WHERE kind != {string:album} ORDER BY {raw:order} LIMIT {int:offset}, {int:limit}', -- 2.46.0 From 5b8551a72676a08f1bdb698134600d6c0d65f786 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 20:46:31 +0100 Subject: [PATCH 45/79] EditAlbum: allow specifying a thumbnail ID manually if none are present --- controllers/EditAlbum.php | 87 ++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 37 deletions(-) diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index c24787d..6d73428 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -72,37 +72,58 @@ class EditAlbum extends HTMLController $parentChoices[$parent['id_tag']] = $parent['tag']; } + $fields = [ + 'id_parent' => [ + 'type' => 'select', + 'label' => 'Parent album', + 'options' => $parentChoices, + ], + '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, + ], + ]; + + // Fetch image assets for this album + if (!empty($id_tag)) + { + list($assets, $num_assets) = AssetIterator::getByOptions([ + 'direction' => 'desc', + 'limit' => 500, + 'id_tag' => $id_tag, + ], true); + + if ($num_assets > 0) + unset($fields['id_asset_thumb']); + } + $form = new Form([ 'request_url' => BASEURL . '/editalbum/?' . ($id_tag ? 'id=' . $id_tag : 'add'), 'content_below' => $after_form, - 'fields' => [ - 'id_parent' => [ - 'type' => 'select', - 'label' => 'Parent album', - 'options' => $parentChoices, - ], - '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, - ], - ], + 'fields' => $fields, ]); + // Add defaults for album if none present if (empty($_POST) && isset($_GET['tag'])) { $parentTag = Tag::fromId($_GET['tag']); @@ -124,17 +145,9 @@ class EditAlbum extends HTMLController $formview = new FormView($form, $form_title ?? ''); $this->page->adopt($formview); - if (!empty($id_tag)) - { - list($assets, $num_assets) = AssetIterator::getByOptions([ - 'direction' => 'desc', - 'limit' => 500, - 'id_tag' => $id_tag, - ], true); - - if ($num_assets > 0) - $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0)); - } + // If we have asset images, show the thumbnail manager + if (!empty($id_tag) && $num_assets > 0) + $this->page->adopt(new FeaturedThumbnailManager($assets, $id_tag ? $album->id_asset_thumb : 0)); if (isset($_POST['changeThumbnail'])) $this->processThumbnail($album); -- 2.46.0 From a4cc5289518524f95fdf73a49ab54ed12d8b1869 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:24:55 +0100 Subject: [PATCH 46/79] ManageAssets: allow batch deletion of assets --- controllers/ManageAssets.php | 50 ++++++++++++++++++++++++++-- templates/AssetManagementWrapper.php | 36 ++++++++++++++++++++ templates/TabularData.php | 19 +++++++++-- 3 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 templates/AssetManagementWrapper.php diff --git a/controllers/ManageAssets.php b/controllers/ManageAssets.php index 4598c8e..499b3c1 100644 --- a/controllers/ManageAssets.php +++ b/controllers/ManageAssets.php @@ -14,10 +14,37 @@ class ManageAssets extends HTMLController if (!Registry::get('user')->isAdmin()) throw new NotAllowedException(); + if (isset($_POST['deleteChecked'], $_POST['delete']) && Session::validateSession()) + $this->handleAssetDeletion(); + Session::resetSessionToken(); $options = [ + 'form' => [ + 'action' => BASEURL . '/manageassets/?' . Session::getSessionTokenKey() . '=' . Session::getSessionToken(), + 'method' => 'post', + 'class' => 'col-md-6 text-end', + 'is_embed' => true, + 'buttons' => [ + 'deleteChecked' => [ + 'type' => 'submit', + 'caption' => 'Delete checked', + 'class' => 'btn-danger', + 'onclick' => 'return confirm(\'Are you sure you want to delete these items?\')', + ], + ], + ], 'columns' => [ + 'checkbox' => [ + 'header' => '<input type="checkbox" id="selectall">', + 'is_sortable' => false, + 'parse' => [ + 'type' => 'function', + 'data' => function($row) { + return '<input type="checkbox" class="asset_select" name="delete[]" value="' . $row['id_asset'] . '">'; + }, + ], + ], 'id_asset' => [ 'value' => 'id_asset', 'header' => 'ID', @@ -71,6 +98,7 @@ class ManageAssets extends HTMLController 'title' => 'Manage assets', 'no_items_label' => 'No assets meet the requirements of the current filter.', 'items_per_page' => 30, + 'index_class' => 'col-md-6', 'base_url' => BASEURL . '/manageassets/', 'get_data' => function($offset = 0, $limit = 30, $order = '', $direction = 'down') { if (!in_array($order, ['id_asset', 'id_user_uploaded', 'title', 'subdir', 'filename'])) @@ -100,7 +128,25 @@ class ManageAssets extends HTMLController ]; $table = new GenericTable($options); - parent::__construct('Asset management - Page ' . $table->getCurrentPage() .''); - $this->page->adopt(new TabularData($table)); + parent::__construct('Asset management - Page ' . $table->getCurrentPage()); + + $wrapper = new AssetManagementWrapper(); + $this->page->adopt($wrapper); + $wrapper->adopt(new TabularData($table)); + } + + private function handleAssetDeletion() + { + if (!isset($_POST['delete']) || !is_array($_POST['delete'])) + throw new UnexpectedValueException(); + + foreach ($_POST['delete'] as $id_asset) + { + $asset = Asset::fromId($id_asset); + $asset->delete(); + } + + header('Location: ' . BASEURL . '/manageassets/'); + exit; } } diff --git a/templates/AssetManagementWrapper.php b/templates/AssetManagementWrapper.php new file mode 100644 index 0000000..545f7c4 --- /dev/null +++ b/templates/AssetManagementWrapper.php @@ -0,0 +1,36 @@ +<?php +/***************************************************************************** + * AssetManagementWrapper.php + * Defines asset management wrapper template. + * + * Kabuki CMS (C) 2013-2015, Aaron van Geffen + *****************************************************************************/ + +class AssetManagementWrapper extends Template +{ + public function html_main() + { + echo ' + <form action="" method="post">'; + + foreach ($this->_subtemplates as $template) + $template->html_main(); + + echo ' + </form> + <script type="text/javascript" defer="defer"> + const allAreSelected = () => { + return document.querySelectorAll(".asset_select").length === + document.querySelectorAll(".asset_select:checked").length; + }; + + const selectAll = document.getElementById("selectall"); + selectAll.addEventListener("change", event => { + const newSelectedState = !allAreSelected(); + document.querySelectorAll(".asset_select").forEach(el => { + el.checked = newSelectedState; + }); + }); + </script>'; + } +} diff --git a/templates/TabularData.php b/templates/TabularData.php index 86f17de..fdcfed3 100644 --- a/templates/TabularData.php +++ b/templates/TabularData.php @@ -145,8 +145,12 @@ class TabularData extends SubTemplate protected function showForm($form) { - echo ' + if (!isset($form['is_embed'])) + echo ' <form action="', $form['action'], '" method="', $form['method'], '" class="', $form['class'], '">'; + else + echo ' + <div class="', $form['class'], '">'; if (!empty($form['is_group'])) echo ' @@ -206,7 +210,12 @@ class TabularData extends SubTemplate foreach ($form['buttons'] as $name => $button) { echo ' - <button class="btn ', isset($button['class']) ? $button['class'] : 'btn-primary', '" type="', $button['type'], '" name="', $name, '">', $button['caption'], '</button>'; + <button class="btn ', isset($button['class']) ? $button['class'] : 'btn-primary', '" type="', $button['type'], '" name="', $name, '"'; + + if (isset($button['onclick'])) + echo ' onclick="', $button['onclick'], '"'; + + echo '>', $button['caption'], '</button>'; if (isset($button['html_after'])) echo $button['html_after']; @@ -216,7 +225,11 @@ class TabularData extends SubTemplate echo ' </div>'; - echo ' + if (!isset($form['is_embed'])) + echo ' </form>'; + else + echo ' + </div>'; } } -- 2.46.0 From 0b0d47acb83309df8eb13ba51d6bab81c5269c15 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:36:32 +0100 Subject: [PATCH 47/79] UploadQueue: error out of HEIC files are presented --- public/js/upload_queue.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/public/js/upload_queue.js b/public/js/upload_queue.js index 75b237d..a0953ff 100644 --- a/public/js/upload_queue.js +++ b/public/js/upload_queue.js @@ -17,6 +17,14 @@ UploadQueue.prototype.addEvents = function() { that.hideSpinner(); that.submit.disabled = false; }; + + if (that.queue.files[0].name.includes(".HEIC")) { + alert('Sorry, the HEIC image format is not supported.\nPlease convert your photos to JPEG before uploading.'); + that.hideSpinner(); + that.submit.disabled = false; + break; + } + that.addPreviewBoxForQueueSlot(i); that.addPreviewForFile(that.queue.files[i], i, callback); }; -- 2.46.0 From aa3a54f237cbf1907c8989fac34b5a2f2c016851 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:39:20 +0100 Subject: [PATCH 48/79] Asset: guard using property_exists in constructor --- models/Asset.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/models/Asset.php b/models/Asset.php index 483c799..bdf6b38 100644 --- a/models/Asset.php +++ b/models/Asset.php @@ -27,7 +27,10 @@ class Asset protected function __construct(array $data) { foreach ($data as $attribute => $value) - $this->$attribute = $value; + { + if (property_exists($this, $attribute)) + $this->$attribute = $value; + } if (!empty($data['date_captured']) && $data['date_captured'] !== 'NULL') $this->date_captured = new DateTime($data['date_captured']); -- 2.46.0 From 1b7e745f1135d06054fefa80a3e1148c22410823 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:41:23 +0100 Subject: [PATCH 49/79] Clean up Tag::resetIdAsset --- models/Tag.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/models/Tag.php b/models/Tag.php index 13af5c7..14696f3 100644 --- a/models/Tag.php +++ b/models/Tag.php @@ -313,8 +313,7 @@ class Tag public function resetIdAsset() { $db = Registry::get('db'); - - $row = $db->query(' + $new_id = $db->queryValue(' SELECT MAX(id_asset) as new_id FROM assets_tags WHERE id_tag = {int:id_tag}', @@ -322,18 +321,12 @@ class 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, + 'new_id' => $new_id ?? 0, 'id_tag' => $this->id_tag, ]); } -- 2.46.0 From b8c53d7d4d52d494859554a5c16d3636ef315542 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:45:03 +0100 Subject: [PATCH 50/79] ViewPhotoAlbum: prevent undefined index due to missing thumb --- controllers/ViewPhotoAlbum.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index d45588b..9f7c07e 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -137,7 +137,8 @@ class ViewPhotoAlbum extends HTMLController 'id_tag' => $album['id_tag'], 'caption' => $album['tag'], 'link' => BASEURL . '/' . $album['slug'] . '/', - 'thumbnail' => !empty($album['id_asset_thumb']) ? $assets[$album['id_asset_thumb']]->getImage() : null, + 'thumbnail' => !empty($album['id_asset_thumb']) && isset($assets[$album['id_asset_thumb']]) + ? $assets[$album['id_asset_thumb']]->getImage() : null, ]; } -- 2.46.0 From 4c928af9add0338ac37e5cbc7af455957752f342 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:46:23 +0100 Subject: [PATCH 51/79] AlbumIndex: set thumbnail dimensions for 'no thumb' images too --- templates/AlbumIndex.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/AlbumIndex.php b/templates/AlbumIndex.php index 3205b81..d311645 100644 --- a/templates/AlbumIndex.php +++ b/templates/AlbumIndex.php @@ -60,7 +60,8 @@ class AlbumIndex extends Template } else echo ' - <img src="', BASEURL, '/images/nothumb.svg" alt="">'; + <img src="', BASEURL, '/images/nothumb.svg" alt="" style="width: ', + static::TILE_WIDTH, 'px; height: ', static::TILE_HEIGHT, 'px">'; if ($this->show_labels) echo ' -- 2.46.0 From 16eda4cfe737af9595d7a4ee288ce2d643d606c4 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:50:08 +0100 Subject: [PATCH 52/79] Move autosuggest styles to default.css --- public/css/admin.css | 24 ------------------------ public/css/default.css | 24 ++++++++++++++++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/public/css/admin.css b/public/css/admin.css index 0316b80..d56938f 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -12,30 +12,6 @@ } -/* (Tag) autosuggest -----------------------*/ -#new_tag_container { - display: block; - position: relative; -} -.autosuggest { - background: #fff; - border: 1px solid #ccc; - position: absolute; - top: 29px; - margin: 0; - padding: 0; -} -.autosuggest li { - display: block !important; - padding: 3px; -} -.autosuggest li:hover, .autosuggest li.selected { - background: #CFECF7; - cursor: pointer; -} - - /* Edit icon on tiled grids -----------------------------*/ .tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama { diff --git a/public/css/default.css b/public/css/default.css index 1732722..7ebf113 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -341,6 +341,30 @@ i.space-invader.alt-7::before { } +/* (Tag) autosuggest +----------------------*/ +#new_tag_container { + display: block; + position: relative; +} +.autosuggest { + background: #fff; + border: 1px solid #ccc; + position: absolute; + top: 29px; + margin: 0; + padding: 0; +} +.autosuggest li { + display: block !important; + padding: 3px; +} +.autosuggest li:hover, .autosuggest li.selected { + background: #CFECF7; + cursor: pointer; +} + + /* Error pages ----------------*/ .errormsg p { -- 2.46.0 From eb04e87085e6d7e373babedc6db0f87590ee53a0 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:52:44 +0100 Subject: [PATCH 53/79] Change autosuggest padding --- public/css/default.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 7ebf113..056e886 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -351,13 +351,14 @@ i.space-invader.alt-7::before { background: #fff; border: 1px solid #ccc; position: absolute; - top: 29px; + left: 2px; + top: 37px; margin: 0; padding: 0; } .autosuggest li { display: block !important; - padding: 3px; + padding: 3px 8px; } .autosuggest li:hover, .autosuggest li.selected { background: #CFECF7; -- 2.46.0 From d83dd6ea6ede77b5b8a62e39b6b1bf6087854733 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:55:44 +0100 Subject: [PATCH 54/79] Remove more obsolete styling --- public/css/admin.css | 14 -------- public/css/default.css | 76 ------------------------------------------ 2 files changed, 90 deletions(-) diff --git a/public/css/admin.css b/public/css/admin.css index d56938f..92c2736 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -1,17 +1,3 @@ -.admin_box { - margin: 0; - padding: 20px; - background: #fff; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - overflow: auto; -} - -.admin_box h2 { - font: 700 24px "Open Sans", sans-serif; - margin: 0 0 0.2em; -} - - /* Edit icon on tiled grids -----------------------------*/ .tiled_grid div.landscape, .tiled_grid div.portrait, .tiled_grid div.panorama { diff --git a/public/css/default.css b/public/css/default.css index 056e886..aee4d77 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -426,50 +426,6 @@ footer a { } -/* Login box styles ----------------------*/ -#login { - background: #fff; - border: 1px solid #aaa; - border-radius: 10px; - box-shadow: 2px 2px 4px rgba(0,0,0,0.1); - margin: 0 auto; - overflow: auto; - padding: 15px; - width: 300px; -} -#login dl *, #login button { - font-size: 15px; - line-height: 35px; -} -#login h3 { - font: 700 24px/36px "Open Sans", sans-serif; - margin: 0; -} -#login dd { - width: 96%; - margin: 0 0 10px; -} -#login input { - background: #eee; - border: 1px solid #aaa; - border-radius: 3px; - padding: 4px 5px; - width: 100%; -} -#login div.alert { - line-height: normal; - margin: 15px 0; -} -#login div.buttonstrip { - float: right; - padding: 0 0 5px; -} -#login button { - line-height: 20px; -} - - /* Styling for the photo pages --------------------------------*/ #photo_frame { @@ -565,38 +521,6 @@ a#previous_photo:hover, a#next_photo:hover { max-width: 100% !important; } - h1#logo { - font-size: 42px; - float: none; - margin: 1em 0 0.5em; - text-align: center; - } - h1#logo:before { - float: none; - font-size: 58px; - margin-right: 8px; - } - - ul#nav { - float: none; - padding: 0; - margin: 1em 0; - text-align: center; - overflow: hidden; - } - - ul#nav li, ul#nav li a { - display: inline-block; - float: none; - } - - ul#nav li a { - float: none; - font-size: 16px; - margin-left: 6px; - padding: 12px 4px; - } - .album_title_box { margin-left: 0; } -- 2.46.0 From 1859a9ea2a7fb916f753046a55fd36707513e259 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 21:57:55 +0100 Subject: [PATCH 55/79] LogInForm: fix smartphone view --- templates/LogInForm.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/LogInForm.php b/templates/LogInForm.php index cafb51e..0e0c96a 100644 --- a/templates/LogInForm.php +++ b/templates/LogInForm.php @@ -11,7 +11,7 @@ class LogInForm extends SubTemplate private $redirect_url = ''; private $emailaddress = ''; - protected $_class = 'content-box container w-50'; + protected $_class = 'content-box container col-lg-6'; public function setRedirectUrl($url) { -- 2.46.0 From e916489d007996d66271e13bc73121b2d62c9c0c Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 22:04:02 +0100 Subject: [PATCH 56/79] PhotoPage: only use columns on large displays --- templates/PhotoPage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 490b2cc..932abc0 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -41,7 +41,7 @@ class PhotoPage extends Template echo ' <div class="row mt-5"> - <div class="col-8"> + <div class="col-lg-8"> <div id="sub_photo" class="content-box"> <h2 class="entry-title">', $this->photo->getTitle(), '</h2>'; @@ -51,7 +51,7 @@ class PhotoPage extends Template echo ' </div> </div> - <div class="col-4">'; + <div class="col-lg-4">'; $this->photoMeta(); -- 2.46.0 From 0b8c614191da2ba191c5c83db741d9fdbb409a40 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 22:07:00 +0100 Subject: [PATCH 57/79] Manage{Assets,Tags}: link user names to edituser --- controllers/ManageAssets.php | 5 +++-- controllers/ManageTags.php | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/controllers/ManageAssets.php b/controllers/ManageAssets.php index 499b3c1..2c2bfdc 100644 --- a/controllers/ManageAssets.php +++ b/controllers/ManageAssets.php @@ -71,8 +71,9 @@ class ManageAssets extends HTMLController 'parse' => [ 'type' => 'function', 'data' => function($row) { - if (!empty($row['first_name'])) - return $row['first_name'] . ' ' . $row['surname']; + if (!empty($row['id_user'])) + return sprintf('<a href="%s/edituser/?id=%d">%s</a>', BASEURL, $row['id_user'], + $row['first_name'] . ' ' . $row['surname']); else return 'n/a'; }, diff --git a/controllers/ManageTags.php b/controllers/ManageTags.php index cb1ffd2..ec12de0 100644 --- a/controllers/ManageTags.php +++ b/controllers/ManageTags.php @@ -56,8 +56,9 @@ class ManageTags extends HTMLController 'parse' => [ 'type' => 'function', 'data' => function($row) { - if (!empty($row['first_name'])) - return $row['first_name'] . ' ' . $row['surname']; + if (!empty($row['id_user'])) + return sprintf('<a href="%s/edituser/?id=%d">%s</a>', BASEURL, $row['id_user'], + $row['first_name'] . ' ' . $row['surname']); else return 'n/a'; }, -- 2.46.0 From c7e4351375e51cfcb5aba803391b71c51fc55dea Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 22:13:55 +0100 Subject: [PATCH 58/79] Change album/tile label font to Coda, too --- public/css/default.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/css/default.css b/public/css/default.css index aee4d77..3e5b8c7 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -192,7 +192,7 @@ i.space-invader.alt-7::before { .tiled_grid div h4 { color: #000; margin: 0; - font: 400 18px "Open Sans", sans-serif; + font: 400 18px 'Coda', sans-serif; padding: 15px 5px; text-overflow: ellipsis; text-align: center; -- 2.46.0 From 2c24a0a7e7018a8dcd6ceef7a72001cbfe4ee63a Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sat, 11 Mar 2023 22:15:17 +0100 Subject: [PATCH 59/79] MainTemplate: open vanity link in new tab --- templates/MainTemplate.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/MainTemplate.php b/templates/MainTemplate.php index d23df69..b9e903b 100644 --- a/templates/MainTemplate.php +++ b/templates/MainTemplate.php @@ -74,7 +74,7 @@ class MainTemplate extends Template } else echo ' - <span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/">Kabuki CMS</a></span>'; + <span class="vanity">Powered by <a href="https://aaronweb.net/projects/kabuki/" target="_blank">Kabuki CMS</a></span>'; echo ' </footer> -- 2.46.0 From 70fcd097cc0b3e1f1f099384d6ae1f358ab595e2 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 00:39:15 +0100 Subject: [PATCH 60/79] EditAsset: remove reference to old admin bar --- controllers/EditAsset.php | 4 ---- controllers/HTMLController.php | 1 - templates/PhotoPage.php | 2 +- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/controllers/EditAsset.php b/controllers/EditAsset.php index 8fc7bf4..a6ca7f9 100644 --- a/controllers/EditAsset.php +++ b/controllers/EditAsset.php @@ -94,10 +94,6 @@ 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) diff --git a/controllers/HTMLController.php b/controllers/HTMLController.php index a5d5236..f141638 100644 --- a/controllers/HTMLController.php +++ b/controllers/HTMLController.php @@ -12,7 +12,6 @@ abstract class HTMLController { protected $page; - protected $admin_bar; public function __construct($title) { diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 932abc0..b2d663f 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -250,7 +250,7 @@ class PhotoPage extends Template echo ' <div id="user_actions_box" class="content-box"> <h3>Actions</h3> - <a class="btn btn-primary" href="', BASEURL, '/editasset/?id=', $this->photo->getId(), '?confirm_delete">Edit photo</a> + <a class="btn btn-primary" href="', BASEURL, '/editasset/?id=', $this->photo->getId(), '">Edit photo</a> <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete photo</a> </div>'; } -- 2.46.0 From 0325a2ec9028fcd607719da46eb8c46518f46b5e Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 00:53:47 +0100 Subject: [PATCH 61/79] EditAssetForm: make form look presentable --- templates/EditAssetForm.php | 85 +++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/templates/EditAssetForm.php b/templates/EditAssetForm.php index 8bdcdaf..d791dcd 100644 --- a/templates/EditAssetForm.php +++ b/templates/EditAssetForm.php @@ -22,9 +22,9 @@ class EditAssetForm extends Template echo ' <form id="asset_form" action="" method="post" enctype="multipart/form-data"> <div class="content-box"> - <div style="float: right"> + <div class="float-end"> <a class="btn btn-danger" href="', BASEURL, '/', $this->asset->getSlug(), '?delete_confirmed">Delete asset</a> - <input class="btn btn-primary" type="submit" value="Save asset data"> + <button class="btn btn-primary" type="submit">Save asset data</button> </div> <h2>Edit asset \'', $this->asset->getTitle(), '\' (', $this->asset->getFilename(), ')</h2> </div>'; @@ -63,20 +63,32 @@ class EditAssetForm extends Template echo ' <div class="content-box key_info"> <h3>Key info</h3> - <dl> - <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="', + <div class="row mb-2"> + <label class="col-form-label col-sm-3">Title (internal):</label> + <div class="col-sm"> + <input class="form-control" type="text" name="title" maxlength="255" size="70" value="', $this->asset->getTitle(), '"> + </div> + </div> + <div class="row mb-2"> + <label class="col-form-label col-sm-3">URL slug:</label> + <div class="col-sm"> + <input class="form-control" type="text" name="slug" maxlength="255" size="70" value="', $this->asset->getSlug(), '"> + </div> + </div> + <div class="row mb-2"> + <label class="col-form-label col-sm-3">Date captured:</label> + <div class="col-sm"> + <input class="form-control" name="date_captured" size="30" value="', $date_captured ? $date_captured->format('Y-m-d H:i:s') : '', '" placeholder="Y-m-d H:i:s"> - - <dt>Display priority</dt> - <dd><input type="number" name="priority" min="0" max="100" step="1" value="', $this->asset->getPriority(), '"> - </dl> + </div> + </div> + <div class="row mb-2"> + <label class="col-form-label col-sm-3">Display priority:</label> + <div class="col-sm-3"> + <input class="form-control" type="number" name="priority" min="0" max="100" step="1" value="', $this->asset->getPriority(), '"> + </div> + </div> </div>'; } @@ -85,7 +97,7 @@ class EditAssetForm extends Template echo ' <div class="content-box linked_tags"> <h3>Linked tags</h3> - <ul id="tag_list">'; + <ul class="list-unstyled" id="tag_list">'; foreach ($this->asset->getTags() as $tag) echo ' @@ -95,7 +107,7 @@ class EditAssetForm extends Template </li>'; echo ' - <li id="new_tag_container"><input type="text" id="new_tag" placeholder="Type to link a new tag"></li> + <li id="new_tag_container"><input class="form-control" type="text" id="new_tag" placeholder="Type to link a new tag"></li> </ul> </div> <script type="text/javascript" src="', BASEURL, '/js/ajax.js"></script> @@ -138,7 +150,7 @@ class EditAssetForm extends Template echo ' <div class="content-box linked_thumbs"> <h3>Thumbnails</h3> - View: <select id="thumbnail_src">'; + View: <select class="form-select w-auto d-inline" id="thumbnail_src">'; $first = INF; foreach ($this->thumbs as $i => $thumb) @@ -220,38 +232,39 @@ class EditAssetForm extends Template protected function section_asset_meta() { echo ' - <div class="content-box asset_meta" style="margin-top: 2%"> - <h3>Asset meta data</h3> - <ul>'; + <div class="content-box asset_meta mt-2"> + <h3>Asset meta data</h3>'; - $i = -1; + $i = 0; foreach ($this->asset->getMeta() as $key => $meta) { - $i++; echo ' - <li> - <input type="text" name="meta_key[', $i, ']" value="', htmlentities($key), '"> - <input type="text" name="meta_value[', $i, ']" value="', htmlentities($meta), '"> - </li>'; + <div class="input-group"> + <input type="text" class="form-control" name="meta_key[', $i, ']" value="', htmlspecialchars($key), '" placeholder="key"> + <input type="text" class="form-control" name="meta_value[', $i, ']" value="', htmlspecialchars($meta), '" placeholder="value"> + </div>'; + $i++; } + echo ' - <li> - <input type="text" name="meta_key[', $i + 1, ']" value=""> - <input type="text" name="meta_value[', $i + 1, ']" value=""> - </li> - </ul> - <p><input type="submit" value="Save metadata"></p> + <div class="input-group"> + <input type="text" class="form-control" name="meta_key[', $i + 1, ']" value="" placeholder="key"> + <input type="text" class="form-control" name="meta_value[', $i + 1, ']" value="" placeholder="value"> + </div> + <div class="text-end mt-3"> + <button class="btn btn-primary" type="submit">Save metadata</button> + </div> </div>'; } protected function section_replace() { echo ' - <div class="content-box replace_asset" style="margin-bottom: 2%; display: block"> + <div class="content-box replace_asset mt-2"> <h3>Replace asset</h3> - File: <input type="file" name="replacement"> - Target: <select name="replacement_target"> + File: <input class="form-control d-inline w-auto" type="file" name="replacement"> + Target: <select class="form-select d-inline w-auto" name="replacement_target"> <option value="full">master file</option>'; foreach ($this->thumbs as $thumb) @@ -287,7 +300,7 @@ class EditAssetForm extends Template echo ' </select> - <input type="submit" value="Save asset"> + <button class="btn btn-primary" type="submit">Save asset</button> </div>'; } } -- 2.46.0 From 01822cdccffc71465e9bcf3fa751c38fd3bd66e5 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 01:00:50 +0100 Subject: [PATCH 62/79] Fix Button, ConfirmDeletePage, WarningDialog templates --- controllers/ViewPhoto.php | 2 +- templates/Button.php | 4 ++-- templates/ConfirmDeletePage.php | 6 +++--- templates/PhotoPage.php | 2 +- templates/WarningDialog.php | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/controllers/ViewPhoto.php b/controllers/ViewPhoto.php index e46929d..5e29ea8 100644 --- a/controllers/ViewPhoto.php +++ b/controllers/ViewPhoto.php @@ -39,7 +39,7 @@ class ViewPhoto extends HTMLController $page = new ConfirmDeletePage($photo->getImage()); $this->page->adopt($page); } - else if (isset($_REQUEST['delete_confirmed'])) + elseif (isset($_REQUEST['delete_confirmed'])) { $album_url = $photo->getSubdir(); $photo->delete(); diff --git a/templates/Button.php b/templates/Button.php index 29024e1..c978c42 100644 --- a/templates/Button.php +++ b/templates/Button.php @@ -6,7 +6,7 @@ * Kabuki CMS (C) 2013-2015, Aaron van Geffen *****************************************************************************/ -class Button extends SubTemplate +class Button extends Template { private $content = ''; private $href = ''; @@ -19,7 +19,7 @@ class Button extends SubTemplate $this->class = $class; } - protected function html_content() + public function html_main() { echo ' <a class="', $this->class, '" href="', $this->href, '">', $this->content, '</a>'; diff --git a/templates/ConfirmDeletePage.php b/templates/ConfirmDeletePage.php index bdba97c..80075c5 100644 --- a/templates/ConfirmDeletePage.php +++ b/templates/ConfirmDeletePage.php @@ -13,7 +13,7 @@ class ConfirmDeletePage extends PhotoPage parent::__construct($photo); } - protected function html_content() + public function html_main() { $this->confirm(); $this->photo(); @@ -22,7 +22,7 @@ class ConfirmDeletePage extends PhotoPage private function confirm() { $buttons = []; - $buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '?delete_confirmed', "btn btn-red"); + $buttons[] = new Button("Delete", BASEURL . '/' . $this->photo->getSlug() . '/?delete_confirmed', "btn btn-danger"); $buttons[] = new Button("Cancel", $this->photo->getPageUrl(), "btn"); $alert = new WarningDialog( @@ -30,6 +30,6 @@ class ConfirmDeletePage extends PhotoPage "You are about to permanently delete the following photo.", $buttons ); - $alert->html_content(); + $alert->html_main(); } } diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index b2d663f..49ac58b 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -251,7 +251,7 @@ class PhotoPage extends Template <div id="user_actions_box" class="content-box"> <h3>Actions</h3> <a class="btn btn-primary" href="', BASEURL, '/editasset/?id=', $this->photo->getId(), '">Edit photo</a> - <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '?confirm_delete">Delete photo</a> + <a class="btn btn-danger" href="', BASEURL, '/', $this->photo->getSlug(), '/?confirm_delete">Delete photo</a> </div>'; } } diff --git a/templates/WarningDialog.php b/templates/WarningDialog.php index 198b7e0..6a9c93e 100644 --- a/templates/WarningDialog.php +++ b/templates/WarningDialog.php @@ -24,6 +24,6 @@ class WarningDialog extends Alert private function addButtons() { foreach ($this->buttons as $button) - $button->html_content(); + $button->html_main(); } } -- 2.46.0 From 3cf281b24d357519c0e1fa9cdf553e18d9155f0a Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 01:04:28 +0100 Subject: [PATCH 63/79] AdminMenu: add error count to badge iff count > 0 --- models/AdminMenu.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/models/AdminMenu.php b/models/AdminMenu.php index aae9e0d..e0515d0 100644 --- a/models/AdminMenu.php +++ b/models/AdminMenu.php @@ -15,9 +15,10 @@ class AdminMenu extends Menu if (!$user->isAdmin()) return; - $this->items[] = [ + $this->items[0] = [ 'label' => 'Admin', 'icon' => 'gear', + 'badge' => ErrorLog::getCount(), 'subs' => [ [ 'uri' => '/managealbums/', @@ -43,6 +44,9 @@ class AdminMenu extends Menu ], ]; + if ($this->items[0]['badge'] == 0) + unset($this->items[0]['badge']); + foreach ($this->items as $i => $item) { if (isset($item['uri'])) -- 2.46.0 From 6087ebe2495615e8164a56a924446320d2f2ee5d Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 01:19:43 +0100 Subject: [PATCH 64/79] AutoSuggest: fix click/append event Keyboard was fine, it was just mouse events that were broken ^^' --- public/js/autosuggest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/autosuggest.js b/public/js/autosuggest.js index 76ab820..e1bcb42 100644 --- a/public/js/autosuggest.js +++ b/public/js/autosuggest.js @@ -124,7 +124,7 @@ class AutoSuggest { node.innerHTML = this.highlightMatches(query_tokens, item.label); node.jsondata = item; node.addEventListener('click', event => { - this.appendCallback(event.target.jsondata); + this.appendCallback(node.jsondata); this.closeContainer(); this.clearInput(); }); -- 2.46.0 From 544944a7f5073aad3a2422eba0940efffe7af173 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 11:32:13 +0100 Subject: [PATCH 65/79] Edit{Album,Tag}: fix new tag creation --- controllers/EditAlbum.php | 2 +- controllers/EditTag.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/controllers/EditAlbum.php b/controllers/EditAlbum.php index 6d73428..9ac4e23 100644 --- a/controllers/EditAlbum.php +++ b/controllers/EditAlbum.php @@ -152,7 +152,7 @@ class EditAlbum extends HTMLController if (isset($_POST['changeThumbnail'])) $this->processThumbnail($album); elseif (!empty($_POST)) - $this->processTagDetails($form, $id_tag, $album); + $this->processTagDetails($form, $id_tag, $album ?? null); } private function processThumbnail($tag) diff --git a/controllers/EditTag.php b/controllers/EditTag.php index c96ef0e..364a6f4 100644 --- a/controllers/EditTag.php +++ b/controllers/EditTag.php @@ -130,7 +130,7 @@ class EditTag extends HTMLController if (isset($_POST['changeThumbnail'])) $this->processThumbnail($tag); elseif (!empty($_POST)) - $this->processTagDetails($form, $id_tag, $tag); + $this->processTagDetails($form, $id_tag, $tag ?? null); } private function processThumbnail($tag) -- 2.46.0 From 54b69ecd11554d5aaaf1b302fe1774ee5395db33 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 11:33:16 +0100 Subject: [PATCH 66/79] MediaUploader: simplify form control design --- templates/MediaUploader.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/templates/MediaUploader.php b/templates/MediaUploader.php index d577ec7..69aa083 100644 --- a/templates/MediaUploader.php +++ b/templates/MediaUploader.php @@ -20,12 +20,9 @@ class MediaUploader extends SubTemplate echo ' <form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" method="post" enctype="multipart/form-data"> <h2>Upload new photos to "', $this->tag->tag, '"</h2> - <div> - <h3>Select files</h3> - <input class="form-control" type="file" id="upload_queue" name="uploads[]" multiple> - </div> - <div> - <input class="btn btn-primary" name="save" id="photo_submit" type="submit" value="Upload the lot"> + <div class="input-group"> + <input class="form-control d-inline" type="file" id="upload_queue" name="uploads[]" multiple> + <button class="btn btn-primary" name="save" id="photo_submit" type="submit">Upload the lot</button> </div> <div id="upload_preview_area"> </div> -- 2.46.0 From 229fb9e5bfc1bf863b6f3b8ac262ee13267f3f4b Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 11:45:37 +0100 Subject: [PATCH 67/79] UploadQueue: refactor into proper ECMAScript class --- public/js/upload_queue.js | 382 +++++++++++++++++++------------------- 1 file changed, 189 insertions(+), 193 deletions(-) diff --git a/public/js/upload_queue.js b/public/js/upload_queue.js index a0953ff..54384f6 100644 --- a/public/js/upload_queue.js +++ b/public/js/upload_queue.js @@ -1,215 +1,211 @@ -function UploadQueue(options) { - this.queue = options.queue_element; - this.preview_area = options.preview_area; - this.upload_progress = []; - this.upload_url = options.upload_url; - this.submit = options.submit_button; - this.addEvents(); -} +class UploadQueue { + constructor(options) { + this.queue = options.queue_element; + this.preview_area = options.preview_area; + this.upload_progress = []; + this.upload_url = options.upload_url; + this.submit = options.submit_button; + this.addEvents(); + } -UploadQueue.prototype.addEvents = function() { - var that = this; - that.queue.addEventListener('change', function() { - that.showSpinner(that.queue, "Generating previews (not uploading yet!)"); - that.clearPreviews(); - for (var i = 0; i < that.queue.files.length; i++) { - var callback = (i !== that.queue.files.length - 1) ? null : function() { - that.hideSpinner(); - that.submit.disabled = false; + addEvents() { + this.queue.addEventListener('change', event => { + this.showSpinner(this.queue, "Generating previews (not uploading yet!)"); + this.clearPreviews(); + for (let i = 0; i < this.queue.files.length; i++) { + const callback = (i !== this.queue.files.length - 1) ? null : () => { + this.hideSpinner(); + this.submit.disabled = false; + }; + + if (this.queue.files[0].name.includes(".HEIC")) { + alert('Sorry, the HEIC image format is not supported.\nPlease convert your photos to JPEG before uploading.'); + this.hideSpinner(); + this.submit.disabled = false; + break; + } + + this.addPreviewBoxForQueueSlot(i); + this.addPreviewForFile(this.queue.files[i], i, callback); }; - - if (that.queue.files[0].name.includes(".HEIC")) { - alert('Sorry, the HEIC image format is not supported.\nPlease convert your photos to JPEG before uploading.'); - that.hideSpinner(); - that.submit.disabled = false; - break; - } - - that.addPreviewBoxForQueueSlot(i); - that.addPreviewForFile(that.queue.files[i], i, callback); - }; - }); - that.submit.addEventListener('click', function(e) { - e.preventDefault(); - that.process(); - }); - this.submit.disabled = true; -}; - -UploadQueue.prototype.clearPreviews = function() { - this.preview_area.innerHTML = ''; - this.submit.disabled = true; - this.current_upload_index = -1; -} - -UploadQueue.prototype.addPreviewBoxForQueueSlot = function(index) { - var preview_box = document.createElement('div'); - preview_box.id = 'upload_preview_' + index; - this.preview_area.appendChild(preview_box); -}; - -UploadQueue.prototype.addPreviewForFile = function(file, index, callback) { - if (!file) { - return false; - } - - var preview = document.createElement('canvas'); - preview.title = file.name; - - var preview_box = document.getElementById('upload_preview_' + index); - preview_box.appendChild(preview); - - var reader = new FileReader(); - var that = this; - reader.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); -}; - -UploadQueue.prototype.process = function() { - this.showSpinner(this.submit, "Preparing to upload files..."); - if (this.queue.files.length > 0) { + this.submit.addEventListener('click', event => { + event.preventDefault(); + this.process(); + }); this.submit.disabled = true; - this.nextFile(); } -}; -UploadQueue.prototype.nextFile = function() { - var files = this.queue.files; - var i = ++this.current_upload_index; - if (i === files.length) { - this.hideSpinner(); - } else { - this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length); - this.sendFile(files[i], i, function() { + clearPreviews() { + this.preview_area.innerHTML = ''; + this.submit.disabled = true; + this.current_upload_index = -1; + } + + addPreviewBoxForQueueSlot(index) { + const preview_box = document.createElement('div'); + preview_box.id = 'upload_preview_' + index; + this.preview_area.appendChild(preview_box); + } + + addPreviewForFile(file, index, callback) { + if (!file) { + return false; + } + + const preview = document.createElement('canvas'); + preview.title = file.name; + + const preview_box = document.getElementById('upload_preview_' + index); + preview_box.appendChild(preview); + + const reader = new FileReader(); + reader.addEventListener('load', event => { + const 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. + const 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. + const 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); + } + + process() { + this.showSpinner(this.submit, "Preparing to upload files..."); + if (this.queue.files.length > 0) { + this.submit.disabled = true; this.nextFile(); + } + } + + nextFile() { + const files = this.queue.files; + const i = ++this.current_upload_index; + if (i === files.length) { + this.hideSpinner(); + } else { + this.setSpinnerLabel("Uploading file " + (i + 1) + " out of " + files.length); + this.sendFile(files[i], i, this.nextFile); + } + } + + sendFile(file, index, callback) { + const request = new XMLHttpRequest(); + request.addEventListener('error', event => { + this.updateProgress(index, -1); }); - } -}; - -UploadQueue.prototype.sendFile = function(file, index, callback) { - // Prepare the request. - var that = this; - var request = new XMLHttpRequest(); - request.addEventListener('error', function(event) { - that.updateProgress(index, -1); - }); - request.addEventListener('progress', function(event) { - that.updateProgress(index, event.loaded / event.total); - }); - request.addEventListener('load', function(event) { - that.updateProgress(index, 1); - if (request.responseText !== null && request.status === 200) { - var obj = JSON.parse(request.responseText); - if (obj.error) { - alert(obj.error); - return; + request.addEventListener('progress', event => { + this.updateProgress(index, event.loaded / event.total); + }); + request.addEventListener('load', event => { + this.updateProgress(index, 1); + if (request.responseText !== null && request.status === 200) { + const obj = JSON.parse(request.responseText); + if (obj.error) { + alert(obj.error); + return; + } + else if (callback) { + callback.call(this, obj); + } } - else if (callback) { - callback.call(that, obj); + }); + + const data = new FormData(); + data.append('uploads', file, file.name); + + request.open('POST', this.upload_url, true); + request.send(data); + } + + addProgressBar(index) { + if (index in this.upload_progress) { + return; + } + + const progress_container = document.createElement('div'); + progress_container.className = 'progress'; + + const progress = document.createElement('div'); + progress_container.appendChild(progress); + + const preview_box = document.getElementById('upload_preview_' + index); + preview_box.appendChild(progress_container); + + this.upload_progress[index] = progress; + } + + updateProgress(index, progress) { + if (!(index in this.upload_progress)) { + this.addProgressBar(index); + } + + const bar = this.upload_progress[index]; + + if (progress >= 0) { + bar.style.width = Math.ceil(progress * 100) + '%'; + } else { + bar.style.width = ""; + if (progress === -1) { + bar.className = "error"; } } - }); - - var data = new FormData(); - data.append('uploads', file, file.name); - - request.open('POST', this.upload_url, true); - request.send(data); -}; - -UploadQueue.prototype.addProgressBar = function(index) { - if (index in this.upload_progress) { - return; } - var progress_container = document.createElement('div'); - progress_container.className = 'progress'; + showSpinner(sibling, label) { + if (this.spinner) { + return; + } - var progress = document.createElement('div'); - progress_container.appendChild(progress); + this.spinner = document.createElement('div'); + this.spinner.className = 'spinner'; + sibling.parentNode.appendChild(this.spinner); - var preview_box = document.getElementById('upload_preview_' + index); - preview_box.appendChild(progress_container); - - this.upload_progress[index] = progress; -}; - -UploadQueue.prototype.updateProgress = function(index, progress) { - if (!(index in this.upload_progress)) { - this.addProgressBar(index); - } - - var bar = this.upload_progress[index]; - - if (progress >= 0) { - bar.style.width = Math.ceil(progress * 100) + '%'; - } else { - bar.style.width = ""; - if (progress === -1) { - bar.className = "error"; + if (label) { + this.spinner_label = document.createElement('span'); + this.spinner_label.className = 'spinner_label'; + this.spinner_label.innerHTML = label; + sibling.parentNode.appendChild(this.spinner_label); } } -}; -UploadQueue.prototype.showSpinner = function(sibling, label) { - if (this.spinner) { - return; + setSpinnerLabel(label) { + if (this.spinner_label) { + this.spinner_label.innerHTML = label; + } } - this.spinner = document.createElement('div'); - this.spinner.className = 'spinner'; - sibling.parentNode.appendChild(this.spinner); - - if (label) { - this.spinner_label = document.createElement('span'); - this.spinner_label.className = 'spinner_label'; - this.spinner_label.innerHTML = label; - sibling.parentNode.appendChild(this.spinner_label); - } -}; - -UploadQueue.prototype.setSpinnerLabel = function(label) { - if (this.spinner_label) { - this.spinner_label.innerHTML = label; + hideSpinner() { + if (this.spinner) { + this.spinner.parentNode.removeChild(this.spinner); + this.spinner = null; + } + if (this.spinner_label) { + this.spinner_label.parentNode.removeChild(this.spinner_label); + this.spinner_label = null; + } } } - -UploadQueue.prototype.hideSpinner = function() { - if (this.spinner) { - this.spinner.parentNode.removeChild(this.spinner); - this.spinner = null; - } - if (this.spinner_label) { - this.spinner_label.parentNode.removeChild(this.spinner_label); - this.spinner_label = null; - } -}; -- 2.46.0 From 3ed84eb4d5a8e0702a3204e165e12209ce492f77 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 11:47:36 +0100 Subject: [PATCH 68/79] UploadQueue: more correct HEIC extension check --- public/js/upload_queue.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/js/upload_queue.js b/public/js/upload_queue.js index 54384f6..506a64b 100644 --- a/public/js/upload_queue.js +++ b/public/js/upload_queue.js @@ -18,7 +18,7 @@ class UploadQueue { this.submit.disabled = false; }; - if (this.queue.files[0].name.includes(".HEIC")) { + if (this.queue.files[0].name.toUpperCase().endsWith(".HEIC")) { alert('Sorry, the HEIC image format is not supported.\nPlease convert your photos to JPEG before uploading.'); this.hideSpinner(); this.submit.disabled = false; -- 2.46.0 From 244af88a9a9636ba92edc49d190fdb9c0710ac40 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:02:21 +0100 Subject: [PATCH 69/79] Asset: cleaner handling of conflicting filenames --- controllers/UploadMedia.php | 4 ---- models/Asset.php | 12 ++++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/controllers/UploadMedia.php b/controllers/UploadMedia.php index 571047b..1457131 100644 --- a/controllers/UploadMedia.php +++ b/controllers/UploadMedia.php @@ -33,14 +33,10 @@ 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(); diff --git a/models/Asset.php b/models/Asset.php index bdf6b38..8b31c0f 100644 --- a/models/Asset.php +++ b/models/Asset.php @@ -187,9 +187,10 @@ class Asset $new_filename = $preferred_filename; $destination = ASSETSDIR . '/' . $preferred_subdir . '/' . $preferred_filename; - while (file_exists($destination)) + for ($i = 1; file_exists($destination); $i++) { - $filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . '_' . mt_rand(10, 99); + $suffix = $i; + $filename = pathinfo($preferred_filename, PATHINFO_FILENAME) . ' (' . $suffix . ')'; $extension = pathinfo($preferred_filename, PATHINFO_EXTENSION); $new_filename = $filename . '.' . $extension; $destination = dirname($destination) . '/' . $new_filename; @@ -206,11 +207,14 @@ class Asset $mimetype = finfo_file($finfo, $destination); finfo_close($finfo); + // We're going to need the base name a few times... + $basename = pathinfo($new_filename, PATHINFO_FILENAME); + // Do we have a title yet? Otherwise, use the filename. - $title = isset($data['title']) ? $data['title'] : pathinfo($preferred_filename, PATHINFO_FILENAME); + $title = $data['title'] ?? $basename; // Same with the slug. - $slug = isset($data['slug']) ? $data['slug'] : $preferred_subdir . '/' . pathinfo($preferred_filename, PATHINFO_FILENAME); + $slug = $data['slug'] ?? sprintf('%s/%s', $preferred_subdir, $basename); // Detected an image? if (substr($mimetype, 0, 5) == 'image') -- 2.46.0 From 3f66fce2626a7864d43fb4eef171f010271c85a4 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:07:17 +0100 Subject: [PATCH 70/79] MediaUploader: explicitly support image/jpeg only --- templates/MediaUploader.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/templates/MediaUploader.php b/templates/MediaUploader.php index 69aa083..1a6aab4 100644 --- a/templates/MediaUploader.php +++ b/templates/MediaUploader.php @@ -21,7 +21,8 @@ class MediaUploader extends SubTemplate <form action="', BASEURL, '/uploadmedia/?tag=', $this->tag->id_tag, '" method="post" enctype="multipart/form-data"> <h2>Upload new photos to "', $this->tag->tag, '"</h2> <div class="input-group"> - <input class="form-control d-inline" type="file" id="upload_queue" name="uploads[]" multiple> + <input class="form-control d-inline" type="file" id="upload_queue" name="uploads[]" + accept="image/jpeg" multiple> <button class="btn btn-primary" name="save" id="photo_submit" type="submit">Upload the lot</button> </div> <div id="upload_preview_area"> -- 2.46.0 From 29bf6af1f81d940978925c04b2f2bbba23e33cdf Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:21:43 +0100 Subject: [PATCH 71/79] Asset: delete thumbnails when deleting an assets --- models/Asset.php | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/models/Asset.php b/models/Asset.php index 8b31c0f..f107b4b 100644 --- a/models/Asset.php +++ b/models/Asset.php @@ -490,9 +490,7 @@ class Asset { $db = Registry::get('db'); - if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename)) - return false; - + // First: delete associated metadata $db->query(' DELETE FROM assets_meta WHERE id_asset = {int:id_asset}', @@ -500,6 +498,7 @@ class Asset 'id_asset' => $this->id_asset, ]); + // Second: figure out what tags to recount cardinality for $recount_tags = $db->queryValues(' SELECT id_tag FROM assets_tags @@ -517,13 +516,30 @@ class Asset Tag::recount($recount_tags); - $return = $db->query(' - DELETE FROM assets + // Third: figure out what associated thumbs to delete + $thumbs_to_delete = $db->queryValues(' + SELECT filename + FROM assets_thumbs WHERE id_asset = {int:id_asset}', [ 'id_asset' => $this->id_asset, ]); + foreach ($thumbs_to_delete as $filename) + { + $thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $filename; + if (is_file($thumb_path)) + unlink($thumb_path); + } + + $db->query(' + DELETE FROM assets_thumbs + WHERE id_asset = {int:id_asset}', + [ + 'id_asset' => $this->id_asset, + ]); + + // Reset asset ID for tags that use this asset for their thumbnail $rows = $db->query(' SELECT id_tag FROM tags @@ -541,6 +557,17 @@ class Asset } } + // Finally, delete the actual asset + if (!unlink(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename)) + return false; + + $return = $db->query(' + DELETE FROM assets + WHERE id_asset = {int:id_asset}', + [ + 'id_asset' => $this->id_asset, + ]); + return $return; } -- 2.46.0 From 41881594e94f8390fbdbeb22066921cb256b74bb Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:34:47 +0100 Subject: [PATCH 72/79] PhotoMosaic: make photo order more intuitive --- models/PhotoMosaic.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index 605bdb0..db86be7 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -79,7 +79,7 @@ class PhotoMosaic return -$priority_diff; // In other cases, we'll just show the newest first. - return $a->getDateCaptured() > $b->getDateCaptured() ? -1 : 1; + return $a->getDateCaptured() <=> $b->getDateCaptured(); } private static function daysApart(Image $a, Image $b) -- 2.46.0 From c73564846890873f8f6866b4924194e35fcfb0d1 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:37:57 +0100 Subject: [PATCH 73/79] ViewPhoto: improve image alignment in page --- public/css/default.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index 3e5b8c7..ee6e69f 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -429,6 +429,7 @@ footer a { /* Styling for the photo pages --------------------------------*/ #photo_frame { + padding-top: 1.5vh; text-align: center; } #photo_frame a { @@ -439,8 +440,8 @@ footer a { #photo_frame a img { border: none; display: block; - height: auto; - width: 100%; + height: 97vh; + width: auto; } #previous_photo, #next_photo { -- 2.46.0 From 85be093a3625ff5e368b1223e8885c4bfe429f2f Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:42:43 +0100 Subject: [PATCH 74/79] ViewPhoto: improve vertical alignment of prev/next buttons --- public/css/default.css | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index ee6e69f..d8294af 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -448,17 +448,12 @@ footer a { background: #fff; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); color: #262626; - font-size: 3em; + font-size: 3rem; line-height: 0.5; - padding: 32px 8px; + padding: 2rem 0.5rem; position: fixed; text-decoration: none; - top: 45%; -} -#previous_photo em, #next_photo em { - position: absolute; - top: -1000em; - left: -1000em; + top: calc(50% - 5rem); } span#previous_photo, span#next_photo { opacity: 0.25; -- 2.46.0 From 5c2eff09b85a10cff26eb298d1524381940e2732 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:55:32 +0100 Subject: [PATCH 75/79] PhotoPage: apply #photo_frame anchor to clicks as well --- public/js/photonav.js | 4 ++-- templates/PhotoPage.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/js/photonav.js b/public/js/photonav.js index e9019f2..8d43dac 100644 --- a/public/js/photonav.js +++ b/public/js/photonav.js @@ -4,14 +4,14 @@ function enableKeyDownNavigation() { var target = document.getElementById("previous_photo").href; if (target) { event.preventDefault(); - document.location.href = target + '#photo_frame'; + document.location.href = target; } } else if (event.keyCode == 39) { var target = document.getElementById("next_photo").href; if (target) { event.preventDefault(); - document.location.href = target + '#photo_frame'; + document.location.href = target; } } }, false); diff --git a/templates/PhotoPage.php b/templates/PhotoPage.php index 49ac58b..52b6fe5 100644 --- a/templates/PhotoPage.php +++ b/templates/PhotoPage.php @@ -84,14 +84,14 @@ class PhotoPage extends Template { if ($this->previous_photo_url) echo ' - <a href="', $this->previous_photo_url, '" id="previous_photo"><i class="bi bi-arrow-left"></i></a>'; + <a href="', $this->previous_photo_url, '#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 ($this->next_photo_url) echo ' - <a href="', $this->next_photo_url, '" id="next_photo"><i class="bi bi-arrow-right"></i></a>'; + <a href="', $this->next_photo_url, '#photo_frame" id="next_photo"><i class="bi bi-arrow-right"></i></a>'; else echo ' <span id="next_photo"><i class="bi bi-arrow-right"></i></span>'; -- 2.46.0 From c991f05dd354f99d365893769740fc4610705a89 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Sun, 12 Mar 2023 12:58:58 +0100 Subject: [PATCH 76/79] ViewPhoto: rework solution to work for panoramas, too --- public/css/default.css | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/public/css/default.css b/public/css/default.css index d8294af..3aabe66 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -433,14 +433,16 @@ footer a { text-align: center; } #photo_frame a { - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); - cursor: -moz-zoom-in; - display: inline-block; + } #photo_frame a img { border: none; - display: block; + box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + cursor: -moz-zoom-in; + display: inline-block; height: 97vh; + max-width: 100%; + object-fit: contain; width: auto; } -- 2.46.0 From e48f065c2540691a30b251cff0c3d9769dda7612 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Mon, 13 Mar 2023 01:33:10 +0100 Subject: [PATCH 77/79] PhotoIndex: fix inadvertent thumb stretching in rare cases --- public/css/default.css | 1 + 1 file changed, 1 insertion(+) diff --git a/public/css/default.css b/public/css/default.css index 3aabe66..93e7c58 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -187,6 +187,7 @@ i.space-invader.alt-7::before { .tiled_grid div img { background: url('../images/nothumb.svg') center no-repeat; border: none; + object-fit: cover; width: 100%; } .tiled_grid div h4 { -- 2.46.0 From c6dc6bbac41d7166ca70def7b994b1ad38dc6563 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Mon, 13 Mar 2023 01:37:31 +0100 Subject: [PATCH 78/79] AlbumIndex: don't over-fit placeholder images --- templates/AlbumIndex.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/AlbumIndex.php b/templates/AlbumIndex.php index d311645..ebfe9c1 100644 --- a/templates/AlbumIndex.php +++ b/templates/AlbumIndex.php @@ -61,7 +61,7 @@ class AlbumIndex extends Template else echo ' <img src="', BASEURL, '/images/nothumb.svg" alt="" style="width: ', - static::TILE_WIDTH, 'px; height: ', static::TILE_HEIGHT, 'px">'; + static::TILE_WIDTH, 'px; height: ', static::TILE_HEIGHT, 'px; object-fit: unset">'; if ($this->show_labels) echo ' -- 2.46.0 From 65cea8ed8ac98eb6132c7e5bb40e66aea3c7ef8c Mon Sep 17 00:00:00 2001 From: Aaron van Geffen <aaron@aaronweb.net> Date: Mon, 13 Mar 2023 16:30:24 +0100 Subject: [PATCH 79/79] UploadMedia: only set thumb asset id for tags that don't have one yet --- controllers/UploadMedia.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/controllers/UploadMedia.php b/controllers/UploadMedia.php index 1457131..194cb9e 100644 --- a/controllers/UploadMedia.php +++ b/controllers/UploadMedia.php @@ -42,8 +42,11 @@ class UploadMedia extends HTMLController $new_ids[] = $asset->getId(); $asset->linkTags([$tag->id_tag]); - $tag->id_asset_thumb = $asset->getId(); - $tag->save(); + if (empty($tag->id_asset_thumb)) + { + $tag->id_asset_thumb = $asset->getId(); + $tag->save(); + } } if (isset($_REQUEST['format']) && $_REQUEST['format'] === 'json') -- 2.46.0