mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
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:
parent
25274e2596
commit
3aa5f657bc
18 changed files with 202 additions and 35 deletions
|
|
@ -54,6 +54,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||||
maximumLoopDuration
|
maximumLoopDuration
|
||||||
noBrowser
|
noBrowser
|
||||||
autostartVideo
|
autostartVideo
|
||||||
|
autostartVideoOnPlaySelected
|
||||||
|
continuePlaylistDefault
|
||||||
showStudioAsText
|
showStudioAsText
|
||||||
css
|
css
|
||||||
cssEnabled
|
cssEnabled
|
||||||
|
|
|
||||||
|
|
@ -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"""
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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}}",
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue