From 8e0e642d3428ac94a8e77a3c9e7cf2e775e0f6b6 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Wed, 29 Nov 2023 22:47:51 +0100 Subject: [PATCH 1/8] PhotoMosaic: reorder methods to be alphabetically ordered --- models/PhotoMosaic.php | 71 +++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index 901e8a0..c3cf9cd 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -8,10 +8,9 @@ class PhotoMosaic { - private $queue = []; - const NUM_DAYS_CUTOFF = 7; private AssetIterator $iterator; + private $queue = []; public function __construct(AssetIterator $iterator) { @@ -23,21 +22,9 @@ class PhotoMosaic $this->iterator->clean(); } - public static function getRecentPhotos() + private static function daysApart(Image $a, Image $b) { - return new self(AssetIterator::getByOptions([ - 'tag' => 'photo', - 'order' => 'date_captured', - 'direction' => 'desc', - 'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs) - ])); - } - - private static function matchTypeMask(Image $image, $type_mask) - { - return ($type_mask & Image::TYPE_PANORAMA) && $image->isPanorama() || - ($type_mask & Image::TYPE_LANDSCAPE) && $image->isLandscape() || - ($type_mask & Image::TYPE_PORTRAIT) && $image->isPortrait(); + return $a->getDateCaptured()->diff($b->getDateCaptured())->days; } private function fetchImage($desired_type = Image::TYPE_PORTRAIT | Image::TYPE_LANDSCAPE | Image::TYPE_PANORAMA, Image $refDateImage = null) @@ -66,25 +53,14 @@ class PhotoMosaic return false; } - private function pushToQueue(Image $image) + public static function getRecentPhotos() { - $this->queue[] = $image; - } - - private static function orderPhotos(Image $a, Image $b) - { - // Show images of highest priority first. - $priority_diff = $a->getPriority() - $b->getPriority(); - if ($priority_diff !== 0) - return -$priority_diff; - - // In other cases, we'll just show the newest first. - return $a->getDateCaptured() <=> $b->getDateCaptured(); - } - - private static function daysApart(Image $a, Image $b) - { - return $a->getDateCaptured()->diff($b->getDateCaptured())->days; + return new self(AssetIterator::getByOptions([ + 'tag' => 'photo', + 'order' => 'date_captured', + 'direction' => 'desc', + 'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs) + ])); } public function getRow() @@ -102,13 +78,13 @@ class PhotoMosaic // Alright, let's initalise a proper row, then. $photos = [$image]; - $num_portrait = $image->isPortrait() ? 1 : 0; + $num_portrait = $image->isPortrait() ? 1 : 0; $num_landscape = $image->isLandscape() ? 1 : 0; // Get an initial batch of non-panorama images to work with. for ($i = 1; $i < 3 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++) { - $num_portrait += $image->isPortrait() ? 1 : 0; + $num_portrait += $image->isPortrait() ? 1 : 0; $num_landscape += $image->isLandscape() ? 1 : 0; $photos[] = $image; } @@ -171,4 +147,27 @@ class PhotoMosaic else return [$photos, 'landscapes']; } + + private static function matchTypeMask(Image $image, $type_mask) + { + return ($type_mask & Image::TYPE_PANORAMA) && $image->isPanorama() || + ($type_mask & Image::TYPE_LANDSCAPE) && $image->isLandscape() || + ($type_mask & Image::TYPE_PORTRAIT) && $image->isPortrait(); + } + + private static function orderPhotos(Image $a, Image $b) + { + // Show images of highest priority first. + $priority_diff = $a->getPriority() - $b->getPriority(); + if ($priority_diff !== 0) + return -$priority_diff; + + // In other cases, we'll just show the newest first. + return $a->getDateCaptured() <=> $b->getDateCaptured(); + } + + private function pushToQueue(Image $image) + { + $this->queue[] = $image; + } } From b98785d7b2bf6bcc681116999c52eb24c829a761 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Fri, 1 Dec 2023 23:39:55 +0100 Subject: [PATCH 2/8] PhotoMosaic: remove unused getRecentPhotos method --- models/PhotoMosaic.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index c3cf9cd..05b2516 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -53,16 +53,6 @@ class PhotoMosaic return false; } - public static function getRecentPhotos() - { - return new self(AssetIterator::getByOptions([ - 'tag' => 'photo', - 'order' => 'date_captured', - 'direction' => 'desc', - 'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs) - ])); - } - public function getRow() { // Fetch the first image... From 8700fc1417e4eaf0be3220be7e5720d18b8976d1 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Fri, 1 Dec 2023 23:41:05 +0100 Subject: [PATCH 3/8] PhotoMosaic: introduce availableLayouts method --- models/PhotoMosaic.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index 05b2516..f4e1339 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -10,11 +10,13 @@ class PhotoMosaic { const NUM_DAYS_CUTOFF = 7; private AssetIterator $iterator; + private $layouts; private $queue = []; public function __construct(AssetIterator $iterator) { $this->iterator = $iterator; + $this->layouts = $this->availableLayouts(); } public function __destruct() @@ -22,6 +24,28 @@ class PhotoMosaic $this->iterator->clean(); } + private function availableLayouts() + { + static $layouts = [ + // Single panorama + 'panorama' => [Image::TYPE_PANORAMA], + + // Big-small juxtapositions + 'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + + // Single row of three + 'landscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'portraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT], + + // Fallback layouts + 'duo' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'single' => [Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT], + ]; + + return $layouts; + } + private static function daysApart(Image $a, Image $b) { return $a->getDateCaptured()->diff($b->getDateCaptured())->days; From d45b467bb1b9569461b72c680e0eacff07553aa6 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sat, 2 Dec 2023 00:24:43 +0100 Subject: [PATCH 4/8] PhotoMosaic: rewrite getRow to use availableLayouts --- models/PhotoMosaic.php | 119 +++++++++++++---------------------------- 1 file changed, 37 insertions(+), 82 deletions(-) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index f4e1339..8fc0eef 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -8,11 +8,13 @@ class PhotoMosaic { - const NUM_DAYS_CUTOFF = 7; private AssetIterator $iterator; private $layouts; + private $processedImages = 0; private $queue = []; + const NUM_DAYS_CUTOFF = 7; + public function __construct(AssetIterator $iterator) { $this->iterator = $iterator; @@ -31,7 +33,8 @@ class PhotoMosaic 'panorama' => [Image::TYPE_PANORAMA], // Big-small juxtapositions - 'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, + Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], 'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], // Single row of three @@ -79,87 +82,39 @@ class PhotoMosaic public function getRow() { - // Fetch the first image... - $image = $this->fetchImage(); - - // No image at all? - if (!$image) - return false; - - // Is it a panorama? Then we've got our row! - elseif ($image->isPanorama()) - return [[$image], 'panorama']; - - // Alright, let's initalise a proper row, then. - $photos = [$image]; - $num_portrait = $image->isPortrait() ? 1 : 0; - $num_landscape = $image->isLandscape() ? 1 : 0; - - // Get an initial batch of non-panorama images to work with. - for ($i = 1; $i < 3 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++) + $currentImages = []; + $selectedLayout = null; + foreach ($this->layouts as $layout => $requiredImageTypes) { - $num_portrait += $image->isPortrait() ? 1 : 0; - $num_landscape += $image->isLandscape() ? 1 : 0; - $photos[] = $image; - } + // Fetch images per the layout requirements + // TODO: pass last image back into fetchImage for date cutoff purposes + $currentImages = array_filter(array_map([$this, 'fetchImage'], $requiredImageTypes)); - // Sort photos by priority and date captured. - usort($photos, self::orderPhotos(...)); - - // Three portraits? - if ($num_portrait === 3) - return [$photos, 'portraits']; - - // At least one portrait? - if ($num_portrait >= 1) - { - // Grab two more landscapes, so we can put a total of four tiles on the side. - for ($i = 0; $image && $i < 2 && ($image = $this->fetchImage(Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT, $image)); $i++) - $photos[] = $image; - - // We prefer to have the portrait on the side, so prepare to process that first. - usort($photos, function($a, $b) { - if ($a->isPortrait() && !$b->isPortrait()) - return -1; - elseif ($b->isPortrait() && !$a->isPortrait()) - return 1; - else - return self::orderPhotos($a, $b); - }); - - // We might not have a full set of photos, but only bother if we have at least three. - if (count($photos) > 3) - return [$photos, 'portrait']; - } - - // One landscape at least, hopefully? - if ($num_landscape >= 1) - { - if (count($photos) === 3) + // Matching requirements? + if (count($currentImages) === count($requiredImageTypes)) { - // We prefer to have the landscape on the side, so prepare to process that first. - usort($photos, function($a, $b) { - if ($a->isLandscape() && !$b->isLandscape()) - return -1; - elseif ($b->isLandscape() && !$a->isLandscape()) - return 1; - else - return self::orderPhotos($a, $b); - }); - - return [$photos, 'landscape']; + $selectedLayout = $layout; + break; } - elseif (count($photos) === 2) - return [$photos, 'duo']; - else - return [$photos, 'single']; + + // Push mismatches back into the queue + array_map([$this, 'pushToQueue'], $currentImages); + $currentImages = []; } - // Last resort: majority vote - if ($num_portrait > $num_landscape) - return [$photos, 'portraits']; + if ($selectedLayout) + { + // Hurray, we've got something that works + usort($currentImages, [$this, 'orderPhotos']); + $this->processedImages += count($currentImages); + return [$currentImages, $selectedLayout]; + } else - return [$photos, 'landscapes']; + { + // Ensure we have no images left in the iterator before giving up + assert($this->processedImages === $this->iterator->num()); + return false; + } } private static function matchTypeMask(Image $image, $type_mask) @@ -171,13 +126,13 @@ class PhotoMosaic private static function orderPhotos(Image $a, Image $b) { - // Show images of highest priority first. - $priority_diff = $a->getPriority() - $b->getPriority(); - if ($priority_diff !== 0) - return -$priority_diff; + // Leave images of different types as-is + if ($a->isLandscape() !== $b->isLandscape()) + return 0; - // In other cases, we'll just show the newest first. - return $a->getDateCaptured() <=> $b->getDateCaptured(); + // Otherwise, show images of highest priority first + $priority_diff = $a->getPriority() - $b->getPriority(); + return -$priority_diff; } private function pushToQueue(Image $image) From f66a400100ec917b71f5c6b263e58e3a92ad652f Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sat, 2 Dec 2023 00:24:47 +0100 Subject: [PATCH 5/8] PhotosIndex: removing unnecessary limit/constant --- templates/PhotosIndex.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index b01a923..adef18d 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -12,7 +12,6 @@ class PhotosIndex extends Template protected $show_edit_buttons; protected $show_headers; protected $show_labels; - protected $row_limit = 1000; protected $previous_header = ''; protected $url_suffix; @@ -47,11 +46,12 @@ class PhotosIndex extends Template echo '
'; - for ($i = $this->row_limit; $i > 0 && $row = $this->mosaic->getRow(); $i--) + $i = 0; + while ($row = $this->mosaic->getRow()) { - list($photos, $what) = $row; + [$photos, $what] = $row; $this->header($photos); - $this->$what($photos, $i % 2); + $this->$what($photos, ($i++) % 2); } echo ' From d42c3c142cda217f9fe34ecb3116b94cf2ceae09 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sat, 2 Dec 2023 00:50:04 +0100 Subject: [PATCH 6/8] PhotosIndex: differentiate dual/single layouts by landscape/portrait --- models/PhotoMosaic.php | 18 ++++---- templates/PhotosIndex.php | 88 ++++++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index 8fc0eef..3f75407 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -33,17 +33,21 @@ class PhotoMosaic 'panorama' => [Image::TYPE_PANORAMA], // Big-small juxtapositions - 'portrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, - Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], - 'landscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'sidePortrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, + Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'sideLandscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], // Single row of three - 'landscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], - 'portraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT], + 'threeLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'threePortraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT], + + // Dual layouts + 'dualLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + 'dualPortraits' => [Image::TYPE_PORTRAIT, Image::TYPE_PORTRAIT], // Fallback layouts - 'duo' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], - 'single' => [Image::TYPE_LANDSCAPE | Image::TYPE_PORTRAIT], + 'singleLandscape' => [Image::TYPE_LANDSCAPE], + 'singlePortrait' => [Image::TYPE_PORTRAIT], ]; return $layouts; diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index adef18d..537078b 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -137,7 +137,7 @@ class PhotosIndex extends Template } } - protected function portrait(array $photos, $altLayout) + protected function sidePortrait(array $photos, $altLayout) { $image = array_shift($photos); @@ -170,7 +170,7 @@ class PhotosIndex extends Template
'; } - protected function landscape(array $photos, $altLayout) + protected function sideLandscape(array $photos, $altLayout) { $image = array_shift($photos); @@ -203,41 +203,7 @@ class PhotosIndex extends Template '; } - protected function duo(array $photos, $altLayout) - { - echo ' -
'; - - foreach ($photos as $image) - { - echo ' -
'; - - $this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true); - - echo ' -
'; - } - - echo ' -
'; - } - - protected function single(array $photos, $altLayout) - { - echo ' -
-
'; - - $image = array_shift($photos); - $this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top'); - - echo ' -
-
'; - } - - protected function landscapes(array $photos, $altLayout) + protected function threeLandscapes(array $photos, $altLayout) { echo '
'; @@ -257,7 +223,7 @@ class PhotosIndex extends Template
'; } - protected function portraits(array $photos, $altLayout) + protected function threePortraits(array $photos, $altLayout) { echo '
'; @@ -277,6 +243,52 @@ class PhotosIndex extends Template
'; } + protected function dualLandscapes(array $photos, $altLayout) + { + echo ' +
'; + + foreach ($photos as $image) + { + echo ' +
'; + + $this->photo($image, 'duo', static::DUO_WIDTH, static::DUO_HEIGHT, true); + + echo ' +
'; + } + + echo ' +
'; + } + + protected function dualPortraits(array $photos, $altLayout) + { + // Recycle the row layout so portraits don't appear too large + $this->threePortraits($photos, $altLayout); + } + + protected function singleLandscape(array $photos, $altLayout) + { + echo ' +
+
'; + + $image = array_shift($photos); + $this->photo($image, 'single', static::SINGLE_WIDTH, static::SINGLE_HEIGHT, 'top'); + + echo ' +
+
'; + } + + protected function singlePortrait(array $photos, $altLayout) + { + // Recycle the row layout so portraits don't appear too large + $this->threePortraits($photos, $altLayout); + } + public function setUrlSuffix($suffix) { $this->url_suffix = $suffix; From efb35cfd6a948bd9a4f43ea73677c915b565a432 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sat, 2 Dec 2023 01:29:11 +0100 Subject: [PATCH 7/8] PhotoMosaic: add sixLandscapes layout, combining side and row --- models/PhotoMosaic.php | 4 ++++ templates/PhotosIndex.php | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/models/PhotoMosaic.php b/models/PhotoMosaic.php index 3f75407..68089ba 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -32,6 +32,10 @@ class PhotoMosaic // Single panorama 'panorama' => [Image::TYPE_PANORAMA], + // A whopping six landscapes? + 'sixLandscapes' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, + Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + // Big-small juxtapositions 'sidePortrait' => [Image::TYPE_PORTRAIT, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index 537078b..c9fe715 100644 --- a/templates/PhotosIndex.php +++ b/templates/PhotosIndex.php @@ -137,6 +137,13 @@ class PhotosIndex extends Template } } + protected function sixLandscapes(array $photos, $altLayout) + { + $chunks = array_chunk($photos, 3); + $this->sideLandscape($chunks[0], $altLayout); + $this->threeLandscapes($chunks[1], $altLayout); + } + protected function sidePortrait(array $photos, $altLayout) { $image = array_shift($photos); From 4d9219586fb230095f97320e12a6cd95b39da5e7 Mon Sep 17 00:00:00 2001 From: Aaron van Geffen Date: Sat, 2 Dec 2023 01:38:07 +0100 Subject: [PATCH 8/8] PageIndexWidget: display current page on smartphones, too --- controllers/ViewPeople.php | 2 +- controllers/ViewPhotoAlbum.php | 2 +- controllers/ViewTimeline.php | 2 +- public/css/default.css | 22 +++++----------------- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/controllers/ViewPeople.php b/controllers/ViewPeople.php index eeca876..ee00b3b 100644 --- a/controllers/ViewPeople.php +++ b/controllers/ViewPeople.php @@ -52,7 +52,7 @@ class ViewPeople extends HTMLController 'start' => $start, 'base_url' => BASEURL . '/people/', 'page_slug' => 'page/%PAGE%/', - 'index_class' => 'pagination-lg mt-5 justify-content-center', + 'index_class' => 'pagination-lg mt-5 justify-content-around justify-content-lg-center', ]); $this->page->adopt(new PageIndexWidget($pagination)); diff --git a/controllers/ViewPhotoAlbum.php b/controllers/ViewPhotoAlbum.php index 9f7c07e..6524c86 100644 --- a/controllers/ViewPhotoAlbum.php +++ b/controllers/ViewPhotoAlbum.php @@ -91,7 +91,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', + 'index_class' => 'pagination-lg justify-content-around justify-content-lg-center', ]); $this->page->adopt(new PageIndexWidget($index)); } diff --git a/controllers/ViewTimeline.php b/controllers/ViewTimeline.php index 72dfd76..d5dd9d3 100644 --- a/controllers/ViewTimeline.php +++ b/controllers/ViewTimeline.php @@ -46,7 +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', + 'index_class' => 'pagination-lg justify-content-around justify-content-lg-center', ]); $this->page->adopt(new PageIndexWidget($index)); } diff --git a/public/css/default.css b/public/css/default.css index fc9988a..c119c9d 100644 --- a/public/css/default.css +++ b/public/css/default.css @@ -81,29 +81,17 @@ a:hover { } @media (max-width: 767px) { - .pagination { - height: 52px; - display: block; - position: relative; - } .pagination .page-number, .pagination .page-padding { display: none; } .pagination .page-link { border-radius: var(--bs-pagination-border-radius) !important; } - .pagination > :first-child, .pagination > :last-child, .pagination .first-wildcard { - display: inline-block; - position: absolute; - } - .pagination > :first-child { - left: 0; - } - .pagination > .first-wildcard { - left: 48%; - } - .pagination > :last-child { - right: 0; + .pagination > :first-child, + .pagination > :last-child, + .pagination .first-wildcard, + .pagination .page-number.active { + display: inline-flex; } }