Add display autoplay and playlist continue options (#1921)

* Add display autoplay and playlist continue options
* Include continue parameter in scene links
This commit is contained in:
WithoutPants 2021-11-07 09:55:51 +11:00 committed by GitHub
parent 25274e2596
commit 3aa5f657bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 202 additions and 35 deletions

View file

@ -54,6 +54,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
maximumLoopDuration maximumLoopDuration
noBrowser noBrowser
autostartVideo autostartVideo
autostartVideoOnPlaySelected
continuePlaylistDefault
showStudioAsText showStudioAsText
css css
cssEnabled cssEnabled

View file

@ -197,27 +197,40 @@ input ConfigDisableDropdownCreateInput {
input ConfigInterfaceInput { input ConfigInterfaceInput {
"""Ordered list of items that should be shown in the menu""" """Ordered list of items that should be shown in the menu"""
menuItems: [String!] menuItems: [String!]
"""Enable sound on mouseover previews""" """Enable sound on mouseover previews"""
soundOnPreview: Boolean soundOnPreview: Boolean
"""Show title and tags in wall view""" """Show title and tags in wall view"""
wallShowTitle: Boolean wallShowTitle: Boolean
"""Wall playback type""" """Wall playback type"""
wallPlayback: String wallPlayback: String
"""Maximum duration (in seconds) in which a scene video will loop in the scene player""" """Maximum duration (in seconds) in which a scene video will loop in the scene player"""
maximumLoopDuration: Int maximumLoopDuration: Int
"""If true, video will autostart on load in the scene player""" """If true, video will autostart on load in the scene player"""
autostartVideo: Boolean autostartVideo: Boolean
"""If true, video will autostart when loading from play random or play selected"""
autostartVideoOnPlaySelected: Boolean
"""If true, next scene in playlist will be played at video end by default"""
continuePlaylistDefault: Boolean
"""If true, studio overlays will be shown as text instead of logo images""" """If true, studio overlays will be shown as text instead of logo images"""
showStudioAsText: Boolean showStudioAsText: Boolean
"""Custom CSS""" """Custom CSS"""
css: String css: String
cssEnabled: Boolean cssEnabled: Boolean
"""Interface language""" """Interface language"""
language: String language: String
"""Slideshow Delay""" """Slideshow Delay"""
slideshowDelay: Int slideshowDelay: Int
"""Set to true to disable creating new objects via the dropdown menus""" """Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput disableDropdownCreate: ConfigDisableDropdownCreateInput
"""Handy Connection Key""" """Handy Connection Key"""
handyKey: String handyKey: String
"""Funscript Time Offset""" """Funscript Time Offset"""
@ -235,29 +248,42 @@ type ConfigDisableDropdownCreate {
type ConfigInterfaceResult { type ConfigInterfaceResult {
"""Ordered list of items that should be shown in the menu""" """Ordered list of items that should be shown in the menu"""
menuItems: [String!] menuItems: [String!]
"""Enable sound on mouseover previews""" """Enable sound on mouseover previews"""
soundOnPreview: Boolean soundOnPreview: Boolean
"""Show title and tags in wall view""" """Show title and tags in wall view"""
wallShowTitle: Boolean wallShowTitle: Boolean
"""Wall playback type""" """Wall playback type"""
wallPlayback: String wallPlayback: String
"""Maximum duration (in seconds) in which a scene video will loop in the scene player""" """Maximum duration (in seconds) in which a scene video will loop in the scene player"""
maximumLoopDuration: Int maximumLoopDuration: Int
""""True if we should not auto-open a browser window on startup""" """"True if we should not auto-open a browser window on startup"""
noBrowser: Boolean noBrowser: Boolean
"""If true, video will autostart on load in the scene player""" """If true, video will autostart on load in the scene player"""
autostartVideo: Boolean autostartVideo: Boolean
"""If true, video will autostart when loading from play random or play selected"""
autostartVideoOnPlaySelected: Boolean
"""If true, next scene in playlist will be played at video end by default"""
continuePlaylistDefault: Boolean
"""If true, studio overlays will be shown as text instead of logo images""" """If true, studio overlays will be shown as text instead of logo images"""
showStudioAsText: Boolean showStudioAsText: Boolean
"""Custom CSS""" """Custom CSS"""
css: String css: String
cssEnabled: Boolean cssEnabled: Boolean
"""Interface language""" """Interface language"""
language: String language: String
"""Slideshow Delay""" """Slideshow Delay"""
slideshowDelay: Int slideshowDelay: Int
"""Fields are true if creating via dropdown menus are disabled""" """Fields are true if creating via dropdown menus are disabled"""
disabledDropdownCreate: ConfigDisableDropdownCreate! disabledDropdownCreate: ConfigDisableDropdownCreate!
"""Handy Connection Key""" """Handy Connection Key"""
handyKey: String handyKey: String
"""Funscript Time Offset""" """Funscript Time Offset"""

View file

@ -251,6 +251,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
setBool(config.AutostartVideo, input.AutostartVideo) setBool(config.AutostartVideo, input.AutostartVideo)
setBool(config.ShowStudioAsText, input.ShowStudioAsText) setBool(config.ShowStudioAsText, input.ShowStudioAsText)
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
if input.Language != nil { if input.Language != nil {
c.Set(config.Language, *input.Language) c.Set(config.Language, *input.Language)

View file

@ -111,6 +111,8 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
noBrowser := config.GetNoBrowserFlag() noBrowser := config.GetNoBrowserFlag()
maximumLoopDuration := config.GetMaximumLoopDuration() maximumLoopDuration := config.GetMaximumLoopDuration()
autostartVideo := config.GetAutostartVideo() autostartVideo := config.GetAutostartVideo()
autostartVideoOnPlaySelected := config.GetAutostartVideoOnPlaySelected()
continuePlaylistDefault := config.GetContinuePlaylistDefault()
showStudioAsText := config.GetShowStudioAsText() showStudioAsText := config.GetShowStudioAsText()
css := config.GetCSS() css := config.GetCSS()
cssEnabled := config.GetCSSEnabled() cssEnabled := config.GetCSSEnabled()
@ -120,21 +122,23 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
scriptOffset := config.GetFunscriptOffset() scriptOffset := config.GetFunscriptOffset()
return &models.ConfigInterfaceResult{ return &models.ConfigInterfaceResult{
MenuItems: menuItems, MenuItems: menuItems,
SoundOnPreview: &soundOnPreview, SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle, WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback, WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration, MaximumLoopDuration: &maximumLoopDuration,
NoBrowser: &noBrowser, NoBrowser: &noBrowser,
AutostartVideo: &autostartVideo, AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText, ShowStudioAsText: &showStudioAsText,
CSS: &css, AutostartVideoOnPlaySelected: &autostartVideoOnPlaySelected,
CSSEnabled: &cssEnabled, ContinuePlaylistDefault: &continuePlaylistDefault,
Language: &language, CSS: &css,
SlideshowDelay: &slideshowDelay, CSSEnabled: &cssEnabled,
DisabledDropdownCreate: config.GetDisableDropdownCreate(), Language: &language,
HandyKey: &handyKey, SlideshowDelay: &slideshowDelay,
FunscriptOffset: &scriptOffset, DisabledDropdownCreate: config.GetDisableDropdownCreate(),
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
} }
} }

View file

@ -130,6 +130,8 @@ const WallShowTitle = "wall_show_title"
const CustomPerformerImageLocation = "custom_performer_image_location" const CustomPerformerImageLocation = "custom_performer_image_location"
const MaximumLoopDuration = "maximum_loop_duration" const MaximumLoopDuration = "maximum_loop_duration"
const AutostartVideo = "autostart_video" const AutostartVideo = "autostart_video"
const AutostartVideoOnPlaySelected = "autostart_video_on_play_selected"
const ContinuePlaylistDefault = "continue_playlist_default"
const ShowStudioAsText = "show_studio_as_text" const ShowStudioAsText = "show_studio_as_text"
const CSSEnabled = "cssEnabled" const CSSEnabled = "cssEnabled"
const WallPlayback = "wall_playback" const WallPlayback = "wall_playback"
@ -808,6 +810,20 @@ func (i *Instance) GetAutostartVideo() bool {
return viper.GetBool(AutostartVideo) return viper.GetBool(AutostartVideo)
} }
func (i *Instance) GetAutostartVideoOnPlaySelected() bool {
i.Lock()
defer i.Unlock()
viper.SetDefault(AutostartVideoOnPlaySelected, true)
return viper.GetBool(AutostartVideoOnPlaySelected)
}
func (i *Instance) GetContinuePlaylistDefault() bool {
i.Lock()
defer i.Unlock()
viper.SetDefault(ContinuePlaylistDefault, false)
return viper.GetBool(ContinuePlaylistDefault)
}
func (i *Instance) GetShowStudioAsText() bool { func (i *Instance) GetShowStudioAsText() bool {
i.Lock() i.Lock()
defer i.Unlock() defer i.Unlock()

View file

@ -94,6 +94,8 @@ func TestConcurrentConfigAccess(t *testing.T) {
i.Set(MaxUploadSize, i.GetMaxUploadSize()) i.Set(MaxUploadSize, i.GetMaxUploadSize())
i.Set(FunscriptOffset, i.GetFunscriptOffset()) i.Set(FunscriptOffset, i.GetFunscriptOffset())
i.Set(DefaultIdentifySettings, i.GetDefaultIdentifySettings()) i.Set(DefaultIdentifySettings, i.GetDefaultIdentifySettings())
i.Set(AutostartVideoOnPlaySelected, i.GetAutostartVideoOnPlaySelected())
i.Set(ContinuePlaylistDefault, i.GetContinuePlaylistDefault())
} }
wg.Done() wg.Done()
}(k) }(k)

View file

@ -1,4 +1,5 @@
### ✨ New Features ### ✨ New Features
* Add options to auto-start videos when playing from selection and continue to scene playlists. ([#1921](https://github.com/stashapp/stash/pull/1921))
* Support is (not) null for multi-relational filter criteria. ([#1785](https://github.com/stashapp/stash/pull/1785)) * Support is (not) null for multi-relational filter criteria. ([#1785](https://github.com/stashapp/stash/pull/1785))
* Optionally open browser on startup (enabled by default for new systems). ([#1832](https://github.com/stashapp/stash/pull/1832)) * Optionally open browser on startup (enabled by default for new systems). ([#1832](https://github.com/stashapp/stash/pull/1832))
* Support setting defaults for Delete File and Delete Generated Files in the Interface Settings. ([#1852](https://github.com/stashapp/stash/pull/1852)) * Support setting defaults for Delete File and Delete Generated Files in the Interface Settings. ([#1852](https://github.com/stashapp/stash/pull/1852))

View file

@ -88,13 +88,8 @@ export const GalleryList: React.FC<IGalleryList> = ({
filterCopy.itemsPerPage = 1; filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await queryFindGalleries(filterCopy); const singleResult = await queryFindGalleries(filterCopy);
if ( if (singleResult.data.findGalleries.galleries.length === 1) {
singleResult && const { id } = singleResult.data.findGalleries.galleries[0];
singleResult.data &&
singleResult.data.findGalleries &&
singleResult.data.findGalleries.galleries.length === 1
) {
const { id } = singleResult!.data!.findGalleries!.galleries[0];
// navigate to the image player page // navigate to the image player page
history.push(`/galleries/${id}`); history.push(`/galleries/${id}`);
} }

View file

@ -305,8 +305,13 @@ export const SceneCard: React.FC<ISceneCardProps> = (
} }
} }
const cont = configuration?.interface.continuePlaylistDefault ?? false;
const sceneLink = props.queue const sceneLink = props.queue
? props.queue.makeLink(props.scene.id, { sceneIndex: props.index }) ? props.queue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
})
: `/scenes/${props.scene.id}`; : `/scenes/${props.scene.id}`;
return ( return (

View file

@ -3,14 +3,17 @@ import { Link } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { Button, Spinner } from "react-bootstrap"; import { Button, Form, Spinner } from "react-bootstrap";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
import { useIntl } from "react-intl";
export interface IPlaylistViewer { export interface IPlaylistViewer {
scenes?: GQL.SlimSceneDataFragment[]; scenes?: GQL.SlimSceneDataFragment[];
currentID?: string; currentID?: string;
start?: number; start?: number;
continue?: boolean;
hasMoreScenes: boolean; hasMoreScenes: boolean;
setContinue: (v: boolean) => void;
onSceneClicked: (id: string) => void; onSceneClicked: (id: string) => void;
onNext: () => void; onNext: () => void;
onPrevious: () => void; onPrevious: () => void;
@ -23,7 +26,9 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
scenes, scenes,
currentID, currentID,
start, start,
continue: continuePlaylist = false,
hasMoreScenes, hasMoreScenes,
setContinue,
onNext, onNext,
onPrevious, onPrevious,
onRandom, onRandom,
@ -31,6 +36,7 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
onMoreScenes, onMoreScenes,
onLessScenes, onLessScenes,
}) => { }) => {
const intl = useIntl();
const [lessLoading, setLessLoading] = useState(false); const [lessLoading, setLessLoading] = useState(false);
const [moreLoading, setMoreLoading] = useState(false); const [moreLoading, setMoreLoading] = useState(false);
@ -91,6 +97,17 @@ export const QueueViewer: React.FC<IPlaylistViewer> = ({
return ( return (
<div id="queue-viewer"> <div id="queue-viewer">
<div className="queue-controls"> <div className="queue-controls">
<div>
<Form.Check
checked={continuePlaylist}
label={intl.formatMessage({
id: "continue",
})}
onChange={() => {
setContinue(!continuePlaylist);
}}
/>
</div>
<div> <div>
{(currentIndex ?? 0) > 0 ? ( {(currentIndex ?? 0) > 0 ? (
<Button <Button

View file

@ -1,6 +1,6 @@
import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap"; import { Tab, Nav, Dropdown, Button, ButtonGroup } from "react-bootstrap";
import queryString from "query-string"; import queryString from "query-string";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useLocation, useHistory, Link } from "react-router-dom"; import { useParams, useLocation, useHistory, Link } from "react-router-dom";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
@ -79,10 +79,13 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
const [queueTotal, setQueueTotal] = useState(0); const [queueTotal, setQueueTotal] = useState(0);
const [queueStart, setQueueStart] = useState(1); const [queueStart, setQueueStart] = useState(1);
const [continuePlaylist, setContinuePlaylist] = useState(false);
const [rerenderPlayer, setRerenderPlayer] = useState(false); const [rerenderPlayer, setRerenderPlayer] = useState(false);
const queryParams = queryString.parse(location.search); const queryParams = useMemo(() => queryString.parse(location.search), [
location.search,
]);
const autoplay = queryParams?.autoplay === "true"; const autoplay = queryParams?.autoplay === "true";
const currentQueueIndex = queueScenes.findIndex((s) => s.id === scene.id); const currentQueueIndex = queueScenes.findIndex((s) => s.id === scene.id);
@ -102,6 +105,10 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
setQueueStart(1); setQueueStart(1);
} }
useEffect(() => {
setContinuePlaylist(queryParams?.continue === "true");
}, [queryParams]);
// HACK - jwplayer doesn't handle re-rendering when scene changes, so force // HACK - jwplayer doesn't handle re-rendering when scene changes, so force
// a rerender by not drawing it // a rerender by not drawing it
useEffect(() => { useEffect(() => {
@ -262,6 +269,7 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
sceneQueue.playScene(history, sceneID, { sceneQueue.playScene(history, sceneID, {
newPage: page, newPage: page,
autoPlay: true, autoPlay: true,
continue: continuePlaylist,
}); });
} }
@ -301,7 +309,7 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
function onComplete() { function onComplete() {
// load the next scene if we're autoplaying // load the next scene if we're autoplaying
if (autoplay) { if (continuePlaylist) {
onQueueNext(); onQueueNext();
} }
} }
@ -485,6 +493,8 @@ const ScenePage: React.FC<IProps> = ({ scene, refetch }) => {
<QueueViewer <QueueViewer
scenes={queueScenes} scenes={queueScenes}
currentID={scene.id} currentID={scene.id}
continue={continuePlaylist}
setContinue={(v) => setContinuePlaylist(v)}
onSceneClicked={(sceneID) => playScene(sceneID)} onSceneClicked={(sceneID) => playScene(sceneID)}
onNext={onQueueNext} onNext={onQueueNext}
onPrevious={onQueuePrevious} onPrevious={onQueuePrevious}

View file

@ -24,6 +24,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
import { SceneCardsGrid } from "./SceneCardsGrid"; import { SceneCardsGrid } from "./SceneCardsGrid";
import { TaggerContext } from "../Tagger/context"; import { TaggerContext } from "../Tagger/context";
import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog"; import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog";
import { ConfigurationContext } from "src/hooks/Config";
interface ISceneList { interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel; filterHook?: (filter: ListFilterModel) => ListFilterModel;
@ -38,6 +39,7 @@ export const SceneList: React.FC<ISceneList> = ({
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
const config = React.useContext(ConfigurationContext);
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false); const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const [isIdentifyDialogOpen, setIsIdentifyDialogOpen] = useState(false); const [isIdentifyDialogOpen, setIsIdentifyDialogOpen] = useState(false);
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
@ -114,7 +116,11 @@ export const SceneList: React.FC<ISceneList> = ({
// populate queue and go to first scene // populate queue and go to first scene
const sceneIDs = Array.from(selectedIds.values()); const sceneIDs = Array.from(selectedIds.values());
const queue = SceneQueue.fromSceneIDList(sceneIDs); const queue = SceneQueue.fromSceneIDList(sceneIDs);
queue.playScene(history, sceneIDs[0], { autoPlay: true }); const autoPlay =
config.configuration?.interface.autostartVideoOnPlaySelected ?? false;
const cont =
config.configuration?.interface.continuePlaylistDefault ?? false;
queue.playScene(history, sceneIDs[0], { autoPlay, continue: cont });
} }
async function playRandom( async function playRandom(
@ -127,7 +133,10 @@ export const SceneList: React.FC<ISceneList> = ({
const pages = Math.ceil(count / filter.itemsPerPage); const pages = Math.ceil(count / filter.itemsPerPage);
const page = Math.floor(Math.random() * pages) + 1; const page = Math.floor(Math.random() * pages) + 1;
const index = Math.floor(Math.random() * filter.itemsPerPage);
const indexMax =
filter.itemsPerPage < count ? filter.itemsPerPage : count;
const index = Math.floor(Math.random() * indexMax);
const filterCopy = _.cloneDeep(filter); const filterCopy = _.cloneDeep(filter);
filterCopy.currentPage = page; filterCopy.currentPage = page;
filterCopy.sortBy = "random"; filterCopy.sortBy = "random";
@ -136,7 +145,15 @@ export const SceneList: React.FC<ISceneList> = ({
const { id } = queryResults!.data!.findScenes!.scenes[index]; const { id } = queryResults!.data!.findScenes!.scenes[index];
// navigate to the image player page // navigate to the image player page
const queue = SceneQueue.fromListFilterModel(filterCopy); const queue = SceneQueue.fromListFilterModel(filterCopy);
queue.playScene(history, id, { sceneIndex: index, autoPlay: true }); const autoPlay =
config.configuration?.interface.autostartVideoOnPlaySelected ?? false;
const cont =
config.configuration?.interface.continuePlaylistDefault ?? false;
queue.playScene(history, id, {
sceneIndex: index,
autoPlay,
continue: cont,
});
} }
} }
} }

View file

@ -543,9 +543,10 @@ input[type="range"].blue-slider {
#queue-viewer { #queue-viewer {
.queue-controls { .queue-controls {
align-items: center;
display: flex; display: flex;
flex: 0 1 auto; flex: 0 1 auto;
justify-content: flex-end; justify-content: space-between;
} }
.thumbnail-container { .thumbnail-container {

View file

@ -38,6 +38,11 @@ export const SettingsInterfacePanel: React.FC = () => {
const [wallPlayback, setWallPlayback] = useState<string>("video"); const [wallPlayback, setWallPlayback] = useState<string>("video");
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0); const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
const [autostartVideo, setAutostartVideo] = useState<boolean>(false); const [autostartVideo, setAutostartVideo] = useState<boolean>(false);
const [
autostartVideoOnPlaySelected,
setAutostartVideoOnPlaySelected,
] = useState(true);
const [continuePlaylistDefault, setContinuePlaylistDefault] = useState(false);
const [slideshowDelay, setSlideshowDelay] = useState<number>(0); const [slideshowDelay, setSlideshowDelay] = useState<number>(0);
const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false); const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false);
const [css, setCSS] = useState<string>(); const [css, setCSS] = useState<string>();
@ -62,6 +67,8 @@ export const SettingsInterfacePanel: React.FC = () => {
maximumLoopDuration, maximumLoopDuration,
noBrowser, noBrowser,
autostartVideo, autostartVideo,
autostartVideoOnPlaySelected,
continuePlaylistDefault,
showStudioAsText, showStudioAsText,
css, css,
cssEnabled, cssEnabled,
@ -84,6 +91,10 @@ export const SettingsInterfacePanel: React.FC = () => {
setMaximumLoopDuration(iCfg.maximumLoopDuration ?? 0); setMaximumLoopDuration(iCfg.maximumLoopDuration ?? 0);
setNoBrowserFlag(iCfg?.noBrowser ?? false); setNoBrowserFlag(iCfg?.noBrowser ?? false);
setAutostartVideo(iCfg.autostartVideo ?? false); setAutostartVideo(iCfg.autostartVideo ?? false);
setAutostartVideoOnPlaySelected(
iCfg.autostartVideoOnPlaySelected ?? true
);
setContinuePlaylistDefault(iCfg.continuePlaylistDefault ?? false);
setShowStudioAsText(iCfg.showStudioAsText ?? false); setShowStudioAsText(iCfg.showStudioAsText ?? false);
setCSS(iCfg.css ?? ""); setCSS(iCfg.css ?? "");
setCSSEnabled(iCfg.cssEnabled ?? false); setCSSEnabled(iCfg.cssEnabled ?? false);
@ -286,6 +297,43 @@ export const SettingsInterfacePanel: React.FC = () => {
}} }}
/> />
</Form.Group> </Form.Group>
<Form.Group id="auto-start-video-on-play-selected">
<Form.Check
checked={autostartVideoOnPlaySelected}
label={intl.formatMessage({
id:
"config.ui.scene_player.options.auto_start_video_on_play_selected.heading",
})}
onChange={() => {
setAutostartVideoOnPlaySelected(!autostartVideoOnPlaySelected);
}}
/>
<Form.Text className="text-muted">
{intl.formatMessage({
id:
"config.ui.scene_player.options.auto_start_video_on_play_selected.description",
})}
</Form.Text>
</Form.Group>
<Form.Group id="continue-playlist-default">
<Form.Check
checked={continuePlaylistDefault}
label={intl.formatMessage({
id:
"config.ui.scene_player.options.continue_playlist_default.heading",
})}
onChange={() => {
setContinuePlaylistDefault(!continuePlaylistDefault);
}}
/>
<Form.Text className="text-muted">
{intl.formatMessage({
id:
"config.ui.scene_player.options.continue_playlist_default.description",
})}
</Form.Text>
</Form.Group>
<Form.Group id="max-loop-duration"> <Form.Group id="max-loop-duration">
<h6> <h6>

View file

@ -11,6 +11,7 @@ import Config from "./Config";
import { TaggerScene } from "./TaggerScene"; import { TaggerScene } from "./TaggerScene";
import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneTaggerModals } from "./sceneTaggerModals";
import { SceneSearchResults } from "./StashSearchResult"; import { SceneSearchResults } from "./StashSearchResult";
import { ConfigurationContext } from "src/hooks/Config";
interface ITaggerProps { interface ITaggerProps {
scenes: GQL.SlimSceneDataFragment[]; scenes: GQL.SlimSceneDataFragment[];
@ -33,14 +34,18 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
submitFingerprints, submitFingerprints,
pendingFingerprints, pendingFingerprints,
} = useContext(TaggerStateContext); } = useContext(TaggerStateContext);
const { configuration } = React.useContext(ConfigurationContext);
const [showConfig, setShowConfig] = useState(false); const [showConfig, setShowConfig] = useState(false);
const [hideUnmatched, setHideUnmatched] = useState(false); const [hideUnmatched, setHideUnmatched] = useState(false);
const intl = useIntl(); const intl = useIntl();
const cont = configuration?.interface.continuePlaylistDefault ?? false;
function generateSceneLink(scene: GQL.SlimSceneDataFragment, index: number) { function generateSceneLink(scene: GQL.SlimSceneDataFragment, index: number) {
return queue return queue
? queue.makeLink(scene.id, { sceneIndex: index }) ? queue.makeLink(scene.id, { sceneIndex: index, continue: cont })
: `/scenes/${scene.id}`; : `/scenes/${scene.id}`;
} }

View file

@ -161,11 +161,16 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
} }
}; };
const cont = config?.interface.continuePlaylistDefault ?? false;
let linkSrc: string = "#"; let linkSrc: string = "#";
if (!props.clickHandler) { if (!props.clickHandler) {
if (props.scene) { if (props.scene) {
linkSrc = props.sceneQueue linkSrc = props.sceneQueue
? props.sceneQueue.makeLink(props.scene.id, { sceneIndex: props.index }) ? props.sceneQueue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
})
: `/scenes/${props.scene.id}`; : `/scenes/${props.scene.id}`;
} else if (props.sceneMarker) { } else if (props.sceneMarker) {
linkSrc = NavUtils.makeSceneMarkerUrl(props.sceneMarker); linkSrc = NavUtils.makeSceneMarkerUrl(props.sceneMarker);

View file

@ -437,7 +437,15 @@
"scene_player": { "scene_player": {
"heading": "Scene Player", "heading": "Scene Player",
"options": { "options": {
"auto_start_video": "Auto-start video" "auto_start_video": "Auto-start video",
"auto_start_video_on_play_selected": {
"heading": "Auto-start video when playing selected",
"description": "Auto-start scene videos when playing selected or random from Scenes page"
},
"continue_playlist_default": {
"heading": "Continue playlist by default",
"description": "Play next scene in queue when video finishes"
}
} }
}, },
"scene_wall": { "scene_wall": {
@ -455,6 +463,7 @@
} }
}, },
"configuration": "Configuration", "configuration": "Configuration",
"continue": "Continue",
"countables": { "countables": {
"galleries": "{count, plural, one {Gallery} other {Galleries}}", "galleries": "{count, plural, one {Gallery} other {Galleries}}",
"images": "{count, plural, one {Image} other {Images}}", "images": "{count, plural, one {Image} other {Images}}",

View file

@ -17,6 +17,7 @@ export interface IPlaySceneOptions {
sceneIndex?: number; sceneIndex?: number;
newPage?: number; newPage?: number;
autoPlay?: boolean; autoPlay?: boolean;
continue?: boolean;
} }
export class SceneQueue { export class SceneQueue {
@ -125,6 +126,7 @@ export class SceneQueue {
options?.newPage options?.newPage
); );
const autoplayParam = options?.autoPlay ? "&autoplay=true" : ""; const autoplayParam = options?.autoPlay ? "&autoplay=true" : "";
return `/scenes/${sceneID}?${paramStr}${autoplayParam}`; const continueParam = options?.continue ? "&continue=true" : "";
return `/scenes/${sceneID}?${paramStr}${autoplayParam}${continueParam}`;
} }
} }