[feature] Use blurhashes in frontend, tidy up gallery view a bit (#3948)

* [feature] Use blurhashes in frontend, tidy up gallery view a bit

* weeeeeeeeeeeeeeeee

* beep boop
This commit is contained in:
tobi
2025-03-31 15:51:17 +02:00
committed by GitHub
parent 85fb63f46f
commit 3949117be0
17 changed files with 508 additions and 124 deletions

View File

@@ -48,10 +48,10 @@ type WebPage struct {
// Can be nil.
Stylesheets []string
// Paths to JS files to add to
// the page as "script" entries.
// JS files to add to the
// page as "script" entries.
// Can be nil.
Javascript []string
Javascript []JavascriptEntry
// Extra parameters to pass to
// the template for rendering,
@@ -60,6 +60,21 @@ type WebPage struct {
Extra map[string]any
}
type JavascriptEntry struct {
// Insert <script> tag at the end
// of <body> rather than in <head>.
Bottom bool
// Path to the js file.
Src string
// Use async="" attribute.
Async bool
// Use defer="" attribute.
Defer bool
}
// TemplateWebPage renders the given HTML template and
// page params within the standard GtS "page" template.
//

View File

@@ -144,6 +144,7 @@ func (p *Processor) WebStatusesGet(
ctx context.Context,
targetAccountID string,
mediaOnly bool,
limit int,
maxID string,
) (*apimodel.PageableResponse, gtserror.WithCode) {
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
@@ -164,7 +165,7 @@ func (p *Processor) WebStatusesGet(
ctx,
account,
mediaOnly,
20,
limit,
maxID,
)
if err != nil && !errors.Is(err, db.ErrNoEntries) {

View File

@@ -68,7 +68,6 @@ func (m *Module) domainBlockListGETHandler(c *gin.Context) {
Instance: instance,
OGMeta: apiutil.OGBase(instance),
Stylesheets: []string{cssFA},
Javascript: []string{jsFrontend},
Extra: map[string]any{"blocklist": domainBlocks},
}

View File

@@ -142,11 +142,22 @@ func (m *Module) prepareProfile(c *gin.Context) *profile {
}
}
// Limit varies depending on whether this is a gallery view or not.
// If gallery view, we want a nice full screen of media, else we
// don't want to overwhelm the viewer with a shitload of posts.
var limit int
if account.WebLayout == "gallery" {
limit = 40
} else {
limit = 20
}
// Get statuses from maxStatusID onwards (or from top if empty string).
statusResp, errWithCode := m.processor.Account().WebStatusesGet(
ctx,
account.ID,
mediaOnly,
limit,
maxStatusID,
)
if errWithCode != nil {
@@ -230,7 +241,17 @@ func (m *Module) profileMicroblog(c *gin.Context, p *profile) {
Instance: p.instance,
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
Stylesheets: stylesheets,
Javascript: []string{jsFrontend},
Javascript: []apiutil.JavascriptEntry{
{
Src: jsFrontend,
Async: true,
Defer: true,
},
{
Bottom: true,
Src: jsBlurhash,
},
},
Extra: map[string]any{
"account": p.account,
"rssFeed": p.rssFeed,
@@ -294,7 +315,17 @@ func (m *Module) profileGallery(c *gin.Context, p *profile) {
Instance: p.instance,
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
Stylesheets: stylesheets,
Javascript: []string{jsFrontend},
Javascript: []apiutil.JavascriptEntry{
{
Src: jsFrontend,
Async: true,
Defer: true,
},
{
Bottom: true,
Src: jsBlurhash,
},
},
Extra: map[string]any{
"account": p.account,
"rssFeed": p.rssFeed,

View File

@@ -54,7 +54,13 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) {
cssStatus, // Used for rendering stub/fake statuses.
cssSettings,
},
Javascript: []string{jsSettings},
Javascript: []apiutil.JavascriptEntry{
{
Src: jsSettings,
Async: true,
Defer: true,
},
},
}
apiutil.TemplateWebPage(c, page)

View File

@@ -146,7 +146,17 @@ func (m *Module) threadGETHandler(c *gin.Context) {
Instance: instance,
OGMeta: apiutil.OGBase(instance).WithStatus(context.Status),
Stylesheets: stylesheets,
Javascript: []string{jsFrontend},
Javascript: []apiutil.JavascriptEntry{
{
Src: jsFrontend,
Async: true,
Defer: true,
},
{
Bottom: true,
Src: jsBlurhash,
},
},
Extra: map[string]any{
"context": context,
},

View File

@@ -68,6 +68,7 @@ const (
cssTag = distPathPrefix + "/tag.css"
jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
jsBlurhash = distPathPrefix + "/blurhash.js" // Blurhash rendering JS.
jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
)

View File

@@ -0,0 +1,146 @@
/*
GoToSocial
Copyright (C) GoToSocial Authors admin@gotosocial.org
SPDX-License-Identifier: AGPL-3.0-or-later
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 <http://www.gnu.org/licenses/>.
*/
import { decode } from "blurhash";
// Generate a blurhash canvas for each image for
// each blurhash container and put it in the summary.
Array.from(document.getElementsByClassName('blurhash-container')).forEach(blurhashContainer => {
const hash = blurhashContainer.dataset.blurhashHash;
const thumbHeight = blurhashContainer.dataset.blurhashHeight;
const thumbWidth = blurhashContainer.dataset.blurhashWidth;
const thumbAspect = blurhashContainer.dataset.blurhashAspect;
/*
It's very expensive to draw big canvases
with blurhashes, so use tiny ones, keeping
aspect ratio of the original thumbnail.
*/
var useWidth = 32;
var useHeight = 32;
switch (true) {
case thumbWidth > thumbHeight:
useHeight = Math.round(useWidth / thumbAspect);
break;
case thumbHeight > thumbWidth:
useWidth = Math.round(useHeight * thumbAspect);
break;
}
const pixels = decode(
hash,
useWidth,
useHeight,
);
// Create canvas of appropriate size.
const canvas = document.createElement("canvas");
canvas.width = useWidth;
canvas.height = useHeight;
// Draw the image data into the canvas.
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(
useWidth,
useHeight,
);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
// Put the canvas inside the container.
blurhashContainer.appendChild(canvas);
});
// Add a smooth transition from blurhash
// to image for each sensitive image.
Array.from(document.getElementsByTagName('img')).forEach(img => {
if (!img.dataset.blurhashHash) {
// Has no blurhash,
// can't transition smoothly.
return;
}
if (img.dataset.sensitive !== "true") {
// Not sensitive, smooth
// transition doesn't matter.
return;
}
if (img.complete) {
// Image already loaded,
// don't stub it with anything.
return;
}
const parentSlide = img.closest(".photoswipe-slide");
if (!parentSlide) {
// Parent slide was nil,
// can't do anything.
return;
}
const blurhashContainer = document.querySelector("div[data-blurhash-hash=\"" + img.dataset.blurhashHash + "\"]");
if (!blurhashContainer) {
// Blurhash div was nil,
// can't do anything.
return;
}
const canvas = blurhashContainer.children[0];
if (!canvas) {
// Canvas was nil,
// can't do anything.
return;
}
// "Replace" the hidden img with a canvas
// that will show initially when it's clicked.
const clone = canvas.cloneNode(true);
clone.getContext("2d").drawImage(canvas, 0, 0);
parentSlide.prepend(clone);
img.className = img.className + " hidden";
// Add a listener so that when the spoiler
// is opened, loading of the image begins.
const parentSummary = img.closest(".media-spoiler");
parentSummary.addEventListener("toggle", (_) => {
if (parentSummary.hasAttribute("open") && !img.complete) {
img.loading = "eager";
}
});
// Add a callback that triggers
// when image loading is complete.
img.addEventListener("load", () => {
// Show the image now that it's loaded.
img.className = img.className.replace(" hidden", "");
// Reset the lazy loading tag to its initial
// value. This doesn't matter too much since
// it's already loaded but it feels neater.
img.loading = "lazy";
// Remove the temporary blurhash
// canvas so only the image shows.
const canvas = parentSlide.getElementsByTagName("canvas")[0];
if (canvas) {
canvas.remove();
}
});
});

View File

@@ -42,13 +42,26 @@
height: 100%;
width: 100%;
div.blurhash-container {
z-index: -1;
position: absolute;
height: 100%;
width: 100%;
}
canvas {
height: 100%;
width: 100%;
object-fit: cover;
}
&[open] summary {
height: auto;
width: auto;
margin: 1rem;
padding: 0;
.show, video, img {
.show {
display: none;
}
@@ -57,6 +70,10 @@
grid-column: 1 / span 3;
grid-row: 1 / span 2;
}
div.blurhash-container > canvas {
display: none;
}
}
summary {
@@ -65,7 +82,7 @@
width: 100%;
z-index: 3;
overflow: hidden;
display: grid;
padding: 1rem;
grid-template-columns: 1fr auto 1fr;
@@ -107,24 +124,15 @@
align-self: center;
}
}
video, img {
z-index: -1;
position: absolute;
height: calc(100% + 1.2rem);
width: calc(100% + 1.2rem);
top: -0.6rem;
left: -0.6rem;
filter: blur(1.2rem);
}
}
video.plyr-video, .plyr {
position: absolute;
height: 100%;
width: 100%;
object-fit: contain;
object-fit: cover;
background: $gray1;
min-width: 100%;
}
.unknown-attachment {
@@ -161,6 +169,86 @@
}
}
}
@media screen and (max-width: 55rem) {
/*
Tablet-ish width, make "show sensitive"
and "eye" buttons smaller + more compact.
*/
& > details {
& > summary {
padding: 0.5rem;
> div.show.sensitive.button {
font-size: smaller;
padding: 0.2rem;
line-height: 1.4rem;
}
> span.eye.button {
font-size: smaller;
padding: 0 0.2rem 0 0.2rem;
> .fa-fw {
line-height: 1.4rem;
}
}
}
&[open] > summary {
margin: 0.5rem;
}
}
}
@media screen and (max-width: 36rem) {
/*
Mobile-ish width, even more compact.
*/
& > details {
& > summary {
> div.show.sensitive.button {
font-size: small;
padding: 0.1rem;
line-height: 1.2rem;
}
> span.eye.button {
font-size: small;
padding: 0 0.1rem 0 0.1rem;
> .fa-fw {
line-height: 1.2rem;
}
}
}
}
}
@media screen and (max-width: 27rem) {
/*
Really really tiny, make the text
on show/hide sensitive even smaller.
*/
& > details > summary > div.show.sensitive.button {
font-size: x-small;
}
}
}
@media (scripting: none) {
.media-wrapper {
canvas.blurhash {
display: none
}
& > details:not([open]) {
background: linear-gradient(
var(--bg-accent),
var(--bg)
);
}
}
}
.pswp__button--open-post-link {
@@ -187,21 +275,75 @@
position: initial;
padding: 0.1rem;
padding-top: 0.2rem;
gap: 0.15rem;
.plyr__controls__item {
/*
Override margins from plyr as
we're displaying in flex with a gap.
*/
margin-left: 0;
margin-right: 0;
&:first-child {
margin-right: 0;
}
/*
Try to split controls in at
least a somewhat sensible way.
*/
&.plyr__volume {
margin-left: auto;
}
&[data-plyr="restart"] {
margin-right: auto;
}
}
/*
Override the rule from plyr that
hides the total duration on thinner
screens, we have enough room.
*/
.plyr__time + .plyr__time {
display: initial;
}
}
.plyr__control {
box-shadow: none;
}
.plyr__control--overlaid {
top: calc(50% - 18px);
.plyr__poster {
background-size: cover;
}
/*
Hide plry controls when it's not inside
a lightbox, but use cursor pointer to
show the whole thing can be clicked.
*/
&.plyr--stopped, &.plyr--paused {
.plyr__controls {
display: none;
}
cursor: pointer;
}
}
.pswp__content {
padding: 2rem;
.plyr {
max-height: 100%;
/*
Render plyr controls as normal
when it's inside a lightbox.
*/
.plyr__control--overlaid {
top: calc(50% - 18px);
}
> .plyr--stopped .plyr__controls,
> .plyr--paused .plyr__controls {
display: flex;
}
}

View File

@@ -35,7 +35,12 @@
grid-row: span 2;
}
@media screen and (max-width: 42rem) {
/*
On really skinny screens allow
media wrapper to take up full
width instead of showing 2 columns.
*/
@media screen and (max-width: 23rem) {
.media-wrapper {
grid-column: span 2;
grid-row: span 2;

View File

@@ -72,27 +72,16 @@
margin-top: 0.15rem;
margin-bottom: 0.15rem;
/* Show 3 cols of media */
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.15rem;
/* Desktop-ish width, show 3 cols of media */
grid-template-columns: repeat(3, 1fr);
@media screen and (max-width: 55rem) {
/* Tablet-ish width, switch to 2 cols */
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 36rem) {
/* Mobile-ish width, switch to 1 col */
grid-template-columns: repeat(1, 1fr);
}
.media-wrapper {
aspect-ratio: 4/3;
aspect-ratio: 1;
border: 0;
border-radius: 0;
background: $bg;
background: $status-bg;
}
}

View File

@@ -60,28 +60,39 @@ lightbox.addFilter('itemData', (item) => {
el._plyrContainer !== undefined
) {
const parentNode = el._plyrContainer.parentNode;
const loopingAuto = el.classList.contains("gifv");
return {
alt: el.getAttribute("alt"),
_video: {
open(c) {
c.appendChild(el._plyrContainer);
if (loopingAuto) {
// Start playing
// when opened.
el._player.play();
}
},
close() {
parentNode.appendChild(el._plyrContainer);
},
pause() {
el._player.pause();
},
play() {
el._player.play();
}
},
width: parseInt(el.dataset.pswpWidth),
height: parseInt(el.dataset.pswpHeight),
parentStatus: el.dataset.pswpParentStatus,
attachmentId: el.dataset.pswpAttachmentId,
loopingAuto: loopingAuto,
};
}
return item;
});
// Open video when user moves to its slide.
lightbox.on("contentActivate", (e) => {
const { content } = e;
if (content.data._video != undefined) {
@@ -89,6 +100,8 @@ lightbox.on("contentActivate", (e) => {
}
});
// Pause + close video when user
// moves away from its slide.
lightbox.on("contentDeactivate", (e) => {
const { content } = e;
if (content.data._video != undefined) {
@@ -97,12 +110,27 @@ lightbox.on("contentDeactivate", (e) => {
}
});
lightbox.on("close", function () {
// Pause video when lightbox is closed.
lightbox.on("closingAnimationStart", function () {
if (lightbox.pswp.currSlide.data._video != undefined) {
lightbox.pswp.currSlide.data._video.close();
}
});
lightbox.on("close", function () {
if (lightbox.pswp.currSlide.data._video != undefined &&
!lightbox.pswp.currSlide.data.loopingAuto) {
lightbox.pswp.currSlide.data._video.pause();
}
});
// Open video when lightbox is opened.
lightbox.on("openingAnimationEnd", function () {
if (lightbox.pswp.currSlide.data._video != undefined) {
lightbox.pswp.currSlide.data._video.play();
}
});
// Add "open this post" link to lightbox UI.
lightbox.on('uiRegister', function() {
lightbox.pswp.ui.registerElement({
name: 'open-post-link',
@@ -164,59 +192,48 @@ dynamicSpoiler("media-spoiler", (spoiler) => {
Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
const loopingAuto = !reduceMotion.matches && video.classList.contains("gifv");
if (loopingAuto) {
// If we're able to play this as a
// looping gifv, then do so, else fall
// back to user-controllable video player.
video.draggable = false;
video.autoplay = true;
video.loop = true;
video.classList.remove("photoswipe-slide");
video.classList.remove("plry-video");
video.load();
video.play();
return;
}
let player = new Plyr(video, {
title: video.title,
settings: [],
controls: ['play-large', 'play', 'progress', 'current-time', 'volume', 'mute', 'fullscreen'],
disableContextMenu: false,
hideControls: false,
// Only show controls for video and audio,
// not looping soundless gifv. Don't show
// volume slider as it's unusable anyway
// when the video is inside a lightbox,
// mute toggle will have to be enough.
controls: loopingAuto
? []
: [
'play-large', // The large play button in the center
'restart', // Restart playback
'rewind', // Rewind by the seek time (default 10 seconds)
'play', // Play/pause playback
'fast-forward', // Fast forward by the seek time (default 10 seconds)
'current-time', // The current time of playback
'duration', // The full duration of the media
'mute', // Toggle mute
'fullscreen', // Toggle fullscreen
],
tooltips: { controls: true, seek: true },
iconUrl: "/assets/plyr.svg",
invertTime: false,
hideControls: false,
listeners: {
fullscreen: () => {
// Check if the photoswipe lightbox is
// open with this as the current slide.
const alreadyInLightbox = (
lightbox.pswp !== undefined &&
video.dataset.pswpAttachmentId === lightbox.pswp.currSlide.data.attachmentId
);
if (alreadyInLightbox) {
// If this video is already open as the
// current photoswipe slide, the fullscreen
// button toggles proper fullscreen.
player.fullscreen.toggle();
} else {
// Otherwise the fullscreen button opens
// the video as current photoswipe slide.
//
// (Don't pause the video while it's
// being transitioned to a slide.)
if (player.playing) {
setTimeout(() => player.play(), 1);
}
play: (_) => {
if (!inLightbox(video)) {
// If the video isn't open in the lightbox
// as the current photoswipe slide, clicking
// on it to play it opens it in the lightbox.
lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
gallery: video.closest(".photoswipe-gallery")
});
} else if (!loopingAuto) {
// If the video *is* open in the lightbox,
// and it's not a looping gifv, clicking
// play just plays or pauses the video.
player.togglePlay();
}
return false;
}
},
}
});
@@ -225,6 +242,21 @@ Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
video._plyrContainer = player.elements.container;
});
// Return true if the photoswipe lightbox is
// open with this element as the current slide.
function inLightbox(element) {
if (lightbox.pswp === undefined) {
return false;
}
if (lightbox.pswp.currSlide === undefined) {
return false;
}
return element.dataset.pswpAttachmentId ===
lightbox.pswp.currSlide.data.attachmentId;
}
Array.from(document.getElementsByTagName('time')).forEach(timeTag => {
const datetime = timeTag.getAttribute('datetime');
const currentText = timeTag.textContent.trim();

View File

@@ -64,6 +64,15 @@ skulk({
}]
],
},
blurhash: {
entryFile: "blurhash",
outputFile: "blurhash.js",
preset: ["js"],
prodCfg: prodCfg,
transform: [
["babelify", { global: true }]
],
},
settings: {
entryFile: "settings",
outputFile: "settings.js",

View File

@@ -13,6 +13,7 @@
"dependencies": {
"@reduxjs/toolkit": "^1.8.6",
"ariakit": "^2.0.0-next.41",
"blurhash": "^2.0.5",
"get-by-dot": "^1.0.2",
"html-to-text": "^9.0.5",
"is-valid-domain": "^0.1.6",

View File

@@ -2398,6 +2398,11 @@ bluebird@^3.7.1, bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
blurhash@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-2.0.5.tgz#efde729fc14a2f03571a6aa91b49cba80d1abe4b"
integrity sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"

View File

@@ -67,7 +67,9 @@ image/webp
<link rel="apple-touch-startup-image" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
{{- include "page_stylesheets.tmpl" . | indent 2 }}
{{- range .javascript }}
<script type="text/javascript" src="{{- . -}}" async="" defer=""></script>
{{- if not .Bottom }}
<script type="text/javascript" src="{{- .Src -}}"{{- if .Async }} async=""{{- end -}}{{- if .Defer }} defer=""{{- end -}}></script>
{{- end }}
{{- end }}
<title>{{- template "instanceTitle" . -}}</title>
</head>
@@ -82,5 +84,10 @@ image/webp
<footer class="page-footer">
{{- include "page_footer.tmpl" . | indent 3 }}
</footer>
{{- range .javascript }}
{{- if .Bottom }}
<script type="text/javascript" src="{{- .Src -}}"></script>
{{- end }}
{{- end }}
</body>
</html>

View File

@@ -17,33 +17,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ -}}
{{- define "imagePreview" }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Original.Width -}}"
height="{{- .Meta.Original.Height -}}"
/>
{{- end }}
{{- define "videoPreview" }}
<img
src="{{- .PreviewURL -}}"
loading="lazy"
{{- if .Description }}
alt="{{- .Description -}}"
title="{{- .Description -}}"
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
/>
{{- end }}
{{- define "audioPreview" }}
{{- define "preview" }}
{{- if and .PreviewURL .Meta.Small.Width }}
<img
src="{{- .PreviewURL -}}"
@@ -54,6 +28,8 @@
{{- end }}
width="{{- .Meta.Small.Width -}}"
height="{{- .Meta.Small.Height -}}"
data-blurhash-hash="{{- .Blurhash -}}"
data-sensitive="{{- .Sensitive -}}"
/>
{{- else }}
<img
@@ -72,29 +48,38 @@
{{- with . }}
<div class="media-wrapper">
<details class="{{- .Item.Type -}}-spoiler media-spoiler" {{- if not .Item.Sensitive }} open{{- end -}}>
<summary>
<summary
{{- if .Item.Description }}
title="{{- .Item.Description -}}"
{{- end }}
>
<div class="show sensitive button" aria-hidden="true">Show sensitive</div>
<span class="eye button" role="button" tabindex="0" aria-label="Toggle media">
<span class="eye button" role="button" tabindex="0" aria-label="Toggle media visibility">
<i class="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
</span>
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
{{- include "videoPreview" .Item | indent 3 }}
{{- else if eq .Item.Type "image" }}
{{- include "imagePreview" .Item | indent 3 }}
{{- else if eq .Item.Type "audio" }}
{{- include "audioPreview" .Item | indent 3 }}
{{- if and (not (eq .Item.Type "unknown")) .Item.Meta.Small.Width }}
<div
class="blurhash-container"
data-blurhash-width="{{- .Item.Meta.Small.Width -}}"
data-blurhash-height="{{- .Item.Meta.Small.Height -}}"
data-blurhash-hash="{{- .Item.Blurhash -}}"
data-blurhash-aspect="{{- .Item.Meta.Small.Aspect -}}"
></div>
{{- end }}
</summary>
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
<video
{{- if eq .Item.Type "video" }}
preload="none"
class="plyr-video photoswipe-slide"
{{- else }}
preload="auto"
muted
autoplay
loop
class="plyr-video photoswipe-slide gifv"
{{- end }}
class="plyr-video photoswipe-slide{{- if eq .Item.Type "gifv" }} gifv{{ end }}"
controls
playsinline
data-pswp-index="{{- .Index -}}"
@@ -125,8 +110,8 @@
data-pswp-height="{{- .Item.Meta.Small.Height -}}px"
{{- else }}
poster="/assets/logo.webp"
width="518px"
height="460px"
data-pswp-width="518px"
data-pswp-height="460px"
{{- end }}
{{- if .Item.Description }}
alt="{{- .Item.Description -}}"
@@ -152,7 +137,7 @@
{{- end }}
>
{{- with .Item }}
{{- include "imagePreview" . | indent 3 }}
{{- include "preview" . | indent 3 }}
{{- end }}
</a>
{{- else }}