mirror of
https://github.com/superseriousbusiness/gotosocial
synced 2025-06-05 21:59:39 +02:00
[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:
@@ -48,10 +48,10 @@ type WebPage struct {
|
|||||||
// Can be nil.
|
// Can be nil.
|
||||||
Stylesheets []string
|
Stylesheets []string
|
||||||
|
|
||||||
// Paths to JS files to add to
|
// JS files to add to the
|
||||||
// the page as "script" entries.
|
// page as "script" entries.
|
||||||
// Can be nil.
|
// Can be nil.
|
||||||
Javascript []string
|
Javascript []JavascriptEntry
|
||||||
|
|
||||||
// Extra parameters to pass to
|
// Extra parameters to pass to
|
||||||
// the template for rendering,
|
// the template for rendering,
|
||||||
@@ -60,6 +60,21 @@ type WebPage struct {
|
|||||||
Extra map[string]any
|
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
|
// TemplateWebPage renders the given HTML template and
|
||||||
// page params within the standard GtS "page" template.
|
// page params within the standard GtS "page" template.
|
||||||
//
|
//
|
||||||
|
@@ -144,6 +144,7 @@ func (p *Processor) WebStatusesGet(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetAccountID string,
|
targetAccountID string,
|
||||||
mediaOnly bool,
|
mediaOnly bool,
|
||||||
|
limit int,
|
||||||
maxID string,
|
maxID string,
|
||||||
) (*apimodel.PageableResponse, gtserror.WithCode) {
|
) (*apimodel.PageableResponse, gtserror.WithCode) {
|
||||||
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
account, err := p.state.DB.GetAccountByID(ctx, targetAccountID)
|
||||||
@@ -164,7 +165,7 @@ func (p *Processor) WebStatusesGet(
|
|||||||
ctx,
|
ctx,
|
||||||
account,
|
account,
|
||||||
mediaOnly,
|
mediaOnly,
|
||||||
20,
|
limit,
|
||||||
maxID,
|
maxID,
|
||||||
)
|
)
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
@@ -68,7 +68,6 @@ func (m *Module) domainBlockListGETHandler(c *gin.Context) {
|
|||||||
Instance: instance,
|
Instance: instance,
|
||||||
OGMeta: apiutil.OGBase(instance),
|
OGMeta: apiutil.OGBase(instance),
|
||||||
Stylesheets: []string{cssFA},
|
Stylesheets: []string{cssFA},
|
||||||
Javascript: []string{jsFrontend},
|
|
||||||
Extra: map[string]any{"blocklist": domainBlocks},
|
Extra: map[string]any{"blocklist": domainBlocks},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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).
|
// Get statuses from maxStatusID onwards (or from top if empty string).
|
||||||
statusResp, errWithCode := m.processor.Account().WebStatusesGet(
|
statusResp, errWithCode := m.processor.Account().WebStatusesGet(
|
||||||
ctx,
|
ctx,
|
||||||
account.ID,
|
account.ID,
|
||||||
mediaOnly,
|
mediaOnly,
|
||||||
|
limit,
|
||||||
maxStatusID,
|
maxStatusID,
|
||||||
)
|
)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
@@ -230,7 +241,17 @@ func (m *Module) profileMicroblog(c *gin.Context, p *profile) {
|
|||||||
Instance: p.instance,
|
Instance: p.instance,
|
||||||
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
|
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
|
||||||
Stylesheets: stylesheets,
|
Stylesheets: stylesheets,
|
||||||
Javascript: []string{jsFrontend},
|
Javascript: []apiutil.JavascriptEntry{
|
||||||
|
{
|
||||||
|
Src: jsFrontend,
|
||||||
|
Async: true,
|
||||||
|
Defer: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Bottom: true,
|
||||||
|
Src: jsBlurhash,
|
||||||
|
},
|
||||||
|
},
|
||||||
Extra: map[string]any{
|
Extra: map[string]any{
|
||||||
"account": p.account,
|
"account": p.account,
|
||||||
"rssFeed": p.rssFeed,
|
"rssFeed": p.rssFeed,
|
||||||
@@ -294,7 +315,17 @@ func (m *Module) profileGallery(c *gin.Context, p *profile) {
|
|||||||
Instance: p.instance,
|
Instance: p.instance,
|
||||||
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
|
OGMeta: apiutil.OGBase(p.instance).WithAccount(p.account),
|
||||||
Stylesheets: stylesheets,
|
Stylesheets: stylesheets,
|
||||||
Javascript: []string{jsFrontend},
|
Javascript: []apiutil.JavascriptEntry{
|
||||||
|
{
|
||||||
|
Src: jsFrontend,
|
||||||
|
Async: true,
|
||||||
|
Defer: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Bottom: true,
|
||||||
|
Src: jsBlurhash,
|
||||||
|
},
|
||||||
|
},
|
||||||
Extra: map[string]any{
|
Extra: map[string]any{
|
||||||
"account": p.account,
|
"account": p.account,
|
||||||
"rssFeed": p.rssFeed,
|
"rssFeed": p.rssFeed,
|
||||||
|
@@ -54,7 +54,13 @@ func (m *Module) SettingsPanelHandler(c *gin.Context) {
|
|||||||
cssStatus, // Used for rendering stub/fake statuses.
|
cssStatus, // Used for rendering stub/fake statuses.
|
||||||
cssSettings,
|
cssSettings,
|
||||||
},
|
},
|
||||||
Javascript: []string{jsSettings},
|
Javascript: []apiutil.JavascriptEntry{
|
||||||
|
{
|
||||||
|
Src: jsSettings,
|
||||||
|
Async: true,
|
||||||
|
Defer: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
apiutil.TemplateWebPage(c, page)
|
apiutil.TemplateWebPage(c, page)
|
||||||
|
@@ -146,7 +146,17 @@ func (m *Module) threadGETHandler(c *gin.Context) {
|
|||||||
Instance: instance,
|
Instance: instance,
|
||||||
OGMeta: apiutil.OGBase(instance).WithStatus(context.Status),
|
OGMeta: apiutil.OGBase(instance).WithStatus(context.Status),
|
||||||
Stylesheets: stylesheets,
|
Stylesheets: stylesheets,
|
||||||
Javascript: []string{jsFrontend},
|
Javascript: []apiutil.JavascriptEntry{
|
||||||
|
{
|
||||||
|
Src: jsFrontend,
|
||||||
|
Async: true,
|
||||||
|
Defer: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Bottom: true,
|
||||||
|
Src: jsBlurhash,
|
||||||
|
},
|
||||||
|
},
|
||||||
Extra: map[string]any{
|
Extra: map[string]any{
|
||||||
"context": context,
|
"context": context,
|
||||||
},
|
},
|
||||||
|
@@ -68,6 +68,7 @@ const (
|
|||||||
cssTag = distPathPrefix + "/tag.css"
|
cssTag = distPathPrefix + "/tag.css"
|
||||||
|
|
||||||
jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
|
jsFrontend = distPathPrefix + "/frontend.js" // Progressive enhancement frontend JS.
|
||||||
|
jsBlurhash = distPathPrefix + "/blurhash.js" // Blurhash rendering JS.
|
||||||
jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
|
jsSettings = distPathPrefix + "/settings.js" // Settings panel React application.
|
||||||
)
|
)
|
||||||
|
|
||||||
|
146
web/source/blurhash/index.js
Normal file
146
web/source/blurhash/index.js
Normal 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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@@ -42,13 +42,26 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 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 {
|
&[open] summary {
|
||||||
height: auto;
|
height: auto;
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
.show, video, img {
|
.show {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +70,10 @@
|
|||||||
grid-column: 1 / span 3;
|
grid-column: 1 / span 3;
|
||||||
grid-row: 1 / span 2;
|
grid-row: 1 / span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.blurhash-container > canvas {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
summary {
|
summary {
|
||||||
@@ -65,7 +82,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
@@ -107,24 +124,15 @@
|
|||||||
align-self: center;
|
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 {
|
video.plyr-video, .plyr {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
object-fit: contain;
|
object-fit: cover;
|
||||||
background: $gray1;
|
background: $gray1;
|
||||||
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.unknown-attachment {
|
.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 {
|
.pswp__button--open-post-link {
|
||||||
@@ -187,21 +275,75 @@
|
|||||||
position: initial;
|
position: initial;
|
||||||
padding: 0.1rem;
|
padding: 0.1rem;
|
||||||
padding-top: 0.2rem;
|
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 {
|
.plyr__control {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plyr__control--overlaid {
|
.plyr__poster {
|
||||||
top: calc(50% - 18px);
|
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 {
|
.pswp__content {
|
||||||
padding: 2rem;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -35,7 +35,12 @@
|
|||||||
grid-row: span 2;
|
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 {
|
.media-wrapper {
|
||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
grid-row: span 2;
|
grid-row: span 2;
|
||||||
|
@@ -72,27 +72,16 @@
|
|||||||
margin-top: 0.15rem;
|
margin-top: 0.15rem;
|
||||||
margin-bottom: 0.15rem;
|
margin-bottom: 0.15rem;
|
||||||
|
|
||||||
|
/* Show 3 cols of media */
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.15rem;
|
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 {
|
.media-wrapper {
|
||||||
aspect-ratio: 4/3;
|
aspect-ratio: 1;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background: $bg;
|
background: $status-bg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -60,28 +60,39 @@ lightbox.addFilter('itemData', (item) => {
|
|||||||
el._plyrContainer !== undefined
|
el._plyrContainer !== undefined
|
||||||
) {
|
) {
|
||||||
const parentNode = el._plyrContainer.parentNode;
|
const parentNode = el._plyrContainer.parentNode;
|
||||||
|
const loopingAuto = el.classList.contains("gifv");
|
||||||
return {
|
return {
|
||||||
alt: el.getAttribute("alt"),
|
alt: el.getAttribute("alt"),
|
||||||
_video: {
|
_video: {
|
||||||
open(c) {
|
open(c) {
|
||||||
c.appendChild(el._plyrContainer);
|
c.appendChild(el._plyrContainer);
|
||||||
|
if (loopingAuto) {
|
||||||
|
// Start playing
|
||||||
|
// when opened.
|
||||||
|
el._player.play();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
close() {
|
close() {
|
||||||
parentNode.appendChild(el._plyrContainer);
|
parentNode.appendChild(el._plyrContainer);
|
||||||
},
|
},
|
||||||
pause() {
|
pause() {
|
||||||
el._player.pause();
|
el._player.pause();
|
||||||
|
},
|
||||||
|
play() {
|
||||||
|
el._player.play();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
width: parseInt(el.dataset.pswpWidth),
|
width: parseInt(el.dataset.pswpWidth),
|
||||||
height: parseInt(el.dataset.pswpHeight),
|
height: parseInt(el.dataset.pswpHeight),
|
||||||
parentStatus: el.dataset.pswpParentStatus,
|
parentStatus: el.dataset.pswpParentStatus,
|
||||||
attachmentId: el.dataset.pswpAttachmentId,
|
attachmentId: el.dataset.pswpAttachmentId,
|
||||||
|
loopingAuto: loopingAuto,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Open video when user moves to its slide.
|
||||||
lightbox.on("contentActivate", (e) => {
|
lightbox.on("contentActivate", (e) => {
|
||||||
const { content } = e;
|
const { content } = e;
|
||||||
if (content.data._video != undefined) {
|
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) => {
|
lightbox.on("contentDeactivate", (e) => {
|
||||||
const { content } = e;
|
const { content } = e;
|
||||||
if (content.data._video != undefined) {
|
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) {
|
if (lightbox.pswp.currSlide.data._video != undefined) {
|
||||||
lightbox.pswp.currSlide.data._video.close();
|
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.on('uiRegister', function() {
|
||||||
lightbox.pswp.ui.registerElement({
|
lightbox.pswp.ui.registerElement({
|
||||||
name: 'open-post-link',
|
name: 'open-post-link',
|
||||||
@@ -164,59 +192,48 @@ dynamicSpoiler("media-spoiler", (spoiler) => {
|
|||||||
|
|
||||||
Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
|
Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
|
||||||
const loopingAuto = !reduceMotion.matches && video.classList.contains("gifv");
|
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, {
|
let player = new Plyr(video, {
|
||||||
title: video.title,
|
title: video.title,
|
||||||
settings: [],
|
settings: [],
|
||||||
controls: ['play-large', 'play', 'progress', 'current-time', 'volume', 'mute', 'fullscreen'],
|
// Only show controls for video and audio,
|
||||||
disableContextMenu: false,
|
// not looping soundless gifv. Don't show
|
||||||
hideControls: false,
|
// 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 },
|
tooltips: { controls: true, seek: true },
|
||||||
iconUrl: "/assets/plyr.svg",
|
iconUrl: "/assets/plyr.svg",
|
||||||
invertTime: false,
|
invertTime: false,
|
||||||
|
hideControls: false,
|
||||||
listeners: {
|
listeners: {
|
||||||
fullscreen: () => {
|
play: (_) => {
|
||||||
// Check if the photoswipe lightbox is
|
if (!inLightbox(video)) {
|
||||||
// open with this as the current slide.
|
// If the video isn't open in the lightbox
|
||||||
const alreadyInLightbox = (
|
// as the current photoswipe slide, clicking
|
||||||
lightbox.pswp !== undefined &&
|
// on it to play it opens it in the lightbox.
|
||||||
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);
|
|
||||||
}
|
|
||||||
lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
|
lightbox.loadAndOpen(parseInt(video.dataset.pswpIndex), {
|
||||||
gallery: video.closest(".photoswipe-gallery")
|
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;
|
return false;
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -225,6 +242,21 @@ Array.from(document.getElementsByClassName("plyr-video")).forEach((video) => {
|
|||||||
video._plyrContainer = player.elements.container;
|
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 => {
|
Array.from(document.getElementsByTagName('time')).forEach(timeTag => {
|
||||||
const datetime = timeTag.getAttribute('datetime');
|
const datetime = timeTag.getAttribute('datetime');
|
||||||
const currentText = timeTag.textContent.trim();
|
const currentText = timeTag.textContent.trim();
|
||||||
|
@@ -64,6 +64,15 @@ skulk({
|
|||||||
}]
|
}]
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
blurhash: {
|
||||||
|
entryFile: "blurhash",
|
||||||
|
outputFile: "blurhash.js",
|
||||||
|
preset: ["js"],
|
||||||
|
prodCfg: prodCfg,
|
||||||
|
transform: [
|
||||||
|
["babelify", { global: true }]
|
||||||
|
],
|
||||||
|
},
|
||||||
settings: {
|
settings: {
|
||||||
entryFile: "settings",
|
entryFile: "settings",
|
||||||
outputFile: "settings.js",
|
outputFile: "settings.js",
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@reduxjs/toolkit": "^1.8.6",
|
"@reduxjs/toolkit": "^1.8.6",
|
||||||
"ariakit": "^2.0.0-next.41",
|
"ariakit": "^2.0.0-next.41",
|
||||||
|
"blurhash": "^2.0.5",
|
||||||
"get-by-dot": "^1.0.2",
|
"get-by-dot": "^1.0.2",
|
||||||
"html-to-text": "^9.0.5",
|
"html-to-text": "^9.0.5",
|
||||||
"is-valid-domain": "^0.1.6",
|
"is-valid-domain": "^0.1.6",
|
||||||
|
@@ -2398,6 +2398,11 @@ bluebird@^3.7.1, bluebird@^3.7.2:
|
|||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
|
||||||
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
|
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:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
|
||||||
version "4.12.0"
|
version "4.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||||
|
@@ -67,7 +67,9 @@ image/webp
|
|||||||
<link rel="apple-touch-startup-image" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
|
<link rel="apple-touch-startup-image" href="{{- .instance.Thumbnail -}}" type="{{- template "thumbnailType" . -}}">
|
||||||
{{- include "page_stylesheets.tmpl" . | indent 2 }}
|
{{- include "page_stylesheets.tmpl" . | indent 2 }}
|
||||||
{{- range .javascript }}
|
{{- 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 }}
|
{{- end }}
|
||||||
<title>{{- template "instanceTitle" . -}}</title>
|
<title>{{- template "instanceTitle" . -}}</title>
|
||||||
</head>
|
</head>
|
||||||
@@ -82,5 +84,10 @@ image/webp
|
|||||||
<footer class="page-footer">
|
<footer class="page-footer">
|
||||||
{{- include "page_footer.tmpl" . | indent 3 }}
|
{{- include "page_footer.tmpl" . | indent 3 }}
|
||||||
</footer>
|
</footer>
|
||||||
|
{{- range .javascript }}
|
||||||
|
{{- if .Bottom }}
|
||||||
|
<script type="text/javascript" src="{{- .Src -}}"></script>
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@@ -17,33 +17,7 @@
|
|||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/ -}}
|
*/ -}}
|
||||||
|
|
||||||
{{- define "imagePreview" }}
|
{{- define "preview" }}
|
||||||
<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" }}
|
|
||||||
{{- if and .PreviewURL .Meta.Small.Width }}
|
{{- if and .PreviewURL .Meta.Small.Width }}
|
||||||
<img
|
<img
|
||||||
src="{{- .PreviewURL -}}"
|
src="{{- .PreviewURL -}}"
|
||||||
@@ -54,6 +28,8 @@
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
width="{{- .Meta.Small.Width -}}"
|
width="{{- .Meta.Small.Width -}}"
|
||||||
height="{{- .Meta.Small.Height -}}"
|
height="{{- .Meta.Small.Height -}}"
|
||||||
|
data-blurhash-hash="{{- .Blurhash -}}"
|
||||||
|
data-sensitive="{{- .Sensitive -}}"
|
||||||
/>
|
/>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
<img
|
<img
|
||||||
@@ -72,29 +48,38 @@
|
|||||||
{{- with . }}
|
{{- with . }}
|
||||||
<div class="media-wrapper">
|
<div class="media-wrapper">
|
||||||
<details class="{{- .Item.Type -}}-spoiler media-spoiler" {{- if not .Item.Sensitive }} open{{- end -}}>
|
<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>
|
<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="hide fa fa-fw fa-eye-slash" aria-hidden="true"></i>
|
||||||
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
|
<i class="show fa fa-fw fa-eye" aria-hidden="true"></i>
|
||||||
</span>
|
</span>
|
||||||
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
|
{{- if and (not (eq .Item.Type "unknown")) .Item.Meta.Small.Width }}
|
||||||
{{- include "videoPreview" .Item | indent 3 }}
|
<div
|
||||||
{{- else if eq .Item.Type "image" }}
|
class="blurhash-container"
|
||||||
{{- include "imagePreview" .Item | indent 3 }}
|
data-blurhash-width="{{- .Item.Meta.Small.Width -}}"
|
||||||
{{- else if eq .Item.Type "audio" }}
|
data-blurhash-height="{{- .Item.Meta.Small.Height -}}"
|
||||||
{{- include "audioPreview" .Item | indent 3 }}
|
data-blurhash-hash="{{- .Item.Blurhash -}}"
|
||||||
|
data-blurhash-aspect="{{- .Item.Meta.Small.Aspect -}}"
|
||||||
|
></div>
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</summary>
|
</summary>
|
||||||
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
|
{{- if or (eq .Item.Type "video") (eq .Item.Type "gifv") }}
|
||||||
<video
|
<video
|
||||||
{{- if eq .Item.Type "video" }}
|
{{- if eq .Item.Type "video" }}
|
||||||
preload="none"
|
preload="none"
|
||||||
|
class="plyr-video photoswipe-slide"
|
||||||
{{- else }}
|
{{- else }}
|
||||||
preload="auto"
|
preload="auto"
|
||||||
muted
|
muted
|
||||||
|
autoplay
|
||||||
|
loop
|
||||||
|
class="plyr-video photoswipe-slide gifv"
|
||||||
{{- end }}
|
{{- end }}
|
||||||
class="plyr-video photoswipe-slide{{- if eq .Item.Type "gifv" }} gifv{{ end }}"
|
|
||||||
controls
|
controls
|
||||||
playsinline
|
playsinline
|
||||||
data-pswp-index="{{- .Index -}}"
|
data-pswp-index="{{- .Index -}}"
|
||||||
@@ -125,8 +110,8 @@
|
|||||||
data-pswp-height="{{- .Item.Meta.Small.Height -}}px"
|
data-pswp-height="{{- .Item.Meta.Small.Height -}}px"
|
||||||
{{- else }}
|
{{- else }}
|
||||||
poster="/assets/logo.webp"
|
poster="/assets/logo.webp"
|
||||||
width="518px"
|
data-pswp-width="518px"
|
||||||
height="460px"
|
data-pswp-height="460px"
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Item.Description }}
|
{{- if .Item.Description }}
|
||||||
alt="{{- .Item.Description -}}"
|
alt="{{- .Item.Description -}}"
|
||||||
@@ -152,7 +137,7 @@
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
>
|
>
|
||||||
{{- with .Item }}
|
{{- with .Item }}
|
||||||
{{- include "imagePreview" . | indent 3 }}
|
{{- include "preview" . | indent 3 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
</a>
|
</a>
|
||||||
{{- else }}
|
{{- else }}
|
||||||
|
Reference in New Issue
Block a user