mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Loop and autostart flags. Save interface options (#230)
This commit is contained in:
@@ -13,6 +13,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
soundOnPreview
|
||||
wallShowTitle
|
||||
maximumLoopDuration
|
||||
autostartVideo
|
||||
css
|
||||
cssEnabled
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -49,10 +49,19 @@ 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{
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
AutostartVideo: &autostartVideo,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
<Divider />
|
||||
<Button intent="primary" onClick={() => onSave()}>Save</Button>
|
||||
</FormGroup>
|
||||
|
||||
<Divider />
|
||||
<Button intent="primary" onClick={() => onSave()}>Save</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,15 +134,20 @@ export class ScenePlayer extends React.Component<IScenePlayerProps, IScenePlayer
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<VideoJSPlayer
|
||||
scene={this.props.scene}
|
||||
timestamp={this.props.timestamp}
|
||||
onReady={this.onReady}
|
||||
onSeeked={this.onSeeked}
|
||||
onTime={this.onTime}>
|
||||
</VideoJSPlayer>
|
||||
)
|
||||
// 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}
|
||||
onSeeked={this.onSeeked}
|
||||
onTime={this.onTime}>
|
||||
</VideoJSPlayer>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}/>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user