diff --git a/package.json b/package.json index ef37748d..cefce9e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "impro", - "version": "0.20.71", + "version": "0.20.72", "type": "module", "scripts": { "start": "rm -rf \"${BUILD_DIR:-build}\" && NODE_ENV=development eleventy --serve", diff --git a/src/css/style.css b/src/css/style.css index 9b7c4076..a93baacc 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -2914,6 +2914,31 @@ animated-button { display: none; } +.gif-play-toggle { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + border: none; + background: none; + cursor: pointer; +} + +.gif-play-toggle app-icon { + display: none; + width: 56px; + height: 56px; + color: var(--black); + background: color-mix(in srgb, var(--white) 70%, transparent); + border-radius: 50%; +} + +streaming-video.is-paused .gif-play-toggle app-icon { + display: inline-flex; +} + .post-video img { position: absolute; inset: 0; diff --git a/src/js/app.js b/src/js/app.js index 58c6dca8..b4b2c205 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -22,6 +22,7 @@ import pageErrorView from "/js/views/pageError.view.js"; import accountNotFoundView from "/js/views/accountNotFound.view.js"; import settingsView from "/js/views/settings.view.js"; import settingsAppearanceView from "/js/views/settings/appearance.view.js"; +import settingsContentAndMediaView from "/js/views/settings/contentAndMedia.view.js"; import settingsMutedWordsView from "/js/views/settings/mutedWords.view.js"; import settingsBlockedAccountsView from "/js/views/settings/blockedAccounts.view.js"; import settingsMutedAccountsView from "/js/views/settings/mutedAccounts.view.js"; @@ -361,6 +362,11 @@ export async function main() { () => settingsAppearanceView, settingsRouteOptions, ); + router.addRoute( + "/settings/content-and-media", + () => settingsContentAndMediaView, + settingsRouteOptions, + ); router.addRoute( "/settings/notifications", () => settingsNotificationsView, diff --git a/src/js/components/streaming-video.js b/src/js/components/streaming-video.js index 8c8779f7..96d08340 100644 --- a/src/js/components/streaming-video.js +++ b/src/js/components/streaming-video.js @@ -1,6 +1,9 @@ import { html, render } from "/js/lib/lit-html.js"; import { Component } from "/js/components/component.js"; import { isTouchOnlyDevice } from "/js/utils.js"; +import { effect, untrack } from "/js/signals.js"; +import { deviceState } from "/js/deviceState.js"; +import "/js/components/app-icon.js"; // Only start loading the video when it's close to visible in the viewport. // Also fires when a hidden page is shown again, resuming autoplay videos @@ -105,6 +108,21 @@ export function updateActiveVideo() { setActiveVideo(pickActiveVideo()); } +const connectedPlayers = new Set(); + +effect(() => { + deviceState.$autoplayDisabled.get(); + untrack(() => { + for (const player of connectedPlayers) { + player.render(); + } + }); +}); + +function isAutoplayDisabled() { + return untrack(() => deviceState.$autoplayDisabled.get()); +} + function formatRemainingTime(seconds) { const minutes = Math.floor(seconds / 60); return `${minutes}:${String(seconds % 60).padStart(2, "0")}`; @@ -139,18 +157,48 @@ class StreamingVideo extends Component { }; handlePlay = () => { + this._pausedByUser = false; + this._updatePausedIndicator(); if (this._isActiveCandidate && activeVideo !== this) { setActiveVideoManually(this); } }; + handlePause = () => { + this._updatePausedIndicator(); + }; + + handleGifToggleClick = (event) => { + event.preventDefault(); + event.stopPropagation(); + const video = this.querySelector("video"); + if (video.paused) { + this.enableStreaming().then(() => video.play().catch(() => {})); + } else { + this._pausedByUser = true; + video.pause(); + } + }; + + // Players without controls show a play button while paused, unless + // they're only paused until autoplay resumes them + _updatePausedIndicator() { + const video = this.querySelector("video"); + this.classList.toggle( + "is-paused", + !this.controls && + video.paused && + (this._pausedByUser || isAutoplayDisabled()), + ); + } + get _isActiveCandidate() { return this.controls && this.autoplay; } activate() { const video = this.querySelector("video"); - if (!video || !video.paused) { + if (!video || !video.paused || isAutoplayDisabled()) { return; } this.enableStreaming().then(() => { @@ -173,6 +221,7 @@ class StreamingVideo extends Component { // We always want to observe / unobserve the video to ensure it's streaming when it should be streamingVideoObserver.observe(this); window.addEventListener("page-transition", this.handlePageTransition); + connectedPlayers.add(this); if (!this.initialized) { this._initialize(); } @@ -193,6 +242,7 @@ class StreamingVideo extends Component { this.loop = this.getAttribute("loop") !== null; this.playsinline = this.getAttribute("playsinline") !== null; this._streamingEnabled = false; + this._pausedByUser = false; this.render(); this.initialized = true; } @@ -200,6 +250,7 @@ class StreamingVideo extends Component { disconnectedCallback() { streamingVideoObserver.unobserve(this); window.removeEventListener("page-transition", this.handlePageTransition); + connectedPlayers.delete(this); if (this._isActiveCandidate) { activeVideoObserver.unobserve(this); allElements.delete(this); @@ -217,16 +268,19 @@ class StreamingVideo extends Component { } render() { + const autoplayDisabled = isAutoplayDisabled(); render( html`