Add new horizontal masonry and image height allocation
- Move image templates to content/image sub-folder
This commit is contained in:
parent
e01040a2e8
commit
163a85c78f
|
@ -0,0 +1,154 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023, the Friendica project
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Friendica\Content;
|
||||
|
||||
use Friendica\Content\Image\Collection\MasonryImageRow;
|
||||
use Friendica\Content\Image\Entity\MasonryImage;
|
||||
use Friendica\Content\Post\Collection\PostMedias;
|
||||
use Friendica\Core\Renderer;
|
||||
|
||||
class Image
|
||||
{
|
||||
public static function getBodyAttachHtml(PostMedias $PostMediaImages): string
|
||||
{
|
||||
$media = '';
|
||||
|
||||
if ($PostMediaImages->haveDimensions()) {
|
||||
if (count($PostMediaImages) > 1) {
|
||||
$media = self::getHorizontalMasonryHtml($PostMediaImages);
|
||||
} elseif (count($PostMediaImages) == 1) {
|
||||
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
|
||||
'$image' => $PostMediaImages[0],
|
||||
'$allocated_height' => $PostMediaImages[0]->getAllocatedHeight(),
|
||||
'$allocated_max_width' => ($PostMediaImages[0]->previewWidth ?? $PostMediaImages[0]->width) . 'px',
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
if (count($PostMediaImages) > 1) {
|
||||
$media = self::getImageGridHtml($PostMediaImages);
|
||||
} elseif (count($PostMediaImages) == 1) {
|
||||
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single.tpl'), [
|
||||
'$image' => $PostMediaImages[0],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PostMedias $images
|
||||
* @return string
|
||||
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
|
||||
*/
|
||||
private static function getImageGridHtml(PostMedias $images): string
|
||||
{
|
||||
// Image for first column (fc) and second column (sc)
|
||||
$images_fc = [];
|
||||
$images_sc = [];
|
||||
|
||||
for ($i = 0; $i < count($images); $i++) {
|
||||
($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]);
|
||||
}
|
||||
|
||||
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/grid.tpl'), [
|
||||
'columns' => [
|
||||
'fc' => $images_fc,
|
||||
'sc' => $images_sc,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a horizontally masoned gallery with a fixed maximum number of pictures per row.
|
||||
*
|
||||
* For each row, we calculate how much of the total width each picture will take depending on their aspect ratio
|
||||
* and how much relative height it needs to accomodate all pictures next to each other with their height normalized.
|
||||
*
|
||||
* @param array $images
|
||||
* @return string
|
||||
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
|
||||
*/
|
||||
private static function getHorizontalMasonryHtml(PostMedias $images): string
|
||||
{
|
||||
static $column_size = 2;
|
||||
|
||||
$rows = array_map(
|
||||
function (PostMedias $PostMediaImages) {
|
||||
if ($singleImageInRow = count($PostMediaImages) == 1) {
|
||||
$PostMediaImages[] = $PostMediaImages[0];
|
||||
}
|
||||
|
||||
$widths = [];
|
||||
$heights = [];
|
||||
foreach ($PostMediaImages as $PostMediaImage) {
|
||||
if ($PostMediaImage->width && $PostMediaImage->height) {
|
||||
$widths[] = $PostMediaImage->width;
|
||||
$heights[] = $PostMediaImage->height;
|
||||
} else {
|
||||
$widths[] = $PostMediaImage->previewWidth;
|
||||
$heights[] = $PostMediaImage->previewHeight;
|
||||
}
|
||||
}
|
||||
|
||||
$maxHeight = max($heights);
|
||||
|
||||
// Corrected width preserving aspect ratio when all images on a row are the same height
|
||||
$correctedWidths = [];
|
||||
foreach ($widths as $i => $width) {
|
||||
$correctedWidths[] = $width * $maxHeight / $heights[$i];
|
||||
}
|
||||
|
||||
$totalWidth = array_sum($correctedWidths);
|
||||
|
||||
$row_images2 = [];
|
||||
|
||||
if ($singleImageInRow) {
|
||||
unset($PostMediaImages[1]);
|
||||
}
|
||||
|
||||
foreach ($PostMediaImages as $i => $PostMediaImage) {
|
||||
$row_images2[] = new MasonryImage(
|
||||
$PostMediaImage->uriId,
|
||||
$PostMediaImage->url,
|
||||
$PostMediaImage->preview,
|
||||
$PostMediaImage->description ?? '',
|
||||
100 * $correctedWidths[$i] / $totalWidth,
|
||||
100 * $maxHeight / $correctedWidths[$i]
|
||||
);
|
||||
}
|
||||
|
||||
// This magic value will stay constant for each image of any given row and is ultimately
|
||||
// used to determine the height of the row container relative to the available width.
|
||||
$commonHeightRatio = 100 * $correctedWidths[0] / $totalWidth / ($widths[0] / $heights[0]);
|
||||
|
||||
return new MasonryImageRow($row_images2, count($row_images2), $commonHeightRatio);
|
||||
},
|
||||
$images->chunk($column_size)
|
||||
);
|
||||
|
||||
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/horizontal_masonry.tpl'), [
|
||||
'$rows' => $rows,
|
||||
'$column_size' => $column_size,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023, the Friendica project
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Friendica\Content\Image\Collection;
|
||||
|
||||
use Friendica\Content\Image\Entity;
|
||||
use Friendica\BaseCollection;
|
||||
use Friendica\Content\Image\Entity\MasonryImage;
|
||||
|
||||
class MasonryImageRow extends BaseCollection
|
||||
{
|
||||
/** @var ?float */
|
||||
protected $heightRatio;
|
||||
|
||||
/**
|
||||
* @param MasonryImage[] $entities
|
||||
* @param int|null $totalCount
|
||||
* @param float|null $heightRatio
|
||||
*/
|
||||
public function __construct(array $entities = [], int $totalCount = null, float $heightRatio = null)
|
||||
{
|
||||
parent::__construct($entities, $totalCount);
|
||||
|
||||
$this->heightRatio = $heightRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Entity\MasonryImage
|
||||
*/
|
||||
public function current(): Entity\MasonryImage
|
||||
{
|
||||
return parent::current();
|
||||
}
|
||||
|
||||
public function getHeightRatio(): ?float
|
||||
{
|
||||
return $this->heightRatio;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (C) 2010-2023, the Friendica project
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Friendica\Content\Image\Entity;
|
||||
|
||||
use Friendica\BaseEntity;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @property-read int $uriId
|
||||
* @property-read UriInterface $url
|
||||
* @property-read ?UriInterface $preview
|
||||
* @property-read string $description
|
||||
* @property-read float $heightRatio
|
||||
* @property-read float $widthRatio
|
||||
* @see \Friendica\Content\Image::getHorizontalMasonryHtml()
|
||||
*/
|
||||
class MasonryImage extends BaseEntity
|
||||
{
|
||||
/** @var int */
|
||||
protected $uriId;
|
||||
/** @var UriInterface */
|
||||
protected $url;
|
||||
/** @var ?UriInterface */
|
||||
protected $preview;
|
||||
/** @var string */
|
||||
protected $description;
|
||||
/** @var float Ratio of the width of the image relative to the total width of the images on the row */
|
||||
protected $widthRatio;
|
||||
/** @var float Ratio of the height of the image relative to its width for height allocation */
|
||||
protected $heightRatio;
|
||||
|
||||
public function __construct(int $uriId, UriInterface $url, ?UriInterface $preview, string $description, float $widthRatio, float $heightRatio)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->uriId = $uriId;
|
||||
$this->preview = $preview;
|
||||
$this->description = $description;
|
||||
$this->widthRatio = $widthRatio;
|
||||
$this->heightRatio = $heightRatio;
|
||||
}
|
||||
}
|
|
@ -42,4 +42,16 @@ class PostMedias extends BaseCollection
|
|||
{
|
||||
return parent::current();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether all the collection's item have at least one set of dimensions provided
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function haveDimensions(): bool
|
||||
{
|
||||
return array_reduce($this->getArrayCopy(), function (bool $carry, Entity\PostMedia $item) {
|
||||
return $carry && $item->hasDimensions();
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,6 +188,30 @@ class PostMedia extends BaseEntity
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the allocated height value used in the content/image/single_with_height_allocation.tpl template
|
||||
*
|
||||
* Either base or preview dimensions need to be set at runtime.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAllocatedHeight(): string
|
||||
{
|
||||
if (!$this->hasDimensions()) {
|
||||
throw new \RangeException('Either width and height or previewWidth and previewHeight must be defined to use this method.');
|
||||
}
|
||||
|
||||
if ($this->width && $this->height) {
|
||||
$width = $this->width;
|
||||
$height = $this->height;
|
||||
} else {
|
||||
$width = $this->previewWidth;
|
||||
$height = $this->previewHeight;
|
||||
}
|
||||
|
||||
return (100 * $height / $width) . '%';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new PostMedia entity with a different preview URI and an optional proxy size name.
|
||||
* The new entity preview's width and height are rescaled according to the provided size.
|
||||
|
@ -263,4 +287,14 @@ class PostMedia extends BaseEntity
|
|||
$this->id,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the media has at least one full set of dimensions, needed for the height allocation feature
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDimensions(): bool
|
||||
{
|
||||
return $this->width && $this->height || $this->previewWidth && $this->previewHeight;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
namespace Friendica\Model;
|
||||
|
||||
use Friendica\Contact\LocalRelationship\Entity\LocalRelationship;
|
||||
use Friendica\Content\Image;
|
||||
use Friendica\Content\Post\Collection\PostMedias;
|
||||
use Friendica\Content\Post\Entity\PostMedia;
|
||||
use Friendica\Content\Text\BBCode;
|
||||
|
@ -3241,7 +3242,7 @@ class Item
|
|||
}
|
||||
|
||||
if (!empty($sharedSplitAttachments)) {
|
||||
$s = self::addGallery($s, $sharedSplitAttachments['visual'], $item['uri-id']);
|
||||
$s = self::addGallery($s, $sharedSplitAttachments['visual']);
|
||||
$s = self::addVisualAttachments($sharedSplitAttachments['visual'], $shared_item, $s, true);
|
||||
$s = self::addLinkAttachment($shared_uri_id ?: $item['uri-id'], $sharedSplitAttachments, $body, $s, true, $quote_shared_links);
|
||||
$s = self::addNonVisualAttachments($sharedSplitAttachments['additional'], $item, $s, true);
|
||||
|
@ -3254,7 +3255,7 @@ class Item
|
|||
$s = substr($s, 0, $pos);
|
||||
}
|
||||
|
||||
$s = self::addGallery($s, $itemSplitAttachments['visual'], $item['uri-id']);
|
||||
$s = self::addGallery($s, $itemSplitAttachments['visual']);
|
||||
$s = self::addVisualAttachments($itemSplitAttachments['visual'], $item, $s, false);
|
||||
$s = self::addLinkAttachment($item['uri-id'], $itemSplitAttachments, $body, $s, false, $shared_links);
|
||||
$s = self::addNonVisualAttachments($itemSplitAttachments['additional'], $item, $s, false);
|
||||
|
@ -3285,45 +3286,32 @@ class Item
|
|||
return $hook_data['html'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PostMedias $images
|
||||
* @return string
|
||||
* @throws \Friendica\Network\HTTPException\ServiceUnavailableException
|
||||
*/
|
||||
private static function makeImageGrid(PostMedias $images): string
|
||||
{
|
||||
// Image for first column (fc) and second column (sc)
|
||||
$images_fc = [];
|
||||
$images_sc = [];
|
||||
|
||||
for ($i = 0; $i < count($images); $i++) {
|
||||
($i % 2 == 0) ? ($images_fc[] = $images[$i]) : ($images_sc[] = $images[$i]);
|
||||
}
|
||||
|
||||
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image_grid.tpl'), [
|
||||
'columns' => [
|
||||
'fc' => $images_fc,
|
||||
'sc' => $images_sc,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify links to pictures to links for the "Fancybox" gallery
|
||||
*
|
||||
* @param string $s
|
||||
* @param PostMedias $PostMedias
|
||||
* @param int $uri_id
|
||||
* @return string
|
||||
*/
|
||||
private static function addGallery(string $s, PostMedias $PostMedias, int $uri_id): string
|
||||
private static function addGallery(string $s, PostMedias $PostMedias): string
|
||||
{
|
||||
foreach ($PostMedias as $PostMedia) {
|
||||
if (!$PostMedia->preview || ($PostMedia->type !== Post\Media::IMAGE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$s = str_replace('<a href="' . $PostMedia->url . '"', '<a data-fancybox="' . $uri_id . '" href="' . $PostMedia->url . '"', $s);
|
||||
if ($PostMedia->hasDimensions()) {
|
||||
$pattern = '#<a href="' . preg_quote($PostMedia->url) . '">(.*?)"></a>#';
|
||||
|
||||
$s = preg_replace_callback($pattern, function () use ($PostMedia) {
|
||||
return Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image/single_with_height_allocation.tpl'), [
|
||||
'$image' => $PostMedia,
|
||||
'$allocated_height' => $PostMedia->getAllocatedHeight(),
|
||||
]);
|
||||
}, $s);
|
||||
} else {
|
||||
$s = str_replace('<a href="' . $PostMedia->url . '"', '<a data-fancybox="uri-id-' . $PostMedia->uriId . '" href="' . $PostMedia->url . '"', $s);
|
||||
}
|
||||
}
|
||||
|
||||
return $s;
|
||||
|
@ -3494,14 +3482,7 @@ class Item
|
|||
}
|
||||
}
|
||||
|
||||
$media = '';
|
||||
if (count($images) > 1) {
|
||||
$media = self::makeImageGrid($images);
|
||||
} elseif (count($images) == 1) {
|
||||
$media = Renderer::replaceMacros(Renderer::getMarkupTemplate('content/image.tpl'), [
|
||||
'$image' => $images[0],
|
||||
]);
|
||||
}
|
||||
$media = Image::getBodyAttachHtml($images);
|
||||
|
||||
// On Diaspora posts the attached pictures are leading
|
||||
if ($item['network'] == Protocol::DIASPORA) {
|
||||
|
|
|
@ -706,6 +706,39 @@ audio {
|
|||
* Image grid settings END
|
||||
**/
|
||||
|
||||
/* This helps allocating space for image before they are loaded, preventing content shifting once they are.
|
||||
* Inspired by https://www.smashingmagazine.com/2016/08/ways-to-reduce-content-shifting-on-page-load/
|
||||
* Please note: The space is effectively allocated using padding-bottom using the image ratio as a value.
|
||||
* This ratio is never known in advance so no value is set in the stylesheet.
|
||||
*/
|
||||
figure.img-allocated-height {
|
||||
position: relative;
|
||||
background: center / auto rgba(0, 0, 0, 0.05) url(/images/icons/image.png) no-repeat;
|
||||
margin: 0;
|
||||
}
|
||||
figure.img-allocated-height img{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal masonry settings START
|
||||
**/
|
||||
.masonry-row {
|
||||
display: -ms-flexbox; /* IE10 */
|
||||
display: flex;
|
||||
/* Both the following values should be the same to ensure consistent margins between images in the grid */
|
||||
column-gap: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
/**
|
||||
* Horizontal masonry settings AND
|
||||
**/
|
||||
|
||||
#contactblock .icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<div class="imagegrid-row">
|
||||
<div class="imagegrid-column">
|
||||
{{foreach $columns.fc as $img}}
|
||||
{{include file="content/image.tpl" image=$img}}
|
||||
{{include file="content/image/single.tpl" image=$img}}
|
||||
{{/foreach}}
|
||||
</div>
|
||||
<div class="imagegrid-column">
|
||||
{{foreach $columns.sc as $img}}
|
||||
{{include file="content/image.tpl" image=$img}}
|
||||
{{include file="content/image/single.tpl" image=$img}}
|
||||
{{/foreach}}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,12 @@
|
|||
{{foreach $rows as $images}}
|
||||
<div class="masonry-row" style="height: {{$images->getHeightRatio()}}%">
|
||||
{{foreach $images as $image}}
|
||||
{{* The absolute pixel value in the calc() should be mirrored from the .imagegrid-row column-gap value *}}
|
||||
{{include file="content/image/single_with_height_allocation.tpl"
|
||||
image=$image
|
||||
allocated_height="calc(`$image->heightRatio * $image->widthRatio / 100`% - 5px / `$column_size`)"
|
||||
allocated_width="`$image->widthRatio`%"
|
||||
}}
|
||||
{{/foreach}}
|
||||
</div>
|
||||
{{/foreach}}
|
|
@ -0,0 +1,20 @@
|
|||
{{* The padding-top height allocation trick only works if the <figure> fills its parent's width completely or with flex. 🤷♂️
|
||||
As a result, we need to add a wrapping element for non-flex (non-image grid) environments, mostly single-image cases.
|
||||
*}}
|
||||
{{if $allocated_max_width}}
|
||||
<div style="max-width: {{$allocated_max_width|default:"auto"}};">
|
||||
{{/if}}
|
||||
|
||||
<figure class="img-allocated-height" style="width: {{$allocated_width|default:"auto"}}; padding-bottom: {{$allocated_height}}">
|
||||
{{if $image->preview}}
|
||||
<a data-fancybox="uri-id-{{$image->uriId}}" href="{{$image->url}}">
|
||||
<img src="{{$image->preview}}" alt="{{$image->description}}" title="{{$image->description}}" loading="lazy">
|
||||
</a>
|
||||
{{else}}
|
||||
<img src="{{$image->url}}" alt="{{$image->description}}" title="{{$image->description}}" loading="lazy">
|
||||
{{/if}}
|
||||
</figure>
|
||||
|
||||
{{if $allocated_max_width}}
|
||||
</div>
|
||||
{{/if}}
|
|
@ -394,3 +394,7 @@ input[type="text"].tt-input {
|
|||
textarea#profile-jot-text:focus + #preview_profile-jot-text, textarea.comment-edit-text:focus + .comment-edit-form .preview {
|
||||
border-color: $link_color;
|
||||
}
|
||||
|
||||
figure.img-allocated-height {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
|
|
@ -354,3 +354,7 @@ input[type="text"].tt-input {
|
|||
textarea#profile-jot-text:focus + #preview_profile-jot-text, textarea.comment-edit-text:focus + .comment-edit-form .preview {
|
||||
border-color: $link_color;
|
||||
}
|
||||
|
||||
figure.img-allocated-height {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user