Loop and autostart flags. Save interface options (#230)

This commit is contained in:
WithoutPants
2019-11-29 12:41:17 +11:00
committed by Leopere
parent bcd3cefcc9
commit 8493c013e7
10 changed files with 179 additions and 47 deletions

View File

@@ -13,6 +13,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
} }
fragment ConfigInterfaceData on ConfigInterfaceResult { fragment ConfigInterfaceData on ConfigInterfaceResult {
soundOnPreview
wallShowTitle
maximumLoopDuration
autostartVideo
css css
cssEnabled cssEnabled
} }

View File

@@ -58,12 +58,28 @@ type ConfigGeneralResult {
} }
input ConfigInterfaceInput { input ConfigInterfaceInput {
"""Enable sound on mouseover previews"""
soundOnPreview: Boolean
"""Show title and tags in wall view"""
wallShowTitle: Boolean
"""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
"""Custom CSS""" """Custom CSS"""
css: String css: String
cssEnabled: Boolean cssEnabled: Boolean
} }
type ConfigInterfaceResult { type ConfigInterfaceResult {
"""Enable sound on mouseover previews"""
soundOnPreview: Boolean
"""Show title and tags in wall view"""
wallShowTitle: Boolean
"""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
"""Custom CSS""" """Custom CSS"""
css: String css: String
cssEnabled: Boolean cssEnabled: Boolean

View File

@@ -82,6 +82,22 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
} }
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) { func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.ConfigInterfaceInput) (*models.ConfigInterfaceResult, error) {
if input.SoundOnPreview != nil {
config.Set(config.SoundOnPreview, *input.SoundOnPreview)
}
if input.WallShowTitle != nil {
config.Set(config.WallShowTitle, *input.WallShowTitle)
}
if input.MaximumLoopDuration != nil {
config.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
}
if input.AutostartVideo != nil {
config.Set(config.AutostartVideo, *input.AutostartVideo)
}
css := "" css := ""
if input.CSS != nil { if input.CSS != nil {

View File

@@ -49,9 +49,18 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
} }
func makeConfigInterfaceResult() *models.ConfigInterfaceResult { func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
soundOnPreview := config.GetSoundOnPreview()
wallShowTitle := config.GetWallShowTitle()
maximumLoopDuration := config.GetMaximumLoopDuration()
autostartVideo := config.GetAutostartVideo()
css := config.GetCSS() css := config.GetCSS()
cssEnabled := config.GetCSSEnabled() cssEnabled := config.GetCSSEnabled()
return &models.ConfigInterfaceResult{ return &models.ConfigInterfaceResult{
SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle,
MaximumLoopDuration: &maximumLoopDuration,
AutostartVideo: &autostartVideo,
CSS: &css, CSS: &css,
CSSEnabled: &cssEnabled, CSSEnabled: &cssEnabled,
} }

View File

@@ -30,6 +30,11 @@ const MaxStreamingTranscodeSize = "max_streaming_transcode_size"
const Host = "host" const Host = "host"
const Port = "port" const Port = "port"
// Interface options
const SoundOnPreview = "sound_on_preview"
const WallShowTitle = "wall_show_title"
const MaximumLoopDuration = "maximum_loop_duration"
const AutostartVideo = "autostart_video"
const CSSEnabled = "cssEnabled" const CSSEnabled = "cssEnabled"
// Logging options // Logging options
@@ -165,6 +170,27 @@ func ValidateCredentials(username string, password string) bool {
return username == authUser && err == nil return username == authUser && err == nil
} }
// Interface options
func GetSoundOnPreview() bool {
viper.SetDefault(SoundOnPreview, true)
return viper.GetBool(SoundOnPreview)
}
func GetWallShowTitle() bool {
viper.SetDefault(WallShowTitle, true)
return viper.GetBool(WallShowTitle)
}
func GetMaximumLoopDuration() int {
viper.SetDefault(MaximumLoopDuration, 0)
return viper.GetInt(MaximumLoopDuration)
}
func GetAutostartVideo() bool {
viper.SetDefault(AutostartVideo, false)
return viper.GetBool(AutostartVideo)
}
func GetCSSPath() string { func GetCSSPath() string {
// use custom.css in the same directory as the config file // use custom.css in the same directory as the config file
configFileUsed := viper.ConfigFileUsed() configFileUsed := viper.ConfigFileUsed()

View File

@@ -5,11 +5,11 @@ import {
FormGroup, FormGroup,
H4, H4,
Spinner, Spinner,
TextArea TextArea,
NumericInput
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import _ from "lodash"; import _ from "lodash";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
import { useInterfaceLocalForage } from "../../hooks/LocalForage";
import { StashService } from "../../core/StashService"; import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors"; import { ErrorUtils } from "../../utils/errors";
import { ToastUtils } from "../../utils/toasts"; import { ToastUtils } from "../../utils/toasts";
@@ -17,12 +17,19 @@ import { ToastUtils } from "../../utils/toasts";
interface IProps {} interface IProps {}
export const SettingsInterfacePanel: FunctionComponent<IProps> = () => { export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
const {data, setData} = useInterfaceLocalForage();
const config = StashService.useConfiguration(); const config = StashService.useConfiguration();
const [soundOnPreview, setSoundOnPreview] = useState<boolean>();
const [wallShowTitle, setWallShowTitle] = useState<boolean>();
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
const [autostartVideo, setAutostartVideo] = useState<boolean>();
const [css, setCSS] = useState<string>(); const [css, setCSS] = useState<string>();
const [cssEnabled, setCSSEnabled] = useState<boolean>(); const [cssEnabled, setCSSEnabled] = useState<boolean>();
const updateInterfaceConfig = StashService.useConfigureInterface({ const updateInterfaceConfig = StashService.useConfigureInterface({
soundOnPreview,
wallShowTitle,
maximumLoopDuration,
autostartVideo,
css, css,
cssEnabled cssEnabled
}); });
@@ -30,6 +37,11 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
useEffect(() => { useEffect(() => {
if (!config.data || !config.data.configuration || !!config.error) { return; } if (!config.data || !config.data.configuration || !!config.error) { return; }
if (!!config.data.configuration.interface) { if (!!config.data.configuration.interface) {
let iCfg = config.data.configuration.interface;
setSoundOnPreview(iCfg.soundOnPreview !== undefined ? iCfg.soundOnPreview : true);
setWallShowTitle(iCfg.wallShowTitle !== undefined ? iCfg.wallShowTitle : true);
setMaximumLoopDuration(iCfg.maximumLoopDuration || 0);
setAutostartVideo(iCfg.autostartVideo !== undefined ? iCfg.autostartVideo : false);
setCSS(config.data.configuration.interface.css || ""); setCSS(config.data.configuration.interface.css || "");
setCSSEnabled(config.data.configuration.interface.cssEnabled || false); setCSSEnabled(config.data.configuration.interface.cssEnabled || false);
} }
@@ -55,25 +67,40 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
helperText="Configuration for wall items" helperText="Configuration for wall items"
> >
<Checkbox <Checkbox
checked={!!data ? data.wall.textContainerEnabled : true} checked={wallShowTitle}
label="Display title and tags" label="Display title and tags"
onChange={() => { onChange={() => setWallShowTitle(!wallShowTitle)}
if (!data) { return; }
const newSettings = _.cloneDeep(data);
newSettings.wall.textContainerEnabled = !data.wall.textContainerEnabled;
setData(newSettings);
}}
/> />
<Checkbox <Checkbox
checked={!!data ? data.wall.soundEnabled : true} checked={soundOnPreview}
label="Enable sound" label="Enable sound"
onChange={() => setSoundOnPreview(!soundOnPreview)}
/>
</FormGroup>
<FormGroup
label="Scene Player"
>
<Checkbox
checked={autostartVideo}
label="Auto-start video"
onChange={() => { onChange={() => {
if (!data) { return; } setAutostartVideo(!autostartVideo)
const newSettings = _.cloneDeep(data);
newSettings.wall.soundEnabled = !data.wall.soundEnabled;
setData(newSettings);
}} }}
/> />
<FormGroup
label="Maximum loop duration"
helperText="Maximum scene duration - in seconds - where scene player will loop the video - 0 to disable"
>
<NumericInput
value={maximumLoopDuration}
type="number"
onValueChange={(value: number) => setMaximumLoopDuration(value)}
min={0}
minorStepSize={1}
/>
</FormGroup>
</FormGroup> </FormGroup>
<FormGroup <FormGroup
@@ -94,10 +121,10 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
fill={true} fill={true}
rows={16}> rows={16}>
</TextArea> </TextArea>
</FormGroup>
<Divider /> <Divider />
<Button intent="primary" onClick={() => onSave()}>Save</Button> <Button intent="primary" onClick={() => onSave()}>Save</Button>
</FormGroup>
</> </>
); );
}; };

View File

@@ -2,10 +2,10 @@ import _ from "lodash";
import React, { FunctionComponent, useRef, useState, useEffect } from "react"; import React, { FunctionComponent, useRef, useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql"; import * as GQL from "../../core/generated-graphql";
import { useInterfaceLocalForage } from "../../hooks/LocalForage";
import { VideoHoverHook } from "../../hooks/VideoHover"; import { VideoHoverHook } from "../../hooks/VideoHover";
import { TextUtils } from "../../utils/text"; import { TextUtils } from "../../utils/text";
import { NavigationUtils } from "../../utils/navigation"; import { NavigationUtils } from "../../utils/navigation";
import { StashService } from "../../core/StashService";
interface IWallItemProps { interface IWallItemProps {
scene?: GQL.SlimSceneDataFragment; scene?: GQL.SlimSceneDataFragment;
@@ -29,9 +29,9 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
const [screenshotPath, setScreenshotPath] = useState<string>(""); const [screenshotPath, setScreenshotPath] = useState<string>("");
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
const [tags, setTags] = useState<JSX.Element[]>([]); const [tags, setTags] = useState<JSX.Element[]>([]);
const config = StashService.useConfiguration();
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true}); const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true});
const interfaceSettings = useInterfaceLocalForage(); const showTextContainer = !!config.data ? config.data.configuration.interface.wallShowTitle : true;
const showTextContainer = !!interfaceSettings.data ? interfaceSettings.data.wall.textContainerEnabled : true;
function onMouseEnter() { function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook); VideoHoverHook.onMouseEnter(videoHoverHook);

View File

@@ -1,11 +1,12 @@
import { Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core"; import { Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core";
import React from "react"; import React, { Component, FunctionComponent } from "react";
import ReactJWPlayer from "react-jw-player"; import ReactJWPlayer from "react-jw-player";
import * as GQL from "../../../core/generated-graphql"; import * as GQL from "../../../core/generated-graphql";
import { SceneHelpers } from "../helpers"; import { SceneHelpers } from "../helpers";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import videojs from "video.js"; import videojs from "video.js";
import "video.js/dist/video-js.css"; import "video.js/dist/video-js.css";
import { StashService } from "../../../core/StashService";
interface IScenePlayerProps { interface IScenePlayerProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
@@ -13,21 +14,26 @@ interface IScenePlayerProps {
onReady?: any; onReady?: any;
onSeeked?: any; onSeeked?: any;
onTime?: any; onTime?: any;
config?: GQL.ConfigInterfaceDataFragment;
} }
interface IScenePlayerState { interface IScenePlayerState {
scrubberPosition: number; scrubberPosition: number;
} }
export class VideoJSPlayer extends React.Component<IScenePlayerProps> { interface IVideoJSPlayerProps extends IScenePlayerProps {
videoJSOptions: videojs.PlayerOptions
}
export class VideoJSPlayer extends React.Component<IVideoJSPlayerProps> {
private player: any; private player: any;
private videoNode: any; private videoNode: any;
constructor(props: IScenePlayerProps) { constructor(props: IVideoJSPlayerProps) {
super(props); super(props);
} }
componentDidMount() { componentDidMount() {
this.player = videojs(this.videoNode); this.player = videojs(this.videoNode, this.props.videoJSOptions);
// dirty hack - make this player look like JWPlayer // dirty hack - make this player look like JWPlayer
this.player.seek = this.player.currentTime; this.player.seek = this.player.currentTime;
@@ -92,7 +98,7 @@ export class VideoJSPlayer extends React.Component<IScenePlayerProps> {
} }
@HotkeysTarget @HotkeysTarget
export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayerState> { export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePlayerState> {
private player: any; private player: any;
private lastTime = 0; private lastTime = 0;
@@ -116,7 +122,7 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
renderPlayer() { renderPlayer() {
if (this.props.scene.is_streamable) { if (this.props.scene.is_streamable) {
const config = this.makeConfig(this.props.scene); const config = this.makeJWPlayerConfig(this.props.scene);
return ( return (
<ReactJWPlayer <ReactJWPlayer
playerId={SceneHelpers.getJWPlayerId()} playerId={SceneHelpers.getJWPlayerId()}
@@ -128,8 +134,12 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
/> />
); );
} else { } else {
// don't render videoJS until config is loaded
if (this.props.config) {
const config = this.makeVideoJSConfig(this.props.scene);
return ( return (
<VideoJSPlayer <VideoJSPlayer
videoJSOptions={config}
scene={this.props.scene} scene={this.props.scene}
timestamp={this.props.timestamp} timestamp={this.props.timestamp}
onReady={this.onReady} onReady={this.onReady}
@@ -139,6 +149,7 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
) )
} }
} }
}
public render() { public render() {
return ( return (
@@ -194,8 +205,16 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
); );
} }
private makeConfig(scene: GQL.SceneDataFragment) { private shouldRepeat(scene: GQL.SceneDataFragment) {
let maxLoopDuration = this.props.config ? this.props.config.maximumLoopDuration : 0;
return !!scene.file.duration && !!maxLoopDuration && scene.file.duration < maxLoopDuration;
}
private makeJWPlayerConfig(scene: GQL.SceneDataFragment) {
if (!scene.paths.stream) { return {}; } if (!scene.paths.stream) { return {}; }
let repeat = this.shouldRepeat(scene);
return { return {
file: scene.paths.stream, file: scene.paths.stream,
image: scene.paths.screenshot, image: scene.paths.screenshot,
@@ -216,12 +235,24 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
}, },
cast: {}, cast: {},
primary: "html5", primary: "html5",
autostart: false, autostart: this.props.config ? this.props.config.autostartVideo : false,
repeat: repeat,
playbackRateControls: true, playbackRateControls: true,
playbackRates: [0.75, 1, 1.5, 2, 3, 4], playbackRates: [0.75, 1, 1.5, 2, 3, 4],
}; };
} }
private makeVideoJSConfig(scene: GQL.SceneDataFragment) {
if (!scene.paths.stream) { return {}; }
let repeat = this.shouldRepeat(scene);
return {
autoplay: this.props.config ? this.props.config.autostartVideo : false,
loop: repeat,
};
}
private onReady() { private onReady() {
this.player = SceneHelpers.getPlayer(); this.player = SceneHelpers.getPlayer();
if (this.props.timestamp > 0) { if (this.props.timestamp > 0) {
@@ -252,3 +283,9 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
this.player.pause(); this.player.pause();
} }
} }
export const ScenePlayer: FunctionComponent<IScenePlayerProps> = (props: IScenePlayerProps) => {
const config = StashService.useConfiguration();
return <ScenePlayerImpl {...props} config={config.data && config.data.configuration ? config.data.configuration.interface : undefined}/>
}

View File

@@ -3,8 +3,6 @@ import _ from "lodash";
import React from "react"; import React from "react";
interface IInterfaceWallConfig { interface IInterfaceWallConfig {
textContainerEnabled: boolean;
soundEnabled: boolean;
} }
export interface IInterfaceConfig { export interface IInterfaceConfig {
wall: IInterfaceWallConfig; wall: IInterfaceWallConfig;
@@ -25,8 +23,7 @@ export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undef
if (result.data === undefined) { if (result.data === undefined) {
result.setData({ result.setData({
wall: { wall: {
textContainerEnabled: true, // nothing here currently
soundEnabled: true,
}, },
}); });
} }

View File

@@ -1,5 +1,5 @@
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { useInterfaceLocalForage } from "./LocalForage"; import { StashService } from "../core/StashService";
export interface IVideoHoverHookData { export interface IVideoHoverHookData {
videoEl: React.RefObject<HTMLVideoElement>; videoEl: React.RefObject<HTMLVideoElement>;
@@ -18,8 +18,8 @@ export class VideoHoverHook {
const isPlaying = useRef<boolean>(false); const isPlaying = useRef<boolean>(false);
const isHovering = useRef<boolean>(false); const isHovering = useRef<boolean>(false);
const interfaceSettings = useInterfaceLocalForage(); const config = StashService.useConfiguration();
const soundEnabled = !!interfaceSettings.data ? interfaceSettings.data.wall.soundEnabled : true; const soundEnabled = !!config.data && !!config.data.configuration ? config.data.configuration.interface.soundOnPreview : true;
useEffect(() => { useEffect(() => {
const videoTag = videoEl.current; const videoTag = videoEl.current;