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 {
soundOnPreview
wallShowTitle
maximumLoopDuration
autostartVideo
css
cssEnabled
}

View File

@@ -58,12 +58,28 @@ type ConfigGeneralResult {
}
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"""
css: String
cssEnabled: Boolean
}
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"""
css: String
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) {
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 := ""
if input.CSS != nil {

View File

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

View File

@@ -30,6 +30,11 @@ const MaxStreamingTranscodeSize = "max_streaming_transcode_size"
const Host = "host"
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"
// Logging options
@@ -165,6 +170,27 @@ func ValidateCredentials(username string, password string) bool {
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 {
// use custom.css in the same directory as the config file
configFileUsed := viper.ConfigFileUsed()

View File

@@ -5,11 +5,11 @@ import {
FormGroup,
H4,
Spinner,
TextArea
TextArea,
NumericInput
} from "@blueprintjs/core";
import _ from "lodash";
import React, { FunctionComponent, useEffect, useState } from "react";
import { useInterfaceLocalForage } from "../../hooks/LocalForage";
import { StashService } from "../../core/StashService";
import { ErrorUtils } from "../../utils/errors";
import { ToastUtils } from "../../utils/toasts";
@@ -17,12 +17,19 @@ import { ToastUtils } from "../../utils/toasts";
interface IProps {}
export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
const {data, setData} = useInterfaceLocalForage();
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 [cssEnabled, setCSSEnabled] = useState<boolean>();
const updateInterfaceConfig = StashService.useConfigureInterface({
soundOnPreview,
wallShowTitle,
maximumLoopDuration,
autostartVideo,
css,
cssEnabled
});
@@ -30,6 +37,11 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
useEffect(() => {
if (!config.data || !config.data.configuration || !!config.error) { return; }
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 || "");
setCSSEnabled(config.data.configuration.interface.cssEnabled || false);
}
@@ -55,25 +67,40 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
helperText="Configuration for wall items"
>
<Checkbox
checked={!!data ? data.wall.textContainerEnabled : true}
checked={wallShowTitle}
label="Display title and tags"
onChange={() => {
if (!data) { return; }
const newSettings = _.cloneDeep(data);
newSettings.wall.textContainerEnabled = !data.wall.textContainerEnabled;
setData(newSettings);
}}
onChange={() => setWallShowTitle(!wallShowTitle)}
/>
<Checkbox
checked={!!data ? data.wall.soundEnabled : true}
checked={soundOnPreview}
label="Enable sound"
onChange={() => setSoundOnPreview(!soundOnPreview)}
/>
</FormGroup>
<FormGroup
label="Scene Player"
>
<Checkbox
checked={autostartVideo}
label="Auto-start video"
onChange={() => {
if (!data) { return; }
const newSettings = _.cloneDeep(data);
newSettings.wall.soundEnabled = !data.wall.soundEnabled;
setData(newSettings);
setAutostartVideo(!autostartVideo)
}}
/>
<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
@@ -94,10 +121,10 @@ export const SettingsInterfacePanel: FunctionComponent<IProps> = () => {
fill={true}
rows={16}>
</TextArea>
</FormGroup>
<Divider />
<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 { Link } from "react-router-dom";
import * as GQL from "../../core/generated-graphql";
import { useInterfaceLocalForage } from "../../hooks/LocalForage";
import { VideoHoverHook } from "../../hooks/VideoHover";
import { TextUtils } from "../../utils/text";
import { NavigationUtils } from "../../utils/navigation";
import { StashService } from "../../core/StashService";
interface IWallItemProps {
scene?: GQL.SlimSceneDataFragment;
@@ -29,9 +29,9 @@ export const WallItem: FunctionComponent<IWallItemProps> = (props: IWallItemProp
const [screenshotPath, setScreenshotPath] = useState<string>("");
const [title, setTitle] = useState<string>("");
const [tags, setTags] = useState<JSX.Element[]>([]);
const config = StashService.useConfiguration();
const videoHoverHook = VideoHoverHook.useVideoHover({resetOnMouseLeave: true});
const interfaceSettings = useInterfaceLocalForage();
const showTextContainer = !!interfaceSettings.data ? interfaceSettings.data.wall.textContainerEnabled : true;
const showTextContainer = !!config.data ? config.data.configuration.interface.wallShowTitle : true;
function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook);

View File

@@ -1,11 +1,12 @@
import { Hotkey, Hotkeys, HotkeysTarget } from "@blueprintjs/core";
import React from "react";
import React, { Component, FunctionComponent } from "react";
import ReactJWPlayer from "react-jw-player";
import * as GQL from "../../../core/generated-graphql";
import { SceneHelpers } from "../helpers";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import videojs from "video.js";
import "video.js/dist/video-js.css";
import { StashService } from "../../../core/StashService";
interface IScenePlayerProps {
scene: GQL.SceneDataFragment;
@@ -13,21 +14,26 @@ interface IScenePlayerProps {
onReady?: any;
onSeeked?: any;
onTime?: any;
config?: GQL.ConfigInterfaceDataFragment;
}
interface IScenePlayerState {
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 videoNode: any;
constructor(props: IScenePlayerProps) {
constructor(props: IVideoJSPlayerProps) {
super(props);
}
componentDidMount() {
this.player = videojs(this.videoNode);
this.player = videojs(this.videoNode, this.props.videoJSOptions);
// dirty hack - make this player look like JWPlayer
this.player.seek = this.player.currentTime;
@@ -92,7 +98,7 @@ export class VideoJSPlayer extends React.Component<IScenePlayerProps> {
}
@HotkeysTarget
export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayerState> {
export class ScenePlayerImpl extends React.Component<IScenePlayerProps, IScenePlayerState> {
private player: any;
private lastTime = 0;
@@ -116,7 +122,7 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
renderPlayer() {
if (this.props.scene.is_streamable) {
const config = this.makeConfig(this.props.scene);
const config = this.makeJWPlayerConfig(this.props.scene);
return (
<ReactJWPlayer
playerId={SceneHelpers.getJWPlayerId()}
@@ -128,8 +134,12 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
/>
);
} else {
// don't render videoJS until config is loaded
if (this.props.config) {
const config = this.makeVideoJSConfig(this.props.scene);
return (
<VideoJSPlayer
videoJSOptions={config}
scene={this.props.scene}
timestamp={this.props.timestamp}
onReady={this.onReady}
@@ -139,6 +149,7 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
)
}
}
}
public render() {
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 {}; }
let repeat = this.shouldRepeat(scene);
return {
file: scene.paths.stream,
image: scene.paths.screenshot,
@@ -216,12 +235,24 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
},
cast: {},
primary: "html5",
autostart: false,
autostart: this.props.config ? this.props.config.autostartVideo : false,
repeat: repeat,
playbackRateControls: true,
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() {
this.player = SceneHelpers.getPlayer();
if (this.props.timestamp > 0) {
@@ -252,3 +283,9 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
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";
interface IInterfaceWallConfig {
textContainerEnabled: boolean;
soundEnabled: boolean;
}
export interface IInterfaceConfig {
wall: IInterfaceWallConfig;
@@ -25,8 +23,7 @@ export function useInterfaceLocalForage(): ILocalForage<IInterfaceConfig | undef
if (result.data === undefined) {
result.setData({
wall: {
textContainerEnabled: true,
soundEnabled: true,
// nothing here currently
},
});
}

View File

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