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/controllers/AccountSettings.php b/controllers/AccountSettings.php new file mode 100644 index 0000000..4f4a431 --- /dev/null +++ b/controllers/AccountSettings.php @@ -0,0 +1,135 @@ +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' => '
To change your password, please fill out the fields below.
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!
', '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/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/models/MainMenu.php b/models/MainMenu.php new file mode 100644 index 0000000..e1ccb05 --- /dev/null +++ b/models/MainMenu.php @@ -0,0 +1,41 @@ +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/Member.php b/models/Member.php index 9fc21f7..84912a4 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); } /** @@ -189,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' => ' ', + ]); + } } 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 @@ +items; + } +} 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 @@ +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; + } +} 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) 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', diff --git a/models/Tag.php b/models/Tag.php index e6b6f13..14696f3 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; @@ -95,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(' @@ -258,7 +278,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}, @@ -292,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}', @@ -301,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, ]); } 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 @@ +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 f3f3b90..92c2736 100644 --- a/public/css/admin.css +++ b/public/css/admin.css @@ -1,126 +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; -} - -.floatleft { - float: left; -} -.floatright { - float: right; -} - -/* 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 -----------------------*/ -#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 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 { - 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 { @@ -157,7 +34,7 @@ body { color: #fff; } #crop_editor input[type=number] { - width: 50px; + width: 75px; background: #555; color: #fff; } @@ -195,119 +72,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..93e7c58 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -4,35 +4,28 @@ * 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); +@import url('//fonts.googleapis.com/css2?family=Coda&display=swap'); @font-face { - font-family: 'Invaders'; - src: url('fonts/invaders.ttf') format('truetype'); - font-weight: normal; - font-style: normal; + font-family: 'Invaders'; + src: url('fonts/invaders.ttf') format('truetype'); + font-weight: normal; + font-style: normal; } body { - font: 13px/1.7 "Open Sans", sans-serif; - padding: 0 0 3em; - margin: 0; + font-family: "Open Sans", sans-serif; background: #aaa 0 -50% fixed; - background-image: radial-gradient(ellipse at top, #ccc 0%, #aaa 55%, #333 100%); + padding: 0 0 3rem; } -#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; @@ -41,101 +34,120 @@ 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); +.page-link { + color: #b50707; + font-family: 'Coda', sans-serif; } -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); +.page-link:hover { + color: #a40d0d; } -a h1#logo { - text-decoration: none; +.active > .page-link, .page-link.active { + background-color: #990b0b; + border-color: #a40d0d; } -a:hover h1#logo, a:hover h1#logo:before { - text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.6); + +.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; +} + +.dropdown-item.active, .dropdown-item:active { + background-color: #990b0b; } /* Navigation ---------------*/ -ul#nav { - margin: 55px 10px 0 0; - padding: 0; - float: right; - list-style: none; +#mainNav { + font-family: 'Coda', sans-serif; + margin-bottom: 4rem; } -ul#nav li { - float: left; +.nav-divider { + height: 2.5rem; + border-left: .1rem solid rgba(255,255,255, 0.2); + margin: 0 0.5rem; } -ul#nav li a { +.navbar-brand { + padding-left: 80px; + position: relative; +} +i.space-invader::before { 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); -} - - -/* Pagination ----------------*/ -.pagination { - clear: both; - text-align: center; -} -.pagination ul { + content: 'B'; display: inline-block; - margin: 0; - padding: 0; - box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); + font: 85px 'Invaders'; + 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); + transition: 0.25s; + width: 110px; } -.pagination ul > li { - display: inline; +.navbar-brand:hover i.space-invader::before { + transform: rotate(5deg); } -.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; +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; + } +} + + +/* Content boxes +------------------*/ +.content-box { background-color: #fff; - border-right: 1px solid #ddd; + margin: 0 auto 2rem; + padding: 2rem; + border-radius: 0.5rem; + box-shadow: 0 0.25rem 0.5rem rgba(0,0,0,0.1); } - -.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; +.content-box h1, +.content-box h2, +.content-box h3 { + font-family: 'Coda', sans-serif; + margin-bottom: 0.5em; } @@ -144,11 +156,12 @@ 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; 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; @@ -172,13 +185,15 @@ ul#nav li a:hover { width: 31%; } .tiled_grid div img { + background: url('../images/nothumb.svg') center no-repeat; border: none; + object-fit: cover; width: 100%; } .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; @@ -285,15 +300,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; @@ -302,7 +321,7 @@ ul#nav li a:hover { } .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 { @@ -314,29 +333,37 @@ ul#nav li a:hover { ---------------------*/ .album_button_box { float: right; - margin-bottom: 20px; + margin-bottom: 3rem; } .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); +/* (Tag) autosuggest +----------------------*/ +#new_tag_container { + display: block; + position: relative; } -.boxed_content h2 { - font: 300 24px "Open Sans", sans-serif; - margin: 0 0 0.2em; +.autosuggest { + background: #fff; + border: 1px solid #ccc; + position: absolute; + left: 2px; + top: 37px; + margin: 0; + padding: 0; +} +.autosuggest li { + display: block !important; + padding: 3px 8px; +} +.autosuggest li:hover, .autosuggest li.selected { + background: #CFECF7; + cursor: pointer; } @@ -354,6 +381,36 @@ ul#nav li a:hover { } +/* 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 { @@ -370,206 +427,36 @@ 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; -} -.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 { - 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; -} - - -/* 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 { + padding-top: 1.5vh; 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; - height: auto; - width: 100%; + 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; } #previous_photo, #next_photo { 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; @@ -579,33 +466,18 @@ 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; - margin: 0 0 10px; -} -#sub_photo h3 { - 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%; + margin-bottom: 1rem; } #sub_photo #tag_list { list-style: none; @@ -624,15 +496,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; @@ -647,15 +510,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 ---------------------------------------*/ @@ -666,38 +520,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; } diff --git a/public/images/nothumb.png b/public/images/nothumb.png deleted file mode 100644 index ce6a2f2..0000000 Binary files a/public/images/nothumb.png and /dev/null differ 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 @@ + + 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(); }); 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/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/public/js/upload_queue.js b/public/js/upload_queue.js index 75b237d..506a64b 100644 --- a/public/js/upload_queue.js +++ b/public/js/upload_queue.js @@ -1,207 +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.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; + break; + } + + this.addPreviewBoxForQueueSlot(i); + this.addPreviewForFile(this.queue.files[i], i, callback); }; - 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; - } -}; 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 diff --git a/templates/AdminBar.php b/templates/AdminBar.php deleted file mode 100644 index 65662fe..0000000 --- a/templates/AdminBar.php +++ /dev/null @@ -1,38 +0,0 @@ - -', $this->_message, '
'; - - $this->additional_alert_content(); - - echo '