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
noBrowser
autostartVideo
autostartVideoOnPlaySelected
continuePlaylistDefault
showStudioAsText
css
cssEnabled

View file

@ -197,27 +197,40 @@ input ConfigDisableDropdownCreateInput {
input ConfigInterfaceInput {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
"""Enable sound on mouseover previews"""
soundOnPreview: Boolean
"""Show title and tags in wall view"""
wallShowTitle: Boolean
"""Wall playback type"""
wallPlayback: String
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
maximumLoopDuration: Int
"""If true, video will autostart on load in the scene player"""
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"""
showStudioAsText: Boolean
"""Custom CSS"""
css: String
cssEnabled: Boolean
"""Interface language"""
language: String
"""Slideshow Delay"""
slideshowDelay: Int
"""Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput
"""Handy Connection Key"""
handyKey: String
"""Funscript Time Offset"""
@ -235,29 +248,42 @@ type ConfigDisableDropdownCreate {
type ConfigInterfaceResult {
"""Ordered list of items that should be shown in the menu"""
menuItems: [String!]
"""Enable sound on mouseover previews"""
soundOnPreview: Boolean
"""Show title and tags in wall view"""
wallShowTitle: Boolean
"""Wall playback type"""
wallPlayback: String
"""Maximum duration (in seconds) in which a scene video will loop in the scene player"""
maximumLoopDuration: Int
""""True if we should not auto-open a browser window on startup"""
noBrowser: Boolean
"""If true, video will autostart on load in the scene player"""
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"""
showStudioAsText: Boolean
"""Custom CSS"""
css: String
cssEnabled: Boolean
"""Interface language"""
language: String
"""Slideshow Delay"""
slideshowDelay: Int
"""Fields are true if creating via dropdown menus are disabled"""
disabledDropdownCreate: ConfigDisableDropdownCreate!
"""Handy Connection Key"""
handyKey: String
"""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.ShowStudioAsText, input.ShowStudioAsText)
setBool(config.AutostartVideoOnPlaySelected, input.AutostartVideoOnPlaySelected)
setBool(config.ContinuePlaylistDefault, input.ContinuePlaylistDefault)
if input.Language != nil {
c.Set(config.Language, *input.Language)

View file

@ -111,6 +111,8 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
noBrowser := config.GetNoBrowserFlag()
maximumLoopDuration := config.GetMaximumLoopDuration()
autostartVideo := config.GetAutostartVideo()
autostartVideoOnPlaySelected := config.GetAutostartVideoOnPlaySelected()
continuePlaylistDefault := config.GetContinuePlaylistDefault()
showStudioAsText := config.GetShowStudioAsText()
css := config.GetCSS()
cssEnabled := config.GetCSSEnabled()
@ -120,21 +122,23 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
scriptOffset := config.GetFunscriptOffset()
return &models.ConfigInterfaceResult{
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration,
NoBrowser: &noBrowser,
AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText,
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
DisabledDropdownCreate: config.GetDisableDropdownCreate(),
HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
MenuItems: menuItems,
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
WallPlayback: &wallPlayback,
MaximumLoopDuration: &maximumLoopDuration,
NoBrowser: &noBrowser,
AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText,
AutostartVideoOnPlaySelected: &autostartVideoOnPlaySelected,
ContinuePlaylistDefault: &continuePlaylistDefault,
CSS: &css,
CSSEnabled: &cssEnabled,
Language: &language,
SlideshowDelay: &slideshowDelay,
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 MaximumLoopDuration = "maximum_loop_duration"
const AutostartVideo = "autostart_video"
const AutostartVideoOnPlaySelected = "autostart_video_on_play_selected"
const ContinuePlaylistDefault = "continue_playlist_default"
const ShowStudioAsText = "show_studio_as_text"
const CSSEnabled = "cssEnabled"
const WallPlayback = "wall_playback"
@ -808,6 +810,20 @@ func (i *Instance) GetAutostartVideo() bool {
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 {
i.Lock()
defer i.Unlock()

View file

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

View file

@ -1,4 +1,5 @@
### ✨ 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))
* 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))

View file

@ -88,13 +88,8 @@ export const GalleryList: React.FC<IGalleryList> = ({
filterCopy.itemsPerPage = 1;
filterCopy.currentPage = index + 1;
const singleResult = await queryFindGalleries(filterCopy);
if (
singleResult &&
singleResult.data &&
singleResult.data.findGalleries &&
singleResult.data.findGalleries.galleries.length === 1
) {
const { id } = singleResult!.data!.findGalleries!.galleries[0];
if (singleResult.data.findGalleries.galleries.length === 1) {
const { id } = singleResult.data.findGalleries.galleries[0];
// navigate to the image player page
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
? props.queue.makeLink(props.scene.id, { sceneIndex: props.index })
? props.queue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
})
: `/scenes/${props.scene.id}`;
return (

View file

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

View file

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

View file

@ -24,6 +24,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
import { SceneCardsGrid } from "./SceneCardsGrid";
import { TaggerContext } from "../Tagger/context";
import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog";
import { ConfigurationContext } from "src/hooks/Config";
interface ISceneList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
@ -38,6 +39,7 @@ export const SceneList: React.FC<ISceneList> = ({
}) => {
const intl = useIntl();
const history = useHistory();
const config = React.useContext(ConfigurationContext);
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
const [isIdentifyDialogOpen, setIsIdentifyDialogOpen] = useState(false);
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
@ -114,7 +116,11 @@ export const SceneList: React.FC<ISceneList> = ({
// populate queue and go to first scene
const sceneIDs = Array.from(selectedIds.values());
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(
@ -127,7 +133,10 @@ export const SceneList: React.FC<ISceneList> = ({
const pages = Math.ceil(count / filter.itemsPerPage);
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);
filterCopy.currentPage = page;
filterCopy.sortBy = "random";
@ -136,7 +145,15 @@ export const SceneList: React.FC<ISceneList> = ({
const { id } = queryResults!.data!.findScenes!.scenes[index];
// navigate to the image player page
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-controls {
align-items: center;
display: flex;
flex: 0 1 auto;
justify-content: flex-end;
justify-content: space-between;
}
.thumbnail-container {

View file

@ -38,6 +38,11 @@ export const SettingsInterfacePanel: React.FC = () => {
const [wallPlayback, setWallPlayback] = useState<string>("video");
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
const [autostartVideo, setAutostartVideo] = useState<boolean>(false);
const [
autostartVideoOnPlaySelected,
setAutostartVideoOnPlaySelected,
] = useState(true);
const [continuePlaylistDefault, setContinuePlaylistDefault] = useState(false);
const [slideshowDelay, setSlideshowDelay] = useState<number>(0);
const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false);
const [css, setCSS] = useState<string>();
@ -62,6 +67,8 @@ export const SettingsInterfacePanel: React.FC = () => {
maximumLoopDuration,
noBrowser,
autostartVideo,
autostartVideoOnPlaySelected,
continuePlaylistDefault,
showStudioAsText,
css,
cssEnabled,
@ -84,6 +91,10 @@ export const SettingsInterfacePanel: React.FC = () => {
setMaximumLoopDuration(iCfg.maximumLoopDuration ?? 0);
setNoBrowserFlag(iCfg?.noBrowser ?? false);
setAutostartVideo(iCfg.autostartVideo ?? false);
setAutostartVideoOnPlaySelected(
iCfg.autostartVideoOnPlaySelected ?? true
);
setContinuePlaylistDefault(iCfg.continuePlaylistDefault ?? false);
setShowStudioAsText(iCfg.showStudioAsText ?? false);
setCSS(iCfg.css ?? "");
setCSSEnabled(iCfg.cssEnabled ?? false);
@ -286,6 +297,43 @@ export const SettingsInterfacePanel: React.FC = () => {
}}
/>
</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">
<h6>

View file

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

View file

@ -437,7 +437,15 @@
"scene_player": {
"heading": "Scene Player",
"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": {
@ -455,6 +463,7 @@
}
},
"configuration": "Configuration",
"continue": "Continue",
"countables": {
"galleries": "{count, plural, one {Gallery} other {Galleries}}",
"images": "{count, plural, one {Image} other {Images}}",

View file

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