diff options
| -rw-r--r-- | internal/api/util/template.go | 21 | ||||
| -rw-r--r-- | internal/processing/account/statuses.go | 3 | ||||
| -rw-r--r-- | internal/web/domain-blocklist.go | 1 | ||||
| -rw-r--r-- | internal/web/profile.go | 35 | ||||
| -rw-r--r-- | internal/web/settings-panel.go | 8 | ||||
| -rw-r--r-- | internal/web/thread.go | 12 | ||||
| -rw-r--r-- | internal/web/web.go | 1 | ||||
| -rw-r--r-- | web/source/blurhash/index.js | 146 | ||||
| -rw-r--r-- | web/source/css/_media-wrapper.css | 176 | ||||
| -rw-r--r-- | web/source/css/_status-media.css | 7 | ||||
| -rw-r--r-- | web/source/css/profile-gallery.css | 19 | ||||
| -rw-r--r-- | web/source/frontend/index.js | 116 | ||||
| -rw-r--r-- | web/source/index.js | 9 | ||||
| -rw-r--r-- | web/source/package.json | 1 | ||||
| -rw-r--r-- | web/source/yarn.lock | 5 | ||||
| -rw-r--r-- | web/template/page.tmpl | 9 | ||||
| -rw-r--r-- | web/template/status_attachment.tmpl | 63 |
17 files changed, 508 insertions, 124 deletions
diff --git a/internal/api/util/template.go b/internal/api/util/template.go index ec04a4d97..38e79484f 100644 --- a/internal/api/util/template.go +++ b/internal/api/util/template.go @@ -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. // diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go index 701fe44ae..3b4e067f9 100644 --- a/internal/processing/account/statuses.go +++ b/internal/processing/account/statuses.go @@ -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) { diff --git a/internal/web/domain-blocklist.go b/internal/web/domain-blocklist.go index 5d631e0f7..309e629f2 100644 --- a/internal/web/domain-blocklist.go +++ b/internal/web/domain-blocklist.go @@ -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}, } diff --git a/internal/web/profile.go b/internal/web/profile.go index 52d918b48..e8483921d 100644 --- a/internal/web/profile.go +++ b/internal/web/profile.go @@ -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, diff --git a/internal/web/settings-panel.go b/internal/web/settings-panel.go index ec8166e95..9b27fe871 100644 --- a/internal/web/settings-panel.go +++ b/internal/web/settings-panel.go @@ -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) diff --git a/internal/web/thread.go b/internal/web/thread.go index 60f7ac4d2..0b296a75b 100644 --- a/internal/web/thread.go +++ b/internal/web/thread.go @@ -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, }, diff --git a/internal/web/web.go b/internal/web/web.go index dbfc2a3b5..15814942a 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -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. ) diff --git a/web/source/blurhash/index.js b/web/source/blurhash/index.js new file mode 100644 index 000000000..c964f69c4 --- /dev/null +++ b/web/source/blurhash/index.js @@ -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(); + } + }); +}); diff --git a/web/source/css/_media-wrapper.css b/web/source/css/_media-wrapper.css index 1c2ae1503..a567cb0fd 100644 --- a/web/source/css/_media-wrapper.css +++ b/web/source/css/_media-wrapper.css @@ -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; } } diff --git a/web/source/css/_status-media.css b/web/source/css/_status-media.css index e8386c87a..761b969be 100644 --- a/web/source/css/_status-media.css +++ b/web/source/css/_status-media.css @@ -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; diff --git a/web/source/css/profile-gallery.css b/web/source/css/profile-gallery.css index cb70eff22..ef18ed7b0 100644 --- a/web/source/css/profile-gallery.css +++ b/web/source/css/profile-gallery.css @@ -72,27 +72,16 @@ margin-top: 0.15rem; margin-bottom: 0.15rem; + /* Show 3 cols of media */ display: grid; - 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); - } + gap: 0.15rem; .media-wrapper { - aspect-ratio: 4/3; + aspect-ratio: 1; border: 0; border-radius: 0; - background: $bg; + background: $status-bg; } } diff --git a/web/source/frontend/index.js b/web/source/frontend/index.js index d45420255..5a6224994 100644 --- a/web/source/frontend/index.js +++ b/web/source/frontend/index.js @@ -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(); diff --git a/web/source/index.js b/web/source/index.js index 5cee28046..c47e9c5bb 100644 --- a/web/source/index.js +++ b/web/source/index.js @@ -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", diff --git a/web/source/package.json b/web/source/package.json index 7b11d7eb2..6a29eeeac 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -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", diff --git a/web/source/yarn.lock b/web/source/yarn.lock index f3f3a5752..8354d00c1 100644 --- a/web/source/yarn.lock +++ b/web/source/yarn.lock @@ -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" diff --git a/web/template/page.tmpl b/web/template/page.tmpl index 7dccc7469..0a54e74cb 100644 --- a/web/template/page.tmpl +++ b/web/template/page.tmpl @@ -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>
\ No newline at end of file diff --git a/web/template/status_attachment.tmpl b/web/template/status_attachment.tmpl index bdfafa96f..4dda7298f 100644 --- a/web/template/status_attachment.tmpl +++ b/web/template/status_attachment.tmpl @@ -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 }} |
