Add option to disable create from dropdown (#1814)

* Convert config hooks to common context
* Add option to disable creating from dropdown
This commit is contained in:
WithoutPants
2021-10-11 17:45:58 +11:00
committed by GitHub
parent 46ae4581b8
commit b5381ff071
24 changed files with 269 additions and 130 deletions

View File

@@ -58,6 +58,11 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
cssEnabled cssEnabled
language language
slideshowDelay slideshowDelay
disabledDropdownCreate {
performer
tag
studio
}
handyKey handyKey
funscriptOffset funscriptOffset
} }

View File

@@ -188,6 +188,12 @@ type ConfigGeneralResult {
stashBoxes: [StashBox!]! stashBoxes: [StashBox!]!
} }
input ConfigDisableDropdownCreateInput {
performer: Boolean
tag: Boolean
studio: Boolean
}
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!]
@@ -210,12 +216,20 @@ input ConfigInterfaceInput {
language: String language: String
"""Slideshow Delay""" """Slideshow Delay"""
slideshowDelay: Int slideshowDelay: Int
"""Set to true to disable creating new objects via the dropdown menus"""
disableDropdownCreate: ConfigDisableDropdownCreateInput
"""Handy Connection Key""" """Handy Connection Key"""
handyKey: String handyKey: String
"""Funscript Time Offset""" """Funscript Time Offset"""
funscriptOffset: Int funscriptOffset: Int
} }
type ConfigDisableDropdownCreate {
performer: Boolean!
tag: Boolean!
studio: Boolean!
}
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!]
@@ -238,6 +252,8 @@ type ConfigInterfaceResult {
language: String language: String
"""Slideshow Delay""" """Slideshow Delay"""
slideshowDelay: Int slideshowDelay: Int
"""Fields are true if creating via dropdown menus are disabled"""
disabledDropdownCreate: ConfigDisableDropdownCreate!
"""Handy Connection Key""" """Handy Connection Key"""
handyKey: String handyKey: String
"""Funscript Time Offset""" """Funscript Time Offset"""

View File

@@ -225,17 +225,19 @@ 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) {
c := config.GetInstance() c := config.GetInstance()
setBool := func(key string, v *bool) {
if v != nil {
c.Set(key, *v)
}
}
if input.MenuItems != nil { if input.MenuItems != nil {
c.Set(config.MenuItems, input.MenuItems) c.Set(config.MenuItems, input.MenuItems)
} }
if input.SoundOnPreview != nil { setBool(config.SoundOnPreview, input.SoundOnPreview)
c.Set(config.SoundOnPreview, *input.SoundOnPreview) setBool(config.WallShowTitle, input.WallShowTitle)
}
if input.WallShowTitle != nil {
c.Set(config.WallShowTitle, *input.WallShowTitle)
}
if input.WallPlayback != nil { if input.WallPlayback != nil {
c.Set(config.WallPlayback, *input.WallPlayback) c.Set(config.WallPlayback, *input.WallPlayback)
@@ -245,13 +247,8 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration) c.Set(config.MaximumLoopDuration, *input.MaximumLoopDuration)
} }
if input.AutostartVideo != nil { setBool(config.AutostartVideo, input.AutostartVideo)
c.Set(config.AutostartVideo, *input.AutostartVideo) setBool(config.ShowStudioAsText, input.ShowStudioAsText)
}
if input.ShowStudioAsText != nil {
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
}
if input.Language != nil { if input.Language != nil {
c.Set(config.Language, *input.Language) c.Set(config.Language, *input.Language)
@@ -269,8 +266,13 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
c.SetCSS(css) c.SetCSS(css)
if input.CSSEnabled != nil { setBool(config.CSSEnabled, input.CSSEnabled)
c.Set(config.CSSEnabled, *input.CSSEnabled)
if input.DisableDropdownCreate != nil {
ddc := input.DisableDropdownCreate
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
setBool(config.DisableDropdownCreateStudio, ddc.Studio)
setBool(config.DisableDropdownCreateTag, ddc.Tag)
} }
if input.HandyKey != nil { if input.HandyKey != nil {

View File

@@ -115,19 +115,20 @@ 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,
AutostartVideo: &autostartVideo, AutostartVideo: &autostartVideo,
ShowStudioAsText: &showStudioAsText, ShowStudioAsText: &showStudioAsText,
CSS: &css, CSS: &css,
CSSEnabled: &cssEnabled, CSSEnabled: &cssEnabled,
Language: &language, Language: &language,
SlideshowDelay: &slideshowDelay, SlideshowDelay: &slideshowDelay,
HandyKey: &handyKey, DisabledDropdownCreate: config.GetDisableDropdownCreate(),
FunscriptOffset: &scriptOffset, HandyKey: &handyKey,
FunscriptOffset: &scriptOffset,
} }
} }

View File

@@ -135,6 +135,13 @@ const ShowStudioAsText = "show_studio_as_text"
const CSSEnabled = "cssEnabled" const CSSEnabled = "cssEnabled"
const WallPlayback = "wall_playback" const WallPlayback = "wall_playback"
const SlideshowDelay = "slideshow_delay" const SlideshowDelay = "slideshow_delay"
const (
DisableDropdownCreatePerformer = "disable_dropdown_create.performer"
DisableDropdownCreateStudio = "disable_dropdown_create.studio"
DisableDropdownCreateTag = "disable_dropdown_create.tag"
)
const HandyKey = "handy_key" const HandyKey = "handy_key"
const FunscriptOffset = "funscript_offset" const FunscriptOffset = "funscript_offset"
@@ -787,6 +794,17 @@ func (i *Instance) GetSlideshowDelay() int {
return viper.GetInt(SlideshowDelay) return viper.GetInt(SlideshowDelay)
} }
func (i *Instance) GetDisableDropdownCreate() *models.ConfigDisableDropdownCreate {
i.Lock()
defer i.Unlock()
return &models.ConfigDisableDropdownCreate{
Performer: viper.GetBool(DisableDropdownCreatePerformer),
Studio: viper.GetBool(DisableDropdownCreateStudio),
Tag: viper.GetBool(DisableDropdownCreateTag),
}
}
func (i *Instance) GetCSSPath() string { func (i *Instance) GetCSSPath() string {
i.RLock() i.RLock()
defer i.RUnlock() defer i.RUnlock()

View File

@@ -31,6 +31,7 @@ import { Setup } from "./components/Setup/Setup";
import { Migrate } from "./components/Setup/Migrate"; import { Migrate } from "./components/Setup/Migrate";
import * as GQL from "./core/generated-graphql"; import * as GQL from "./core/generated-graphql";
import { LoadingIndicator } from "./components/Shared"; import { LoadingIndicator } from "./components/Shared";
import ConfigurationProvider from "./hooks/Config";
initPolyfills(); initPolyfills();
@@ -138,12 +139,17 @@ export const App: React.FC = () => {
return ( return (
<ErrorBoundary> <ErrorBoundary>
<IntlProvider locale={language} messages={messages} formats={intlFormats}> <IntlProvider locale={language} messages={messages} formats={intlFormats}>
<ToastProvider> <ConfigurationProvider
<LightboxProvider> configuration={config.data?.configuration}
{maybeRenderNavbar()} loading={config.loading}
<div className="main container-fluid">{renderContent()}</div> >
</LightboxProvider> <ToastProvider>
</ToastProvider> <LightboxProvider>
{maybeRenderNavbar()}
<div className="main container-fluid">{renderContent()}</div>
</LightboxProvider>
</ToastProvider>
</ConfigurationProvider>
</IntlProvider> </IntlProvider>
</ErrorBoundary> </ErrorBoundary>
); );

View File

@@ -1,2 +1,5 @@
### ✨ New Features
* Added interface options to disable creating performers/studios/tags from dropdown selectors. ([#1814](https://github.com/stashapp/stash/pull/1814))
### 🐛 Bug fixes ### 🐛 Bug fixes
* Fix huge memory usage spike during clean task. ([#1805](https://github.com/stashapp/stash/pull/1805)) * Fix huge memory usage spike during clean task. ([#1805](https://github.com/stashapp/stash/pull/1805))

View File

@@ -2,7 +2,6 @@ import { Button, ButtonGroup } from "react-bootstrap";
import React from "react"; import React from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import { import {
GridCard, GridCard,
HoverPopover, HoverPopover,
@@ -12,6 +11,7 @@ import {
} from "src/components/Shared"; } from "src/components/Shared";
import { PopoverCountButton } from "src/components/Shared/PopoverCountButton"; import { PopoverCountButton } from "src/components/Shared/PopoverCountButton";
import { NavUtils, TextUtils } from "src/utils"; import { NavUtils, TextUtils } from "src/utils";
import { ConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
@@ -24,9 +24,8 @@ interface IProps {
} }
export const GalleryCard: React.FC<IProps> = (props) => { export const GalleryCard: React.FC<IProps> = (props) => {
const config = useConfiguration(); const { configuration } = React.useContext(ConfigurationContext);
const showStudioAsText = const showStudioAsText = configuration?.interface.showStudioAsText ?? false;
config?.data?.configuration.interface.showStudioAsText ?? false;
function maybeRenderScenePopoverButton() { function maybeRenderScenePopoverButton() {
if (props.gallery.scenes.length === 0) return; if (props.gallery.scenes.length === 0) return;

View File

@@ -13,8 +13,8 @@ import Mousetrap from "mousetrap";
import { SessionUtils } from "src/utils"; import { SessionUtils } from "src/utils";
import { Icon } from "src/components/Shared"; import { Icon } from "src/components/Shared";
import { ConfigurationContext } from "src/hooks/Config";
import { Manual } from "./Help/Manual"; import { Manual } from "./Help/Manual";
import { useConfiguration } from "../core/StashService";
interface IMenuItem { interface IMenuItem {
name: string; name: string;
@@ -120,7 +120,7 @@ const allMenuItems: IMenuItem[] = [
export const MainNavbar: React.FC = () => { export const MainNavbar: React.FC = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { data: config, loading } = useConfiguration(); const { configuration, loading } = React.useContext(ConfigurationContext);
// Show all menu items by default, unless config says otherwise // Show all menu items by default, unless config says otherwise
const [menuItems, setMenuItems] = useState<IMenuItem[]>(allMenuItems); const [menuItems, setMenuItems] = useState<IMenuItem[]>(allMenuItems);
@@ -129,7 +129,7 @@ export const MainNavbar: React.FC = () => {
const [showManual, setShowManual] = useState(false); const [showManual, setShowManual] = useState(false);
useEffect(() => { useEffect(() => {
const iCfg = config?.configuration?.interface; const iCfg = configuration?.interface;
if (iCfg?.menuItems) { if (iCfg?.menuItems) {
setMenuItems( setMenuItems(
allMenuItems.filter((menuItem) => allMenuItems.filter((menuItem) =>
@@ -137,7 +137,7 @@ export const MainNavbar: React.FC = () => {
) )
); );
} }
}, [config]); }, [configuration]);
// react-bootstrap typing bug // react-bootstrap typing bug
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -20,7 +20,6 @@ import {
usePerformerCreate, usePerformerCreate,
useTagCreate, useTagCreate,
queryScrapePerformerURL, queryScrapePerformerURL,
useConfiguration,
} from "src/core/StashService"; } from "src/core/StashService";
import { import {
Icon, Icon,
@@ -41,6 +40,7 @@ import {
genderToString, genderToString,
stringToGender, stringToGender,
} from "src/utils/gender"; } from "src/utils/gender";
import { ConfigurationContext } from "src/hooks/Config";
import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerScrapeModal from "./PerformerScrapeModal";
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
@@ -88,7 +88,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const [scrapedPerformer, setScrapedPerformer] = useState< const [scrapedPerformer, setScrapedPerformer] = useState<
GQL.ScrapedPerformer | undefined GQL.ScrapedPerformer | undefined
>(); >();
const stashConfig = useConfiguration(); const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true); const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
@@ -601,7 +601,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
if (!performer) { if (!performer) {
return; return;
} }
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? []; const stashBoxes = stashConfig?.general.stashBoxes ?? [];
const popover = ( const popover = (
<Dropdown.Menu id="performer-scraper-popover"> <Dropdown.Menu id="performer-scraper-popover">

View File

@@ -2,8 +2,8 @@
import React from "react"; import React from "react";
import ReactJWPlayer from "react-jw-player"; import ReactJWPlayer from "react-jw-player";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import { JWUtils, ScreenUtils } from "src/utils"; import { JWUtils, ScreenUtils } from "src/utils";
import { ConfigurationContext } from "src/hooks/Config";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import { Interactive } from "../../utils/interactive"; import { Interactive } from "../../utils/interactive";
@@ -366,16 +366,12 @@ export class ScenePlayerImpl extends React.Component<
export const ScenePlayer: React.FC<IScenePlayerProps> = ( export const ScenePlayer: React.FC<IScenePlayerProps> = (
props: IScenePlayerProps props: IScenePlayerProps
) => { ) => {
const config = useConfiguration(); const { configuration } = React.useContext(ConfigurationContext);
return ( return (
<ScenePlayerImpl <ScenePlayerImpl
{...props} {...props}
config={ config={configuration ? configuration.interface : undefined}
config.data && config.data.configuration
? config.data.configuration.interface
: undefined
}
/> />
); );
}; };

View File

@@ -3,7 +3,6 @@ import { Button, ButtonGroup } from "react-bootstrap";
import { Link } from "react-router-dom"; 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 { useConfiguration } from "src/core/StashService";
import { import {
Icon, Icon,
TagLink, TagLink,
@@ -13,6 +12,7 @@ import {
} from "src/components/Shared"; } from "src/components/Shared";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard"; import { GridCard } from "../Shared/GridCard";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
@@ -80,15 +80,14 @@ interface ISceneCardProps {
export const SceneCard: React.FC<ISceneCardProps> = ( export const SceneCard: React.FC<ISceneCardProps> = (
props: ISceneCardProps props: ISceneCardProps
) => { ) => {
const config = useConfiguration(); const { configuration } = React.useContext(ConfigurationContext);
// studio image is missing if it uses the default // studio image is missing if it uses the default
const missingStudioImage = props.scene.studio?.image_path?.endsWith( const missingStudioImage = props.scene.studio?.image_path?.endsWith(
"?default=true" "?default=true"
); );
const showStudioAsText = const showStudioAsText =
missingStudioImage || missingStudioImage || (configuration?.interface.showStudioAsText ?? false);
(config?.data?.configuration.interface.showStudioAsText ?? false);
function maybeRenderSceneSpecsOverlay() { function maybeRenderSceneSpecsOverlay() {
return ( return (
@@ -327,9 +326,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
image={props.scene.paths.screenshot ?? undefined} image={props.scene.paths.screenshot ?? undefined}
video={props.scene.paths.preview ?? undefined} video={props.scene.paths.preview ?? undefined}
isPortrait={isPortrait()} isPortrait={isPortrait()}
soundActive={ soundActive={configuration?.interface?.soundOnPreview ?? false}
config.data?.configuration?.interface?.soundOnPreview ?? false
}
/> />
<RatingBanner rating={props.scene.rating} /> <RatingBanner rating={props.scene.rating} />
{maybeRenderSceneSpecsOverlay()} {maybeRenderSceneSpecsOverlay()}

View File

@@ -18,7 +18,6 @@ import {
useListSceneScrapers, useListSceneScrapers,
useSceneUpdate, useSceneUpdate,
mutateReloadScrapers, mutateReloadScrapers,
useConfiguration,
queryScrapeSceneQueryFragment, queryScrapeSceneQueryFragment,
} from "src/core/StashService"; } from "src/core/StashService";
import { import {
@@ -35,6 +34,7 @@ import { ImageUtils, FormUtils, TextUtils, getStashIDs } from "src/utils";
import { MovieSelect } from "src/components/Shared/Select"; import { MovieSelect } from "src/components/Shared/Select";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { Prompt } from "react-router"; import { Prompt } from "react-router";
import { ConfigurationContext } from "src/hooks/Config";
import { SceneMovieTable } from "./SceneMovieTable"; import { SceneMovieTable } from "./SceneMovieTable";
import { RatingStars } from "./RatingStars"; import { RatingStars } from "./RatingStars";
import { SceneScrapeDialog } from "./SceneScrapeDialog"; import { SceneScrapeDialog } from "./SceneScrapeDialog";
@@ -76,7 +76,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
string | undefined string | undefined
>(scene.paths.screenshot ?? undefined); >(scene.paths.screenshot ?? undefined);
const stashConfig = useConfiguration(); const { configuration: stashConfig } = React.useContext(ConfigurationContext);
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -380,7 +380,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
} }
function renderScrapeQueryMenu() { function renderScrapeQueryMenu() {
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? []; const stashBoxes = stashConfig?.general.stashBoxes ?? [];
if (stashBoxes.length === 0 && queryableScrapers.length === 0) return; if (stashBoxes.length === 0 && queryableScrapers.length === 0) return;
@@ -450,7 +450,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
}; };
function renderScraperMenu() { function renderScraperMenu() {
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? []; const stashBoxes = stashConfig?.general.stashBoxes ?? [];
return ( return (
<DropdownButton <DropdownButton

View File

@@ -1,13 +1,11 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { Form, Button, Collapse } from "react-bootstrap"; import { Form, Button, Collapse } from "react-bootstrap";
import { import { mutateMetadataGenerate } from "src/core/StashService";
mutateMetadataGenerate,
useConfiguration,
} from "src/core/StashService";
import { Modal, Icon } from "src/components/Shared"; import { Modal, Icon } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config";
interface ISceneGenerateDialogProps { interface ISceneGenerateDialogProps {
selectedIds: string[]; selectedIds: string[];
@@ -17,7 +15,7 @@ interface ISceneGenerateDialogProps {
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = ( export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
props: ISceneGenerateDialogProps props: ISceneGenerateDialogProps
) => { ) => {
const { data, error, loading } = useConfiguration(); const { configuration } = React.useContext(ConfigurationContext);
const [sprites, setSprites] = useState(true); const [sprites, setSprites] = useState(true);
const [phashes, setPhashes] = useState(true); const [phashes, setPhashes] = useState(true);
@@ -49,17 +47,16 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
const Toast = useToast(); const Toast = useToast();
useEffect(() => { useEffect(() => {
if (!data?.configuration) return; if (!configuration) return;
const conf = data.configuration; if (configuration.general) {
if (conf.general) { setPreviewSegments(configuration.general.previewSegments);
setPreviewSegments(conf.general.previewSegments); setPreviewSegmentDuration(configuration.general.previewSegmentDuration);
setPreviewSegmentDuration(conf.general.previewSegmentDuration); setPreviewExcludeStart(configuration.general.previewExcludeStart);
setPreviewExcludeStart(conf.general.previewExcludeStart); setPreviewExcludeEnd(configuration.general.previewExcludeEnd);
setPreviewExcludeEnd(conf.general.previewExcludeEnd); setPreviewPreset(configuration.general.previewPreset);
setPreviewPreset(conf.general.previewPreset);
} }
}, [data]); }, [configuration]);
async function onGenerate() { async function onGenerate() {
try { try {
@@ -90,15 +87,6 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
} }
} }
if (error) {
Toast.error(error);
props.onClose();
}
if (loading) {
return <></>;
}
return ( return (
<Modal <Modal
show show

View File

@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
import { DurationInput, LoadingIndicator } from "src/components/Shared"; import { DurationInput, LoadingIndicator } from "src/components/Shared";
import { useConfiguration, useConfigureInterface } from "src/core/StashService"; import { useConfiguration, useConfigureInterface } from "src/core/StashService";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import * as GQL from "src/core/generated-graphql";
import { CheckboxGroup } from "./CheckboxGroup"; import { CheckboxGroup } from "./CheckboxGroup";
const allMenuItems = [ const allMenuItems = [
@@ -38,6 +39,10 @@ export const SettingsInterfacePanel: React.FC = () => {
const [language, setLanguage] = useState<string>("en"); const [language, setLanguage] = useState<string>("en");
const [handyKey, setHandyKey] = useState<string>(); const [handyKey, setHandyKey] = useState<string>();
const [funscriptOffset, setFunscriptOffset] = useState<number>(0); const [funscriptOffset, setFunscriptOffset] = useState<number>(0);
const [
disableDropdownCreate,
setDisableDropdownCreate,
] = useState<GQL.ConfigDisableDropdownCreateInput>({});
const [updateInterfaceConfig] = useConfigureInterface({ const [updateInterfaceConfig] = useConfigureInterface({
menuItems: menuItemIds, menuItems: menuItemIds,
@@ -53,6 +58,7 @@ export const SettingsInterfacePanel: React.FC = () => {
slideshowDelay, slideshowDelay,
handyKey, handyKey,
funscriptOffset, funscriptOffset,
disableDropdownCreate,
}); });
useEffect(() => { useEffect(() => {
@@ -70,6 +76,11 @@ export const SettingsInterfacePanel: React.FC = () => {
setSlideshowDelay(iCfg?.slideshowDelay ?? 5000); setSlideshowDelay(iCfg?.slideshowDelay ?? 5000);
setHandyKey(iCfg?.handyKey ?? ""); setHandyKey(iCfg?.handyKey ?? "");
setFunscriptOffset(iCfg?.funscriptOffset ?? 0); setFunscriptOffset(iCfg?.funscriptOffset ?? 0);
setDisableDropdownCreate({
performer: iCfg?.disabledDropdownCreate.performer,
studio: iCfg?.disabledDropdownCreate.studio,
tag: iCfg?.disabledDropdownCreate.tag,
});
}, [config]); }, [config]);
async function onSave() { async function onSave() {
@@ -257,6 +268,64 @@ export const SettingsInterfacePanel: React.FC = () => {
</Form.Text> </Form.Text>
</Form.Group> </Form.Group>
<Form.Group>
<h5>{intl.formatMessage({ id: "config.ui.editing.heading" })}</h5>
<Form.Group>
<h6>
{intl.formatMessage({
id: "config.ui.editing.disable_dropdown_create.heading",
})}
</h6>
<Form.Check
id="disableDropdownCreate_performer"
checked={disableDropdownCreate.performer ?? false}
label={intl.formatMessage({
id: "performer",
})}
onChange={() => {
setDisableDropdownCreate({
...disableDropdownCreate,
performer: !disableDropdownCreate.performer ?? true,
});
}}
/>
<Form.Check
id="disableDropdownCreate_studio"
checked={disableDropdownCreate.studio ?? false}
label={intl.formatMessage({
id: "studio",
})}
onChange={() => {
setDisableDropdownCreate({
...disableDropdownCreate,
studio: !disableDropdownCreate.studio ?? true,
});
}}
/>
<Form.Check
id="disableDropdownCreate_tag"
checked={disableDropdownCreate.tag ?? false}
label={intl.formatMessage({
id: "tag",
})}
onChange={() => {
setDisableDropdownCreate({
...disableDropdownCreate,
tag: !disableDropdownCreate.tag ?? true,
});
}}
/>
<Form.Text className="text-muted">
{intl.formatMessage({
id: "config.ui.editing.disable_dropdown_create.description",
})}
</Form.Text>
</Form.Group>
</Form.Group>
<Form.Group> <Form.Group>
<h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5> <h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5>
<Form.Check <Form.Check

View File

@@ -1,9 +1,9 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Col, Form, Row } from "react-bootstrap"; import { Button, Col, Form, Row } from "react-bootstrap";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useConfiguration } from "src/core/StashService";
import { Icon, Modal } from "src/components/Shared"; import { Icon, Modal } from "src/components/Shared";
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
import { ConfigurationContext } from "src/hooks/Config";
interface IDirectorySelectionDialogProps { interface IDirectorySelectionDialogProps {
onClose: (paths?: string[]) => void; onClose: (paths?: string[]) => void;
@@ -13,9 +13,9 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
props: IDirectorySelectionDialogProps props: IDirectorySelectionDialogProps
) => { ) => {
const intl = useIntl(); const intl = useIntl();
const { data } = useConfiguration(); const { configuration } = React.useContext(ConfigurationContext);
const libraryPaths = data?.configuration.general.stashes.map((s) => s.path); const libraryPaths = configuration?.general.stashes.map((s) => s.path);
const [paths, setPaths] = useState<string[]>([]); const [paths, setPaths] = useState<string[]>([]);
const [currentDirectory, setCurrentDirectory] = useState<string>(""); const [currentDirectory, setCurrentDirectory] = useState<string>("");

View File

@@ -24,6 +24,7 @@ import {
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { SelectComponents } from "react-select/src/components"; import { SelectComponents } from "react-select/src/components";
import { ConfigurationContext } from "src/hooks/Config";
export type ValidTypes = export type ValidTypes =
| GQL.SlimPerformerDataFragment | GQL.SlimPerformerDataFragment
@@ -400,6 +401,10 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
const { data, loading } = useAllPerformersForFilter(); const { data, loading } = useAllPerformersForFilter();
const [createPerformer] = usePerformerCreate(); const [createPerformer] = usePerformerCreate();
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disabledDropdownCreate.performer ?? true;
const performers = data?.allPerformers ?? []; const performers = data?.allPerformers ?? [];
const onCreate = async (name: string) => { const onCreate = async (name: string) => {
@@ -416,7 +421,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
<FilterSelectComponent <FilterSelectComponent
{...props} {...props}
isMulti={props.isMulti ?? false} isMulti={props.isMulti ?? false}
creatable={props.creatable ?? true} creatable={props.creatable ?? defaultCreatable}
onCreate={onCreate} onCreate={onCreate}
type="performers" type="performers"
isLoading={loading} isLoading={loading}
@@ -436,6 +441,10 @@ export const StudioSelect: React.FC<
const { data, loading } = useAllStudiosForFilter(); const { data, loading } = useAllStudiosForFilter();
const [createStudio] = useStudioCreate(); const [createStudio] = useStudioCreate();
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disabledDropdownCreate.studio ?? true;
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]); const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
const studios = useMemo( const studios = useMemo(
() => () =>
@@ -542,7 +551,7 @@ export const StudioSelect: React.FC<
isLoading={loading} isLoading={loading}
items={studios} items={studios}
placeholder={props.noSelectionString ?? "Select studio..."} placeholder={props.noSelectionString ?? "Select studio..."}
creatable={props.creatable ?? true} creatable={props.creatable ?? defaultCreatable}
onCreate={onCreate} onCreate={onCreate}
/> />
); );
@@ -573,6 +582,10 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
const [createTag] = useTagCreate(); const [createTag] = useTagCreate();
const placeholder = props.noSelectionString ?? "Select tags..."; const placeholder = props.noSelectionString ?? "Select tags...";
const { configuration } = React.useContext(ConfigurationContext);
const defaultCreatable =
!configuration?.interface.disabledDropdownCreate.tag ?? true;
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]); const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
const tags = useMemo( const tags = useMemo(
() => (data?.allTags ?? []).filter((tag) => !exclude.includes(tag.id)), () => (data?.allTags ?? []).filter((tag) => !exclude.includes(tag.id)),
@@ -675,7 +688,7 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
components={{ Option: TagOption }} components={{ Option: TagOption }}
isMulti={props.isMulti ?? false} isMulti={props.isMulti ?? false}
items={tags} items={tags}
creatable={props.creatable ?? true} creatable={props.creatable ?? defaultCreatable}
type="tags" type="tags"
placeholder={placeholder} placeholder={placeholder}
isLoading={loading} isLoading={loading}

View File

@@ -6,10 +6,11 @@ import { useLocalForage } from "src/hooks";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator } from "src/components/Shared";
import { stashBoxSceneQuery, useConfiguration } from "src/core/StashService"; import { stashBoxSceneQuery } from "src/core/StashService";
import { Manual } from "src/components/Help/Manual"; import { Manual } from "src/components/Help/Manual";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config";
import Config from "./Config"; import Config from "./Config";
import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "./constants"; import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "./constants";
import { TaggerList } from "./TaggerList"; import { TaggerList } from "./TaggerList";
@@ -20,7 +21,7 @@ interface ITaggerProps {
} }
export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => { export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
const stashConfig = useConfiguration(); const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>( const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
LOCAL_FORAGE_KEY, LOCAL_FORAGE_KEY,
initialConfig initialConfig
@@ -63,20 +64,19 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
if (!config) return <LoadingIndicator />; if (!config) return <LoadingIndicator />;
const savedEndpointIndex = const savedEndpointIndex =
stashConfig.data?.configuration.general.stashBoxes.findIndex( stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === config.selectedEndpoint (s) => s.endpoint === config.selectedEndpoint
) ?? -1; ) ?? -1;
const selectedEndpointIndex = const selectedEndpointIndex =
savedEndpointIndex === -1 && savedEndpointIndex === -1 && stashConfig?.general.stashBoxes.length
stashConfig.data?.configuration.general.stashBoxes.length
? 0 ? 0
: savedEndpointIndex; : savedEndpointIndex;
const selectedEndpoint = const selectedEndpoint =
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex]; stashConfig?.general.stashBoxes[selectedEndpointIndex];
function getEndpointIndex(endpoint: string) { function getEndpointIndex(endpoint: string) {
return ( return (
stashConfig.data?.configuration.general.stashBoxes.findIndex( stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === endpoint (s) => s.endpoint === endpoint
) ?? -1 ) ?? -1
); );

View File

@@ -1,6 +1,6 @@
import React, { Dispatch, useState } from "react"; import React, { Dispatch, useState } from "react";
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
import { useConfiguration } from "src/core/StashService"; import { ConfigurationContext } from "src/hooks/Config";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { ITaggerConfig, PERFORMER_FIELDS } from "../constants"; import { ITaggerConfig, PERFORMER_FIELDS } from "../constants";
@@ -13,7 +13,7 @@ interface IConfigProps {
} }
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => { const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
const stashConfig = useConfiguration(); const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [showExclusionModal, setShowExclusionModal] = useState(false); const [showExclusionModal, setShowExclusionModal] = useState(false);
const excludedFields = config.excludedPerformerFields ?? []; const excludedFields = config.excludedPerformerFields ?? [];
@@ -26,7 +26,7 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
}); });
}; };
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? []; const stashBoxes = stashConfig?.general.stashBoxes ?? [];
const handleFieldSelect = (fields: string[]) => { const handleFieldSelect = (fields: string[]) => {
setConfig({ ...config, excludedPerformerFields: fields }); setConfig({ ...config, excludedPerformerFields: fields });
@@ -77,13 +77,11 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
onChange={handleInstanceSelect} onChange={handleInstanceSelect}
> >
{!stashBoxes.length && <option>No instances found</option>} {!stashBoxes.length && <option>No instances found</option>}
{stashConfig.data?.configuration.general.stashBoxes.map( {stashConfig?.general.stashBoxes.map((i) => (
(i) => ( <option value={i.endpoint} key={i.endpoint}>
<option value={i.endpoint} key={i.endpoint}> {i.endpoint}
{i.endpoint} </option>
</option> ))}
)
)}
</Form.Control> </Form.Control>
</Form.Group> </Form.Group>
</div> </div>

View File

@@ -9,11 +9,11 @@ import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator, Modal } from "src/components/Shared"; import { LoadingIndicator, Modal } from "src/components/Shared";
import { import {
stashBoxPerformerQuery, stashBoxPerformerQuery,
useConfiguration,
useJobsSubscribe, useJobsSubscribe,
mutateStashBoxBatchPerformerTag, mutateStashBoxBatchPerformerTag,
} from "src/core/StashService"; } from "src/core/StashService";
import { Manual } from "src/components/Help/Manual"; import { Manual } from "src/components/Help/Manual";
import { ConfigurationContext } from "src/hooks/Config";
import StashSearchResult from "./StashSearchResult"; import StashSearchResult from "./StashSearchResult";
import PerformerConfig from "./Config"; import PerformerConfig from "./Config";
@@ -491,7 +491,7 @@ interface ITaggerProps {
export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => { export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
const jobsSubscribe = useJobsSubscribe(); const jobsSubscribe = useJobsSubscribe();
const stashConfig = useConfiguration(); const { configuration: stashConfig } = React.useContext(ConfigurationContext);
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>( const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
LOCAL_FORAGE_KEY, LOCAL_FORAGE_KEY,
initialConfig initialConfig
@@ -524,16 +524,15 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
if (!config) return <LoadingIndicator />; if (!config) return <LoadingIndicator />;
const savedEndpointIndex = const savedEndpointIndex =
stashConfig.data?.configuration.general.stashBoxes.findIndex( stashConfig?.general.stashBoxes.findIndex(
(s) => s.endpoint === config.selectedEndpoint (s) => s.endpoint === config.selectedEndpoint
) ?? -1; ) ?? -1;
const selectedEndpointIndex = const selectedEndpointIndex =
savedEndpointIndex === -1 && savedEndpointIndex === -1 && stashConfig?.general.stashBoxes.length
stashConfig.data?.configuration.general.stashBoxes.length
? 0 ? 0
: savedEndpointIndex; : savedEndpointIndex;
const selectedEndpoint = const selectedEndpoint =
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex]; stashConfig?.general.stashBoxes[selectedEndpointIndex];
async function batchAdd(performerInput: string) { async function batchAdd(performerInput: string) {
if (performerInput && selectedEndpoint) { if (performerInput && selectedEndpoint) {
@@ -641,7 +640,7 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
}} }}
isIdle={batchJobID === undefined} isIdle={batchJobID === undefined}
config={config} config={config}
stashBoxes={stashConfig.data?.configuration.general.stashBoxes} stashBoxes={stashConfig?.general.stashBoxes}
onBatchAdd={batchAdd} onBatchAdd={batchAdd}
onBatchUpdate={batchUpdate} onBatchUpdate={batchUpdate}
/> />

View File

@@ -1,10 +1,10 @@
import React, { useRef, useState, useEffect } from "react"; import React, { useRef, useState, useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService";
import { TextUtils, NavUtils } from "src/utils"; import { TextUtils, NavUtils } from "src/utils";
import cx from "classnames"; import cx from "classnames";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config";
interface IWallItemProps { interface IWallItemProps {
index?: number; index?: number;
@@ -105,10 +105,9 @@ const Preview: React.FC<{
export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => { export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const wallItem = useRef() as React.MutableRefObject<HTMLDivElement>; const wallItem = useRef() as React.MutableRefObject<HTMLDivElement>;
const config = useConfiguration(); const { configuration: config } = React.useContext(ConfigurationContext);
const showTextContainer = const showTextContainer = config?.interface.wallShowTitle ?? true;
config.data?.configuration.interface.wallShowTitle ?? true;
const previews = props.sceneMarker const previews = props.sceneMarker
? { ? {
@@ -203,11 +202,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
<div className="wall-item"> <div className="wall-item">
<div className={`wall-item-container ${props.className}`} ref={wallItem}> <div className={`wall-item-container ${props.className}`} ref={wallItem}>
<Link onClick={clickHandler} to={linkSrc} className="wall-item-anchor"> <Link onClick={clickHandler} to={linkSrc} className="wall-item-anchor">
<Preview <Preview previews={previews} config={config} active={active} />
previews={previews}
config={config.data?.configuration}
active={active}
/>
{renderText()} {renderText()}
</Link> </Link>
</div> </div>

View File

@@ -0,0 +1,28 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
interface IContext {
configuration?: GQL.ConfigDataFragment;
loading?: boolean;
}
export const ConfigurationContext = React.createContext<IContext>({});
export const ConfigurationProvider: React.FC<IContext> = ({
loading,
configuration,
children,
}) => {
return (
<ConfigurationContext.Provider
value={{
configuration,
loading,
}}
>
{children}
</ConfigurationContext.Provider>
);
};
export default ConfigurationProvider;

View File

@@ -15,9 +15,9 @@ import debounce from "lodash/debounce";
import { Icon, LoadingIndicator } from "src/components/Shared"; import { Icon, LoadingIndicator } from "src/components/Shared";
import { useInterval, usePageVisibility } from "src/hooks"; import { useInterval, usePageVisibility } from "src/hooks";
import { useConfiguration } from "src/core/StashService";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { DisplayMode, LightboxImage, ScrollMode } from "./LightboxImage"; import { DisplayMode, LightboxImage, ScrollMode } from "./LightboxImage";
import { ConfigurationContext } from "../Config";
const CLASSNAME = "Lightbox"; const CLASSNAME = "Lightbox";
const CLASSNAME_HEADER = `${CLASSNAME}-header`; const CLASSNAME_HEADER = `${CLASSNAME}-header`;
@@ -93,11 +93,10 @@ export const LightboxComponent: React.FC<IProps> = ({
const allowNavigation = images.length > 1 || pageCallback; const allowNavigation = images.length > 1 || pageCallback;
const intl = useIntl(); const intl = useIntl();
const config = useConfiguration(); const { configuration: config } = React.useContext(ConfigurationContext);
const userSelectedSlideshowDelayOrDefault = const userSelectedSlideshowDelayOrDefault =
config?.data?.configuration.interface.slideshowDelay ?? config?.interface.slideshowDelay ?? DEFAULT_SLIDESHOW_DELAY;
DEFAULT_SLIDESHOW_DELAY;
// slideshowInterval is used for controlling the logic // slideshowInterval is used for controlling the logic
// displaySlideshowInterval is for display purposes only // displaySlideshowInterval is for display purposes only

View File

@@ -327,6 +327,13 @@
"heading": "Custom CSS", "heading": "Custom CSS",
"option_label": "Custom CSS enabled" "option_label": "Custom CSS enabled"
}, },
"editing": {
"disable_dropdown_create": {
"heading": "Disable dropdown create",
"description": "Remove the ability to create new objects from the dropdown selectors"
},
"heading": "Editing"
},
"handy_connection_key": { "handy_connection_key": {
"description": "Handy connection key to use for interactive scenes.", "description": "Handy connection key to use for interactive scenes.",
"heading": "Handy Connection Key" "heading": "Handy Connection Key"