diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 5aeb56e96..31e3e79be 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -16,6 +16,7 @@ import "./live"; import "./PlaylistButtons"; import "./source-selector"; import "./persist-volume"; +import "./autostart-button"; import MarkersPlugin, { type IMarker } from "./markers"; void MarkersPlugin; import "./vtt-thumbnails"; @@ -28,6 +29,7 @@ import cx from "classnames"; import { useSceneSaveActivity, useSceneIncrementPlayCount, + useConfigureInterface, } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; @@ -249,6 +251,7 @@ export const ScenePlayer: React.FC = PatchComponent( const sceneId = useRef(); const [sceneSaveActivity] = useSceneSaveActivity(); const [sceneIncrementPlayCount] = useSceneIncrementPlayCount(); + const [updateInterfaceConfig] = useConfigureInterface(); const [time, setTime] = useState(0); const [ready, setReady] = useState(false); @@ -389,6 +392,9 @@ export const ScenePlayer: React.FC = PatchComponent( skipButtons: {}, trackActivity: {}, vrMenu: {}, + autostartButton: { + enabled: interfaceConfig?.autostartVideo ?? false, + }, abLoopPlugin: { start: 0, end: false, @@ -434,6 +440,9 @@ export const ScenePlayer: React.FC = PatchComponent( }; // empty deps - only init once // showAbLoopControls is necessary to re-init the player when the config changes + // Note: interfaceConfig?.autostartVideo is intentionally excluded to prevent + // player re-initialization when toggling autostart (which would interrupt playback) + // eslint-disable-next-line react-hooks/exhaustive-deps }, [uiConfig?.showAbLoopControls, uiConfig?.enableChromecast]); useEffect(() => { @@ -675,11 +684,6 @@ export const ScenePlayer: React.FC = PatchComponent( } } - auto.current = - autoplay || - (interfaceConfig?.autostartVideo ?? false) || - _initialTimestamp > 0; - const alwaysStartFromBeginning = uiConfig?.alwaysStartFromBeginning ?? false; const resumeTime = scene.resume_time ?? 0; @@ -698,6 +702,15 @@ export const ScenePlayer: React.FC = PatchComponent( player.load(); player.focus(); + // Check the autostart button plugin for user preference + const autostartButton = player.autostartButton(); + const buttonEnabled = autostartButton.getEnabled(); + auto.current = + autoplay || + buttonEnabled || + (interfaceConfig?.autostartVideo ?? false) || + _initialTimestamp > 0; + player.ready(() => { player.vttThumbnails().src(scene.paths.vtt ?? null); @@ -841,6 +854,30 @@ export const ScenePlayer: React.FC = PatchComponent( sceneSaveActivity, ]); + // Sync autostart button with config changes + useEffect(() => { + const player = getPlayer(); + if (!player) return; + + async function updateAutoStart(enabled: boolean) { + await updateInterfaceConfig({ + variables: { + input: { + autostartVideo: enabled, + }, + }, + }); + } + + const autostartButton = player.autostartButton(); + if (autostartButton) { + autostartButton.syncWithConfig( + interfaceConfig?.autostartVideo ?? false + ); + autostartButton.updateAutoStart = updateAutoStart; + } + }, [getPlayer, updateInterfaceConfig, interfaceConfig?.autostartVideo]); + useEffect(() => { const player = getPlayer(); if (!player) return; diff --git a/ui/v2.5/src/components/ScenePlayer/autostart-button.ts b/ui/v2.5/src/components/ScenePlayer/autostart-button.ts new file mode 100644 index 000000000..f5a35a63f --- /dev/null +++ b/ui/v2.5/src/components/ScenePlayer/autostart-button.ts @@ -0,0 +1,126 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import videojs, { VideoJsPlayer } from "video.js"; + +interface IAutostartButtonOptions { + enabled?: boolean; +} + +interface AutostartButtonOptions extends videojs.ComponentOptions { + autostartEnabled: boolean; +} + +class AutostartButton extends videojs.getComponent("Button") { + private autostartEnabled: boolean; + + constructor(player: VideoJsPlayer, options: AutostartButtonOptions) { + super(player, options); + this.autostartEnabled = options.autostartEnabled; + this.updateIcon(); + } + + buildCSSClass() { + return `vjs-autostart-button ${super.buildCSSClass()}`; + } + + private updateIcon() { + this.removeClass("vjs-icon-play-circle"); + this.removeClass("vjs-icon-cancel"); + + if (this.autostartEnabled) { + this.addClass("vjs-icon-play-circle"); + this.controlText(this.localize("Auto-start enabled (click to disable)")); + } else { + this.addClass("vjs-icon-cancel"); + this.controlText(this.localize("Auto-start disabled (click to enable)")); + } + } + + handleClick(event: Event) { + // Prevent the click from bubbling up and affecting the video player + event.stopPropagation(); + + this.autostartEnabled = !this.autostartEnabled; + this.updateIcon(); + this.trigger("autostartchanged", { enabled: this.autostartEnabled }); + } + + public setEnabled(enabled: boolean) { + this.autostartEnabled = enabled; + this.updateIcon(); + } +} + +class AutostartButtonPlugin extends videojs.getPlugin("plugin") { + private button: AutostartButton; + private autostartEnabled: boolean; + updateAutoStart: (enabled: boolean) => Promise = () => { + return Promise.resolve(); + }; + + constructor(player: VideoJsPlayer, options?: IAutostartButtonOptions) { + super(player, options); + + this.autostartEnabled = options?.enabled ?? false; + + this.button = new AutostartButton(player, { + autostartEnabled: this.autostartEnabled, + }); + + player.ready(() => { + this.ready(); + }); + } + + private ready() { + // Add button to control bar, before the fullscreen button + const { controlBar } = this.player; + const fullscreenToggle = controlBar.getChild("fullscreenToggle"); + if (fullscreenToggle) { + controlBar.addChild(this.button); + controlBar.el().insertBefore(this.button.el(), fullscreenToggle.el()); + } else { + controlBar.addChild(this.button); + } + + // Listen for changes + this.button.on("autostartchanged", (_, data: { enabled: boolean }) => { + this.autostartEnabled = data.enabled; + this.updateAutoStart(this.autostartEnabled); + }); + } + + public isEnabled(): boolean { + return this.autostartEnabled; + } + + public getEnabled(): boolean { + return this.autostartEnabled; + } + + public setEnabled(enabled: boolean) { + this.autostartEnabled = enabled; + this.button.setEnabled(enabled); + } + + public syncWithConfig(configEnabled: boolean) { + // Sync button state with external config changes + if (this.autostartEnabled !== configEnabled) { + this.setEnabled(configEnabled); + } + } +} + +// Register the plugin with video.js. +videojs.registerComponent("AutostartButton", AutostartButton); +videojs.registerPlugin("autostartButton", AutostartButtonPlugin); + +declare module "video.js" { + interface VideoJsPlayer { + autostartButton: () => AutostartButtonPlugin; + } + interface VideoJsPlayerPluginOptions { + autostartButton?: IAutostartButtonOptions; + } +} + +export default AutostartButtonPlugin; diff --git a/ui/v2.5/src/components/ScenePlayer/styles.scss b/ui/v2.5/src/components/ScenePlayer/styles.scss index 0e8041071..fc143a873 100644 --- a/ui/v2.5/src/components/ScenePlayer/styles.scss +++ b/ui/v2.5/src/components/ScenePlayer/styles.scss @@ -100,6 +100,57 @@ $sceneTabWidth: 450px; width: 1.6em; } + .vjs-autostart-button { + cursor: pointer; + + &.vjs-icon-play-circle::before { + align-items: center; + background-color: rgba(255, 255, 255, 0.9); + border-radius: 50%; + color: rgba(80, 80, 80, 0.9); + content: "\f101"; + font-size: 1em; + line-height: 1; + margin-left: 1rem; + padding: 0.3em; + position: relative; + z-index: 2; + } + + &.vjs-icon-cancel::before { + align-items: center; + background-color: rgba(80, 80, 80, 0.9); + border-radius: 50%; + color: #fff; + content: "\f103"; + font-size: 1em; + line-height: 1; + margin-right: 1rem; + padding: 0.3em; + position: relative; + z-index: 2; + } + + &.vjs-icon-play-circle::after, + &.vjs-icon-cancel::after { + background-color: rgb(255 255 255 / 70%); + border-radius: 8px; + content: ""; + height: 2.5rem; + left: 50%; + opacity: 0.7; + position: absolute; + top: 50%; + transform: translate(-50%, -50%) rotate(90deg); + width: 1rem; + z-index: 1; + } + + &:hover { + text-shadow: 0 0 1em rgba(255, 255, 255, 0.75); + } + } + .vjs-touch-overlay .vjs-play-control { z-index: 1; } @@ -344,9 +395,16 @@ $sceneTabWidth: 450px; } } } + @media (max-width: 576px) { + .vjs-control-bar { + .vjs-autostart-button { + display: none; + } + } + } // make controls a little more compact on smaller screens - @media (max-width: 576px) { + @media (max-width: 768px) { .vjs-control-bar { .vjs-control { width: 2.5em;