mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
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:
@@ -58,6 +58,11 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
cssEnabled
|
||||
language
|
||||
slideshowDelay
|
||||
disabledDropdownCreate {
|
||||
performer
|
||||
tag
|
||||
studio
|
||||
}
|
||||
handyKey
|
||||
funscriptOffset
|
||||
}
|
||||
|
||||
@@ -188,6 +188,12 @@ type ConfigGeneralResult {
|
||||
stashBoxes: [StashBox!]!
|
||||
}
|
||||
|
||||
input ConfigDisableDropdownCreateInput {
|
||||
performer: Boolean
|
||||
tag: Boolean
|
||||
studio: Boolean
|
||||
}
|
||||
|
||||
input ConfigInterfaceInput {
|
||||
"""Ordered list of items that should be shown in the menu"""
|
||||
menuItems: [String!]
|
||||
@@ -210,12 +216,20 @@ input ConfigInterfaceInput {
|
||||
language: String
|
||||
"""Slideshow Delay"""
|
||||
slideshowDelay: Int
|
||||
"""Set to true to disable creating new objects via the dropdown menus"""
|
||||
disableDropdownCreate: ConfigDisableDropdownCreateInput
|
||||
"""Handy Connection Key"""
|
||||
handyKey: String
|
||||
"""Funscript Time Offset"""
|
||||
funscriptOffset: Int
|
||||
}
|
||||
|
||||
type ConfigDisableDropdownCreate {
|
||||
performer: Boolean!
|
||||
tag: Boolean!
|
||||
studio: Boolean!
|
||||
}
|
||||
|
||||
type ConfigInterfaceResult {
|
||||
"""Ordered list of items that should be shown in the menu"""
|
||||
menuItems: [String!]
|
||||
@@ -238,6 +252,8 @@ type ConfigInterfaceResult {
|
||||
language: String
|
||||
"""Slideshow Delay"""
|
||||
slideshowDelay: Int
|
||||
"""Fields are true if creating via dropdown menus are disabled"""
|
||||
disabledDropdownCreate: ConfigDisableDropdownCreate!
|
||||
"""Handy Connection Key"""
|
||||
handyKey: String
|
||||
"""Funscript Time Offset"""
|
||||
|
||||
@@ -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) {
|
||||
c := config.GetInstance()
|
||||
|
||||
setBool := func(key string, v *bool) {
|
||||
if v != nil {
|
||||
c.Set(key, *v)
|
||||
}
|
||||
}
|
||||
|
||||
if input.MenuItems != nil {
|
||||
c.Set(config.MenuItems, input.MenuItems)
|
||||
}
|
||||
|
||||
if input.SoundOnPreview != nil {
|
||||
c.Set(config.SoundOnPreview, *input.SoundOnPreview)
|
||||
}
|
||||
|
||||
if input.WallShowTitle != nil {
|
||||
c.Set(config.WallShowTitle, *input.WallShowTitle)
|
||||
}
|
||||
setBool(config.SoundOnPreview, input.SoundOnPreview)
|
||||
setBool(config.WallShowTitle, input.WallShowTitle)
|
||||
|
||||
if input.WallPlayback != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
if input.AutostartVideo != nil {
|
||||
c.Set(config.AutostartVideo, *input.AutostartVideo)
|
||||
}
|
||||
|
||||
if input.ShowStudioAsText != nil {
|
||||
c.Set(config.ShowStudioAsText, *input.ShowStudioAsText)
|
||||
}
|
||||
setBool(config.AutostartVideo, input.AutostartVideo)
|
||||
setBool(config.ShowStudioAsText, input.ShowStudioAsText)
|
||||
|
||||
if input.Language != nil {
|
||||
c.Set(config.Language, *input.Language)
|
||||
@@ -269,8 +266,13 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input models.
|
||||
|
||||
c.SetCSS(css)
|
||||
|
||||
if input.CSSEnabled != nil {
|
||||
c.Set(config.CSSEnabled, *input.CSSEnabled)
|
||||
setBool(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 {
|
||||
|
||||
@@ -115,19 +115,20 @@ func makeConfigInterfaceResult() *models.ConfigInterfaceResult {
|
||||
scriptOffset := config.GetFunscriptOffset()
|
||||
|
||||
return &models.ConfigInterfaceResult{
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
WallPlayback: &wallPlayback,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
AutostartVideo: &autostartVideo,
|
||||
ShowStudioAsText: &showStudioAsText,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Language: &language,
|
||||
SlideshowDelay: &slideshowDelay,
|
||||
HandyKey: &handyKey,
|
||||
FunscriptOffset: &scriptOffset,
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
WallPlayback: &wallPlayback,
|
||||
MaximumLoopDuration: &maximumLoopDuration,
|
||||
AutostartVideo: &autostartVideo,
|
||||
ShowStudioAsText: &showStudioAsText,
|
||||
CSS: &css,
|
||||
CSSEnabled: &cssEnabled,
|
||||
Language: &language,
|
||||
SlideshowDelay: &slideshowDelay,
|
||||
DisabledDropdownCreate: config.GetDisableDropdownCreate(),
|
||||
HandyKey: &handyKey,
|
||||
FunscriptOffset: &scriptOffset,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,6 +135,13 @@ const ShowStudioAsText = "show_studio_as_text"
|
||||
const CSSEnabled = "cssEnabled"
|
||||
const WallPlayback = "wall_playback"
|
||||
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 FunscriptOffset = "funscript_offset"
|
||||
|
||||
@@ -787,6 +794,17 @@ func (i *Instance) GetSlideshowDelay() int {
|
||||
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 {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
|
||||
@@ -31,6 +31,7 @@ import { Setup } from "./components/Setup/Setup";
|
||||
import { Migrate } from "./components/Setup/Migrate";
|
||||
import * as GQL from "./core/generated-graphql";
|
||||
import { LoadingIndicator } from "./components/Shared";
|
||||
import ConfigurationProvider from "./hooks/Config";
|
||||
|
||||
initPolyfills();
|
||||
|
||||
@@ -138,12 +139,17 @@ export const App: React.FC = () => {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<IntlProvider locale={language} messages={messages} formats={intlFormats}>
|
||||
<ToastProvider>
|
||||
<LightboxProvider>
|
||||
{maybeRenderNavbar()}
|
||||
<div className="main container-fluid">{renderContent()}</div>
|
||||
</LightboxProvider>
|
||||
</ToastProvider>
|
||||
<ConfigurationProvider
|
||||
configuration={config.data?.configuration}
|
||||
loading={config.loading}
|
||||
>
|
||||
<ToastProvider>
|
||||
<LightboxProvider>
|
||||
{maybeRenderNavbar()}
|
||||
<div className="main container-fluid">{renderContent()}</div>
|
||||
</LightboxProvider>
|
||||
</ToastProvider>
|
||||
</ConfigurationProvider>
|
||||
</IntlProvider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
* Fix huge memory usage spike during clean task. ([#1805](https://github.com/stashapp/stash/pull/1805))
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import {
|
||||
GridCard,
|
||||
HoverPopover,
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
} from "src/components/Shared";
|
||||
import { PopoverCountButton } from "src/components/Shared/PopoverCountButton";
|
||||
import { NavUtils, TextUtils } from "src/utils";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
|
||||
@@ -24,9 +24,8 @@ interface IProps {
|
||||
}
|
||||
|
||||
export const GalleryCard: React.FC<IProps> = (props) => {
|
||||
const config = useConfiguration();
|
||||
const showStudioAsText =
|
||||
config?.data?.configuration.interface.showStudioAsText ?? false;
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const showStudioAsText = configuration?.interface.showStudioAsText ?? false;
|
||||
|
||||
function maybeRenderScenePopoverButton() {
|
||||
if (props.gallery.scenes.length === 0) return;
|
||||
|
||||
@@ -13,8 +13,8 @@ import Mousetrap from "mousetrap";
|
||||
|
||||
import { SessionUtils } from "src/utils";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { Manual } from "./Help/Manual";
|
||||
import { useConfiguration } from "../core/StashService";
|
||||
|
||||
interface IMenuItem {
|
||||
name: string;
|
||||
@@ -120,7 +120,7 @@ const allMenuItems: IMenuItem[] = [
|
||||
export const MainNavbar: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { data: config, loading } = useConfiguration();
|
||||
const { configuration, loading } = React.useContext(ConfigurationContext);
|
||||
|
||||
// Show all menu items by default, unless config says otherwise
|
||||
const [menuItems, setMenuItems] = useState<IMenuItem[]>(allMenuItems);
|
||||
@@ -129,7 +129,7 @@ export const MainNavbar: React.FC = () => {
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const iCfg = config?.configuration?.interface;
|
||||
const iCfg = configuration?.interface;
|
||||
if (iCfg?.menuItems) {
|
||||
setMenuItems(
|
||||
allMenuItems.filter((menuItem) =>
|
||||
@@ -137,7 +137,7 @@ export const MainNavbar: React.FC = () => {
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [config]);
|
||||
}, [configuration]);
|
||||
|
||||
// react-bootstrap typing bug
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
usePerformerCreate,
|
||||
useTagCreate,
|
||||
queryScrapePerformerURL,
|
||||
useConfiguration,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
Icon,
|
||||
@@ -41,6 +40,7 @@ import {
|
||||
genderToString,
|
||||
stringToGender,
|
||||
} from "src/utils/gender";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
||||
@@ -88,7 +88,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
const [scrapedPerformer, setScrapedPerformer] = useState<
|
||||
GQL.ScrapedPerformer | undefined
|
||||
>();
|
||||
const stashConfig = useConfiguration();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
@@ -601,7 +601,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||
if (!performer) {
|
||||
return;
|
||||
}
|
||||
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
|
||||
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
|
||||
|
||||
const popover = (
|
||||
<Dropdown.Menu id="performer-scraper-popover">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import React from "react";
|
||||
import ReactJWPlayer from "react-jw-player";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { JWUtils, ScreenUtils } from "src/utils";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||
import { Interactive } from "../../utils/interactive";
|
||||
|
||||
@@ -366,16 +366,12 @@ export class ScenePlayerImpl extends React.Component<
|
||||
export const ScenePlayer: React.FC<IScenePlayerProps> = (
|
||||
props: IScenePlayerProps
|
||||
) => {
|
||||
const config = useConfiguration();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
return (
|
||||
<ScenePlayerImpl
|
||||
{...props}
|
||||
config={
|
||||
config.data && config.data.configuration
|
||||
? config.data.configuration.interface
|
||||
: undefined
|
||||
}
|
||||
config={configuration ? configuration.interface : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@ import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { Link } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import {
|
||||
Icon,
|
||||
TagLink,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
} from "src/components/Shared";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
import { GridCard } from "../Shared/GridCard";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
@@ -80,15 +80,14 @@ interface ISceneCardProps {
|
||||
export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
props: ISceneCardProps
|
||||
) => {
|
||||
const config = useConfiguration();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
// studio image is missing if it uses the default
|
||||
const missingStudioImage = props.scene.studio?.image_path?.endsWith(
|
||||
"?default=true"
|
||||
);
|
||||
const showStudioAsText =
|
||||
missingStudioImage ||
|
||||
(config?.data?.configuration.interface.showStudioAsText ?? false);
|
||||
missingStudioImage || (configuration?.interface.showStudioAsText ?? false);
|
||||
|
||||
function maybeRenderSceneSpecsOverlay() {
|
||||
return (
|
||||
@@ -327,9 +326,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
||||
image={props.scene.paths.screenshot ?? undefined}
|
||||
video={props.scene.paths.preview ?? undefined}
|
||||
isPortrait={isPortrait()}
|
||||
soundActive={
|
||||
config.data?.configuration?.interface?.soundOnPreview ?? false
|
||||
}
|
||||
soundActive={configuration?.interface?.soundOnPreview ?? false}
|
||||
/>
|
||||
<RatingBanner rating={props.scene.rating} />
|
||||
{maybeRenderSceneSpecsOverlay()}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
useListSceneScrapers,
|
||||
useSceneUpdate,
|
||||
mutateReloadScrapers,
|
||||
useConfiguration,
|
||||
queryScrapeSceneQueryFragment,
|
||||
} from "src/core/StashService";
|
||||
import {
|
||||
@@ -35,6 +34,7 @@ import { ImageUtils, FormUtils, TextUtils, getStashIDs } from "src/utils";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { SceneMovieTable } from "./SceneMovieTable";
|
||||
import { RatingStars } from "./RatingStars";
|
||||
import { SceneScrapeDialog } from "./SceneScrapeDialog";
|
||||
@@ -76,7 +76,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
string | undefined
|
||||
>(scene.paths.screenshot ?? undefined);
|
||||
|
||||
const stashConfig = useConfiguration();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -380,7 +380,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
function renderScrapeQueryMenu() {
|
||||
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
|
||||
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
|
||||
|
||||
if (stashBoxes.length === 0 && queryableScrapers.length === 0) return;
|
||||
|
||||
@@ -450,7 +450,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
};
|
||||
|
||||
function renderScraperMenu() {
|
||||
const stashBoxes = stashConfig.data?.configuration.general.stashBoxes ?? [];
|
||||
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Form, Button, Collapse } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataGenerate,
|
||||
useConfiguration,
|
||||
} from "src/core/StashService";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { Modal, Icon } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useIntl } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface ISceneGenerateDialogProps {
|
||||
selectedIds: string[];
|
||||
@@ -17,7 +15,7 @@ interface ISceneGenerateDialogProps {
|
||||
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
props: ISceneGenerateDialogProps
|
||||
) => {
|
||||
const { data, error, loading } = useConfiguration();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
const [sprites, setSprites] = useState(true);
|
||||
const [phashes, setPhashes] = useState(true);
|
||||
@@ -49,17 +47,16 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
const Toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.configuration) return;
|
||||
if (!configuration) return;
|
||||
|
||||
const conf = data.configuration;
|
||||
if (conf.general) {
|
||||
setPreviewSegments(conf.general.previewSegments);
|
||||
setPreviewSegmentDuration(conf.general.previewSegmentDuration);
|
||||
setPreviewExcludeStart(conf.general.previewExcludeStart);
|
||||
setPreviewExcludeEnd(conf.general.previewExcludeEnd);
|
||||
setPreviewPreset(conf.general.previewPreset);
|
||||
if (configuration.general) {
|
||||
setPreviewSegments(configuration.general.previewSegments);
|
||||
setPreviewSegmentDuration(configuration.general.previewSegmentDuration);
|
||||
setPreviewExcludeStart(configuration.general.previewExcludeStart);
|
||||
setPreviewExcludeEnd(configuration.general.previewExcludeEnd);
|
||||
setPreviewPreset(configuration.general.previewPreset);
|
||||
}
|
||||
}, [data]);
|
||||
}, [configuration]);
|
||||
|
||||
async function onGenerate() {
|
||||
try {
|
||||
@@ -90,15 +87,6 @@ export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
}
|
||||
}
|
||||
|
||||
if (error) {
|
||||
Toast.error(error);
|
||||
props.onClose();
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useIntl } from "react-intl";
|
||||
import { DurationInput, LoadingIndicator } from "src/components/Shared";
|
||||
import { useConfiguration, useConfigureInterface } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { CheckboxGroup } from "./CheckboxGroup";
|
||||
|
||||
const allMenuItems = [
|
||||
@@ -38,6 +39,10 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
const [language, setLanguage] = useState<string>("en");
|
||||
const [handyKey, setHandyKey] = useState<string>();
|
||||
const [funscriptOffset, setFunscriptOffset] = useState<number>(0);
|
||||
const [
|
||||
disableDropdownCreate,
|
||||
setDisableDropdownCreate,
|
||||
] = useState<GQL.ConfigDisableDropdownCreateInput>({});
|
||||
|
||||
const [updateInterfaceConfig] = useConfigureInterface({
|
||||
menuItems: menuItemIds,
|
||||
@@ -53,6 +58,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
slideshowDelay,
|
||||
handyKey,
|
||||
funscriptOffset,
|
||||
disableDropdownCreate,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -70,6 +76,11 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
setSlideshowDelay(iCfg?.slideshowDelay ?? 5000);
|
||||
setHandyKey(iCfg?.handyKey ?? "");
|
||||
setFunscriptOffset(iCfg?.funscriptOffset ?? 0);
|
||||
setDisableDropdownCreate({
|
||||
performer: iCfg?.disabledDropdownCreate.performer,
|
||||
studio: iCfg?.disabledDropdownCreate.studio,
|
||||
tag: iCfg?.disabledDropdownCreate.tag,
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
async function onSave() {
|
||||
@@ -257,6 +268,64 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
</Form.Text>
|
||||
</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>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5>
|
||||
<Form.Check
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Col, Form, Row } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { Icon, Modal } from "src/components/Shared";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IDirectorySelectionDialogProps {
|
||||
onClose: (paths?: string[]) => void;
|
||||
@@ -13,9 +13,9 @@ export const DirectorySelectionDialog: React.FC<IDirectorySelectionDialogProps>
|
||||
props: IDirectorySelectionDialogProps
|
||||
) => {
|
||||
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 [currentDirectory, setCurrentDirectory] = useState<string>("");
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { useToast } from "src/hooks";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { SelectComponents } from "react-select/src/components";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
export type ValidTypes =
|
||||
| GQL.SlimPerformerDataFragment
|
||||
@@ -400,6 +401,10 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
const { data, loading } = useAllPerformersForFilter();
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const defaultCreatable =
|
||||
!configuration?.interface.disabledDropdownCreate.performer ?? true;
|
||||
|
||||
const performers = data?.allPerformers ?? [];
|
||||
|
||||
const onCreate = async (name: string) => {
|
||||
@@ -416,7 +421,7 @@ export const PerformerSelect: React.FC<IFilterProps> = (props) => {
|
||||
<FilterSelectComponent
|
||||
{...props}
|
||||
isMulti={props.isMulti ?? false}
|
||||
creatable={props.creatable ?? true}
|
||||
creatable={props.creatable ?? defaultCreatable}
|
||||
onCreate={onCreate}
|
||||
type="performers"
|
||||
isLoading={loading}
|
||||
@@ -436,6 +441,10 @@ export const StudioSelect: React.FC<
|
||||
const { data, loading } = useAllStudiosForFilter();
|
||||
const [createStudio] = useStudioCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const defaultCreatable =
|
||||
!configuration?.interface.disabledDropdownCreate.studio ?? true;
|
||||
|
||||
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
||||
const studios = useMemo(
|
||||
() =>
|
||||
@@ -542,7 +551,7 @@ export const StudioSelect: React.FC<
|
||||
isLoading={loading}
|
||||
items={studios}
|
||||
placeholder={props.noSelectionString ?? "Select studio..."}
|
||||
creatable={props.creatable ?? true}
|
||||
creatable={props.creatable ?? defaultCreatable}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
);
|
||||
@@ -573,6 +582,10 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
|
||||
const [createTag] = useTagCreate();
|
||||
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 tags = useMemo(
|
||||
() => (data?.allTags ?? []).filter((tag) => !exclude.includes(tag.id)),
|
||||
@@ -675,7 +688,7 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
|
||||
components={{ Option: TagOption }}
|
||||
isMulti={props.isMulti ?? false}
|
||||
items={tags}
|
||||
creatable={props.creatable ?? true}
|
||||
creatable={props.creatable ?? defaultCreatable}
|
||||
type="tags"
|
||||
placeholder={placeholder}
|
||||
isLoading={loading}
|
||||
|
||||
@@ -6,10 +6,11 @@ import { useLocalForage } from "src/hooks";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
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 { SceneQueue } from "src/models/sceneQueue";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import Config from "./Config";
|
||||
import { LOCAL_FORAGE_KEY, ITaggerConfig, initialConfig } from "./constants";
|
||||
import { TaggerList } from "./TaggerList";
|
||||
@@ -20,7 +21,7 @@ interface ITaggerProps {
|
||||
}
|
||||
|
||||
export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
||||
const stashConfig = useConfiguration();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
|
||||
LOCAL_FORAGE_KEY,
|
||||
initialConfig
|
||||
@@ -63,20 +64,19 @@ export const Tagger: React.FC<ITaggerProps> = ({ scenes, queue }) => {
|
||||
if (!config) return <LoadingIndicator />;
|
||||
|
||||
const savedEndpointIndex =
|
||||
stashConfig.data?.configuration.general.stashBoxes.findIndex(
|
||||
stashConfig?.general.stashBoxes.findIndex(
|
||||
(s) => s.endpoint === config.selectedEndpoint
|
||||
) ?? -1;
|
||||
const selectedEndpointIndex =
|
||||
savedEndpointIndex === -1 &&
|
||||
stashConfig.data?.configuration.general.stashBoxes.length
|
||||
savedEndpointIndex === -1 && stashConfig?.general.stashBoxes.length
|
||||
? 0
|
||||
: savedEndpointIndex;
|
||||
const selectedEndpoint =
|
||||
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex];
|
||||
stashConfig?.general.stashBoxes[selectedEndpointIndex];
|
||||
|
||||
function getEndpointIndex(endpoint: string) {
|
||||
return (
|
||||
stashConfig.data?.configuration.general.stashBoxes.findIndex(
|
||||
stashConfig?.general.stashBoxes.findIndex(
|
||||
(s) => s.endpoint === endpoint
|
||||
) ?? -1
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Dispatch, useState } from "react";
|
||||
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 { ITaggerConfig, PERFORMER_FIELDS } from "../constants";
|
||||
@@ -13,7 +13,7 @@ interface IConfigProps {
|
||||
}
|
||||
|
||||
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
const stashConfig = useConfiguration();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const [showExclusionModal, setShowExclusionModal] = useState(false);
|
||||
|
||||
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[]) => {
|
||||
setConfig({ ...config, excludedPerformerFields: fields });
|
||||
@@ -77,13 +77,11 @@ const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
onChange={handleInstanceSelect}
|
||||
>
|
||||
{!stashBoxes.length && <option>No instances found</option>}
|
||||
{stashConfig.data?.configuration.general.stashBoxes.map(
|
||||
(i) => (
|
||||
<option value={i.endpoint} key={i.endpoint}>
|
||||
{i.endpoint}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
{stashConfig?.general.stashBoxes.map((i) => (
|
||||
<option value={i.endpoint} key={i.endpoint}>
|
||||
{i.endpoint}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,11 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { LoadingIndicator, Modal } from "src/components/Shared";
|
||||
import {
|
||||
stashBoxPerformerQuery,
|
||||
useConfiguration,
|
||||
useJobsSubscribe,
|
||||
mutateStashBoxBatchPerformerTag,
|
||||
} from "src/core/StashService";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
import StashSearchResult from "./StashSearchResult";
|
||||
import PerformerConfig from "./Config";
|
||||
@@ -491,7 +491,7 @@ interface ITaggerProps {
|
||||
|
||||
export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
|
||||
const jobsSubscribe = useJobsSubscribe();
|
||||
const stashConfig = useConfiguration();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const [{ data: config }, setConfig] = useLocalForage<ITaggerConfig>(
|
||||
LOCAL_FORAGE_KEY,
|
||||
initialConfig
|
||||
@@ -524,16 +524,15 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
|
||||
if (!config) return <LoadingIndicator />;
|
||||
|
||||
const savedEndpointIndex =
|
||||
stashConfig.data?.configuration.general.stashBoxes.findIndex(
|
||||
stashConfig?.general.stashBoxes.findIndex(
|
||||
(s) => s.endpoint === config.selectedEndpoint
|
||||
) ?? -1;
|
||||
const selectedEndpointIndex =
|
||||
savedEndpointIndex === -1 &&
|
||||
stashConfig.data?.configuration.general.stashBoxes.length
|
||||
savedEndpointIndex === -1 && stashConfig?.general.stashBoxes.length
|
||||
? 0
|
||||
: savedEndpointIndex;
|
||||
const selectedEndpoint =
|
||||
stashConfig.data?.configuration.general.stashBoxes[selectedEndpointIndex];
|
||||
stashConfig?.general.stashBoxes[selectedEndpointIndex];
|
||||
|
||||
async function batchAdd(performerInput: string) {
|
||||
if (performerInput && selectedEndpoint) {
|
||||
@@ -641,7 +640,7 @@ export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
|
||||
}}
|
||||
isIdle={batchJobID === undefined}
|
||||
config={config}
|
||||
stashBoxes={stashConfig.data?.configuration.general.stashBoxes}
|
||||
stashBoxes={stashConfig?.general.stashBoxes}
|
||||
onBatchAdd={batchAdd}
|
||||
onBatchUpdate={batchUpdate}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { TextUtils, NavUtils } from "src/utils";
|
||||
import cx from "classnames";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IWallItemProps {
|
||||
index?: number;
|
||||
@@ -105,10 +105,9 @@ const Preview: React.FC<{
|
||||
export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
|
||||
const [active, setActive] = useState(false);
|
||||
const wallItem = useRef() as React.MutableRefObject<HTMLDivElement>;
|
||||
const config = useConfiguration();
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
|
||||
const showTextContainer =
|
||||
config.data?.configuration.interface.wallShowTitle ?? true;
|
||||
const showTextContainer = config?.interface.wallShowTitle ?? true;
|
||||
|
||||
const previews = props.sceneMarker
|
||||
? {
|
||||
@@ -203,11 +202,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
|
||||
<div className="wall-item">
|
||||
<div className={`wall-item-container ${props.className}`} ref={wallItem}>
|
||||
<Link onClick={clickHandler} to={linkSrc} className="wall-item-anchor">
|
||||
<Preview
|
||||
previews={previews}
|
||||
config={config.data?.configuration}
|
||||
active={active}
|
||||
/>
|
||||
<Preview previews={previews} config={config} active={active} />
|
||||
{renderText()}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
28
ui/v2.5/src/hooks/Config.tsx
Normal file
28
ui/v2.5/src/hooks/Config.tsx
Normal 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;
|
||||
@@ -15,9 +15,9 @@ import debounce from "lodash/debounce";
|
||||
|
||||
import { Icon, LoadingIndicator } from "src/components/Shared";
|
||||
import { useInterval, usePageVisibility } from "src/hooks";
|
||||
import { useConfiguration } from "src/core/StashService";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { DisplayMode, LightboxImage, ScrollMode } from "./LightboxImage";
|
||||
import { ConfigurationContext } from "../Config";
|
||||
|
||||
const CLASSNAME = "Lightbox";
|
||||
const CLASSNAME_HEADER = `${CLASSNAME}-header`;
|
||||
@@ -93,11 +93,10 @@ export const LightboxComponent: React.FC<IProps> = ({
|
||||
const allowNavigation = images.length > 1 || pageCallback;
|
||||
|
||||
const intl = useIntl();
|
||||
const config = useConfiguration();
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
|
||||
const userSelectedSlideshowDelayOrDefault =
|
||||
config?.data?.configuration.interface.slideshowDelay ??
|
||||
DEFAULT_SLIDESHOW_DELAY;
|
||||
config?.interface.slideshowDelay ?? DEFAULT_SLIDESHOW_DELAY;
|
||||
|
||||
// slideshowInterval is used for controlling the logic
|
||||
// displaySlideshowInterval is for display purposes only
|
||||
|
||||
@@ -327,6 +327,13 @@
|
||||
"heading": "Custom CSS",
|
||||
"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": {
|
||||
"description": "Handy connection key to use for interactive scenes.",
|
||||
"heading": "Handy Connection Key"
|
||||
|
||||
Reference in New Issue
Block a user