diff --git a/controllers/ViewPeople.php b/controllers/ViewPeople.php index eeca876e..ee00b3bf 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 9f7c07eb..6524c86b 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 72dfd763..d5dd9d39 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/models/PhotoMosaic.php b/models/PhotoMosaic.php index 901e8a0e..68089ba8 100644 --- a/models/PhotoMosaic.php +++ b/models/PhotoMosaic.php @@ -8,14 +8,17 @@ class PhotoMosaic { + private AssetIterator $iterator; + private $layouts; + private $processedImages = 0; private $queue = []; const NUM_DAYS_CUTOFF = 7; - private AssetIterator $iterator; public function __construct(AssetIterator $iterator) { $this->iterator = $iterator; + $this->layouts = $this->availableLayouts(); } public function __destruct() @@ -23,21 +26,40 @@ class PhotoMosaic $this->iterator->clean(); } - public static function getRecentPhotos() + private function availableLayouts() { - return new self(AssetIterator::getByOptions([ - 'tag' => 'photo', - 'order' => 'date_captured', - 'direction' => 'desc', - 'limit' => 15, // worst case: 3 rows * (portrait + 4 thumbs) - ])); + static $layouts = [ + // 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], + 'sideLandscape' => [Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE, Image::TYPE_LANDSCAPE], + + // Single row of three + '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 + 'singleLandscape' => [Image::TYPE_LANDSCAPE], + 'singlePortrait' => [Image::TYPE_PORTRAIT], + ]; + + return $layouts; } - private static function matchTypeMask(Image $image, $type_mask) + private static function daysApart(Image $a, Image $b) { - 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,109 +88,63 @@ class PhotoMosaic return false; } - private function pushToQueue(Image $image) + public function getRow() { - $this->queue[] = $image; + $currentImages = []; + $selectedLayout = null; + foreach ($this->layouts as $layout => $requiredImageTypes) + { + // 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)); + + // Matching requirements? + if (count($currentImages) === count($requiredImageTypes)) + { + $selectedLayout = $layout; + break; + } + + // Push mismatches back into the queue + array_map([$this, 'pushToQueue'], $currentImages); + $currentImages = []; + } + + if ($selectedLayout) + { + // Hurray, we've got something that works + usort($currentImages, [$this, 'orderPhotos']); + $this->processedImages += count($currentImages); + return [$currentImages, $selectedLayout]; + } + else + { + // 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) + { + 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. + // Leave images of different types as-is + if ($a->isLandscape() !== $b->isLandscape()) + return 0; + + // Otherwise, 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(); + return -$priority_diff; } - private static function daysApart(Image $a, Image $b) + private function pushToQueue(Image $image) { - return $a->getDateCaptured()->diff($b->getDateCaptured())->days; - } - - 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++) - { - $num_portrait += $image->isPortrait() ? 1 : 0; - $num_landscape += $image->isLandscape() ? 1 : 0; - $photos[] = $image; - } - - // 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) - { - // 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']; - } - elseif (count($photos) === 2) - return [$photos, 'duo']; - else - return [$photos, 'single']; - } - - // Last resort: majority vote - if ($num_portrait > $num_landscape) - return [$photos, 'portraits']; - else - return [$photos, 'landscapes']; + $this->queue[] = $image; } } diff --git a/public/css/default.css b/public/css/default.css index fc9988a5..c119c9dc 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; } } diff --git a/templates/PhotosIndex.php b/templates/PhotosIndex.php index b01a9230..c9fe7159 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 '