[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. // 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.
// //

View File

@@ -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) {

View File

@@ -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},
} }

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). // 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,

View File

@@ -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)

View File

@@ -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,
}, },

View File

@@ -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.
) )

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%; 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;
} }
} }

View File

@@ -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;

View File

@@ -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;
} }
} }

View File

@@ -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();

View File

@@ -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",

View File

@@ -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",

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" 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"

View File

@@ -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>

View File

@@ -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 }}