mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Settings UI refactor (#2086)
* Full width settings page * Group settings * Make config fields optional * auto save on change * Add settings context * Refactor stash library section * Restructure settings * Refactor tasks page * Add collapse buttons for setting groups * Add collapse buttons in library * Add loading indicator * Simplify task options. Add details to manual * Add manual links to tasks page * Add help tooltips * Refactor about page * Refactor log page * Refactor tools panel * Refactor plugin page * Refactor task queue * Improve disabled styling
This commit is contained in:
@@ -1,174 +1,48 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { DurationInput, LoadingIndicator } from "src/components/Shared";
|
||||
import {
|
||||
useConfiguration,
|
||||
useConfigureDefaults,
|
||||
useConfigureInterface,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { CheckboxGroup } from "./CheckboxGroup";
|
||||
import { withoutTypename } from "src/utils";
|
||||
import { SettingSection } from "../SettingSection";
|
||||
import {
|
||||
BooleanSetting,
|
||||
ModalSetting,
|
||||
NumberSetting,
|
||||
SelectSetting,
|
||||
StringSetting,
|
||||
} from "../Inputs";
|
||||
import { SettingStateContext } from "../context";
|
||||
import { DurationUtils } from "src/utils";
|
||||
|
||||
const allMenuItems = [
|
||||
{ id: "scenes", label: "Scenes" },
|
||||
{ id: "images", label: "Images" },
|
||||
{ id: "movies", label: "Movies" },
|
||||
{ id: "markers", label: "Markers" },
|
||||
{ id: "galleries", label: "Galleries" },
|
||||
{ id: "performers", label: "Performers" },
|
||||
{ id: "studios", label: "Studios" },
|
||||
{ id: "tags", label: "Tags" },
|
||||
{ id: "scenes", headingID: "scenes" },
|
||||
{ id: "images", headingID: "images" },
|
||||
{ id: "movies", headingID: "movies" },
|
||||
{ id: "markers", headingID: "markers" },
|
||||
{ id: "galleries", headingID: "galleries" },
|
||||
{ id: "performers", headingID: "performers" },
|
||||
{ id: "studios", headingID: "studios" },
|
||||
{ id: "tags", headingID: "tags" },
|
||||
];
|
||||
|
||||
const SECONDS_TO_MS = 1000;
|
||||
|
||||
export const SettingsInterfacePanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const { data: config, error, loading } = useConfiguration();
|
||||
const [menuItemIds, setMenuItemIds] = useState<string[]>(
|
||||
allMenuItems.map((item) => item.id)
|
||||
|
||||
const { interface: iface, saveInterface, loading, error } = React.useContext(
|
||||
SettingStateContext
|
||||
);
|
||||
const [noBrowser, setNoBrowserFlag] = useState<boolean>(false);
|
||||
const [soundOnPreview, setSoundOnPreview] = useState<boolean>(true);
|
||||
const [wallShowTitle, setWallShowTitle] = useState<boolean>(true);
|
||||
const [wallPlayback, setWallPlayback] = useState<string>("video");
|
||||
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
|
||||
const [autostartVideo, setAutostartVideo] = useState<boolean>(false);
|
||||
const [
|
||||
autostartVideoOnPlaySelected,
|
||||
setAutostartVideoOnPlaySelected,
|
||||
] = useState(true);
|
||||
const [continuePlaylistDefault, setContinuePlaylistDefault] = useState(false);
|
||||
const [slideshowDelay, setSlideshowDelay] = useState<number>(0);
|
||||
const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false);
|
||||
const [css, setCSS] = useState<string>();
|
||||
const [cssEnabled, setCSSEnabled] = useState<boolean>(false);
|
||||
const [language, setLanguage] = useState<string>("en");
|
||||
const [handyKey, setHandyKey] = useState<string>();
|
||||
const [funscriptOffset, setFunscriptOffset] = useState<number>(0);
|
||||
const [deleteFileDefault, setDeleteFileDefault] = useState<boolean>(false);
|
||||
const [deleteGeneratedDefault, setDeleteGeneratedDefault] = useState<boolean>(
|
||||
true
|
||||
);
|
||||
const [
|
||||
disableDropdownCreate,
|
||||
setDisableDropdownCreate,
|
||||
] = useState<GQL.ConfigDisableDropdownCreateInput>({});
|
||||
|
||||
const [updateInterfaceConfig] = useConfigureInterface({
|
||||
menuItems: menuItemIds,
|
||||
soundOnPreview,
|
||||
wallShowTitle,
|
||||
wallPlayback,
|
||||
maximumLoopDuration,
|
||||
noBrowser,
|
||||
autostartVideo,
|
||||
autostartVideoOnPlaySelected,
|
||||
continuePlaylistDefault,
|
||||
showStudioAsText,
|
||||
css,
|
||||
cssEnabled,
|
||||
language,
|
||||
slideshowDelay,
|
||||
handyKey,
|
||||
funscriptOffset,
|
||||
disableDropdownCreate,
|
||||
});
|
||||
|
||||
const [updateDefaultsConfig] = useConfigureDefaults();
|
||||
|
||||
useEffect(() => {
|
||||
if (config) {
|
||||
const { interface: iCfg, defaults } = config.configuration;
|
||||
setMenuItemIds(iCfg.menuItems ?? allMenuItems.map((item) => item.id));
|
||||
setSoundOnPreview(iCfg.soundOnPreview ?? true);
|
||||
setWallShowTitle(iCfg.wallShowTitle ?? true);
|
||||
setWallPlayback(iCfg.wallPlayback ?? "video");
|
||||
setMaximumLoopDuration(iCfg.maximumLoopDuration ?? 0);
|
||||
setNoBrowserFlag(iCfg?.noBrowser ?? false);
|
||||
setAutostartVideo(iCfg.autostartVideo ?? false);
|
||||
setAutostartVideoOnPlaySelected(
|
||||
iCfg.autostartVideoOnPlaySelected ?? true
|
||||
);
|
||||
setContinuePlaylistDefault(iCfg.continuePlaylistDefault ?? false);
|
||||
setShowStudioAsText(iCfg.showStudioAsText ?? false);
|
||||
setCSS(iCfg.css ?? "");
|
||||
setCSSEnabled(iCfg.cssEnabled ?? false);
|
||||
setLanguage(iCfg.language ?? "en-US");
|
||||
setSlideshowDelay(iCfg.slideshowDelay ?? 5000);
|
||||
setHandyKey(iCfg.handyKey ?? "");
|
||||
setFunscriptOffset(iCfg.funscriptOffset ?? 0);
|
||||
setDisableDropdownCreate({
|
||||
performer: iCfg.disabledDropdownCreate.performer,
|
||||
studio: iCfg.disabledDropdownCreate.studio,
|
||||
tag: iCfg.disabledDropdownCreate.tag,
|
||||
});
|
||||
|
||||
setDeleteFileDefault(defaults.deleteFile ?? false);
|
||||
setDeleteGeneratedDefault(defaults.deleteGenerated ?? true);
|
||||
}
|
||||
}, [config]);
|
||||
|
||||
async function onSave() {
|
||||
const prevCSS = config?.configuration.interface.css;
|
||||
const prevCSSenabled = config?.configuration.interface.cssEnabled;
|
||||
try {
|
||||
if (config?.configuration.defaults) {
|
||||
await updateDefaultsConfig({
|
||||
variables: {
|
||||
input: {
|
||||
...withoutTypename(config?.configuration.defaults),
|
||||
deleteFile: deleteFileDefault,
|
||||
deleteGenerated: deleteGeneratedDefault,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const result = await updateInterfaceConfig();
|
||||
|
||||
// Force refetch of custom css if it was changed
|
||||
if (
|
||||
prevCSS !== result.data?.configureInterface.css ||
|
||||
prevCSSenabled !== result.data?.configureInterface.cssEnabled
|
||||
) {
|
||||
await fetch("/css", { cache: "reload" });
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl
|
||||
.formatMessage({ id: "configuration" })
|
||||
.toLocaleLowerCase(),
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (error) return <h1>{error.message}</h1>;
|
||||
if (loading) return <LoadingIndicator />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{intl.formatMessage({ id: "config.ui.title" })}</h4>
|
||||
<Form.Group controlId="language">
|
||||
<h5>{intl.formatMessage({ id: "config.ui.language.heading" })}</h5>
|
||||
<Form.Control
|
||||
as="select"
|
||||
className="col-4 input-control"
|
||||
value={language}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setLanguage(e.currentTarget.value)
|
||||
}
|
||||
<SettingSection headingID="config.ui.basic_settings">
|
||||
<SelectSetting
|
||||
id="language"
|
||||
headingID="config.ui.language.heading"
|
||||
value={iface.language ?? undefined}
|
||||
onChange={(v) => saveInterface({ language: v })}
|
||||
>
|
||||
<option value="en-US">English (United States)</option>
|
||||
<option value="en-GB">English (United Kingdom)</option>
|
||||
@@ -181,76 +55,61 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
<option value="sv-SE">Swedish (Sweden)</option>
|
||||
<option value="zh-TW">繁體中文 (台灣)</option>
|
||||
<option value="zh-CN">简体中文 (中国)</option>
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.menu_items.heading" })}</h5>
|
||||
<CheckboxGroup
|
||||
groupId="menu-items"
|
||||
items={allMenuItems}
|
||||
checkedIds={menuItemIds}
|
||||
onChange={setMenuItemIds}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({ id: "config.ui.menu_items.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</SelectSetting>
|
||||
|
||||
<hr />
|
||||
<div className="setting-group">
|
||||
<div className="setting">
|
||||
<div>
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.menu_items.heading",
|
||||
})}
|
||||
</h3>
|
||||
<div className="sub-heading">
|
||||
{intl.formatMessage({ id: "config.ui.menu_items.description" })}
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<CheckboxGroup
|
||||
groupId="menu-items"
|
||||
items={allMenuItems}
|
||||
checkedIds={iface.menuItems ?? undefined}
|
||||
onChange={(v) => saveInterface({ menuItems: v })}
|
||||
/>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<h4>
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.desktop_integration.desktop_integration",
|
||||
})}
|
||||
</h4>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
<SettingSection headingID="config.ui.desktop_integration.desktop_integration">
|
||||
<BooleanSetting
|
||||
id="skip-browser"
|
||||
checked={noBrowser}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.desktop_integration.skip_opening_browser",
|
||||
})}
|
||||
onChange={() => setNoBrowserFlag(!noBrowser)}
|
||||
headingID="config.ui.desktop_integration.skip_opening_browser"
|
||||
subHeadingID="config.ui.desktop_integration.skip_opening_browser_on_startup"
|
||||
checked={iface.noBrowser ?? undefined}
|
||||
onChange={(v) => saveInterface({ noBrowser: v })}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.desktop_integration.skip_opening_browser_on_startup",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<hr />
|
||||
</SettingSection>
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.scene_wall.heading" })}</h5>
|
||||
<Form.Check
|
||||
<SettingSection headingID="config.ui.scene_wall.heading">
|
||||
<BooleanSetting
|
||||
id="wall-show-title"
|
||||
checked={wallShowTitle}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_wall.options.display_title",
|
||||
})}
|
||||
onChange={() => setWallShowTitle(!wallShowTitle)}
|
||||
headingID="config.ui.scene_wall.options.display_title"
|
||||
checked={iface.wallShowTitle ?? undefined}
|
||||
onChange={(v) => saveInterface({ wallShowTitle: v })}
|
||||
/>
|
||||
<Form.Check
|
||||
<BooleanSetting
|
||||
id="wall-sound-enabled"
|
||||
checked={soundOnPreview}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_wall.options.toggle_sound",
|
||||
})}
|
||||
onChange={() => setSoundOnPreview(!soundOnPreview)}
|
||||
headingID="config.ui.scene_wall.options.toggle_sound"
|
||||
checked={iface.soundOnPreview ?? undefined}
|
||||
onChange={(v) => saveInterface({ soundOnPreview: v })}
|
||||
/>
|
||||
<Form.Label htmlFor="wall-preview">
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.ui.preview_type.heading" })}
|
||||
</h6>
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
name="wall-preview"
|
||||
className="col-4 input-control"
|
||||
value={wallPlayback}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setWallPlayback(e.currentTarget.value)
|
||||
}
|
||||
|
||||
<SelectSetting
|
||||
id="wall-preview"
|
||||
headingID="config.ui.preview_type.heading"
|
||||
subHeadingID="config.ui.preview_type.description"
|
||||
value={iface.wallPlayback ?? undefined}
|
||||
onChange={(v) => saveInterface({ wallPlayback: v })}
|
||||
>
|
||||
<option value="video">
|
||||
{intl.formatMessage({ id: "config.ui.preview_type.options.video" })}
|
||||
@@ -265,269 +124,172 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
id: "config.ui.preview_type.options.static",
|
||||
})}
|
||||
</option>
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({ id: "config.ui.preview_type.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</SelectSetting>
|
||||
</SettingSection>
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.scene_list.heading" })}</h5>
|
||||
<Form.Check
|
||||
<SettingSection headingID="config.ui.scene_list.heading">
|
||||
<BooleanSetting
|
||||
id="show-text-studios"
|
||||
checked={showStudioAsText}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_list.options.show_studio_as_text",
|
||||
})}
|
||||
onChange={() => {
|
||||
setShowStudioAsText(!showStudioAsText);
|
||||
headingID="config.ui.scene_list.options.show_studio_as_text"
|
||||
checked={iface.showStudioAsText ?? undefined}
|
||||
onChange={(v) => saveInterface({ showStudioAsText: v })}
|
||||
/>
|
||||
</SettingSection>
|
||||
|
||||
<SettingSection headingID="config.ui.scene_player.heading">
|
||||
<BooleanSetting
|
||||
id="auto-start-video"
|
||||
headingID="config.ui.scene_player.options.auto_start_video"
|
||||
checked={iface.autostartVideo ?? undefined}
|
||||
onChange={(v) => saveInterface({ autostartVideo: v })}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id="auto-start-video-on-play-selected"
|
||||
headingID="config.ui.scene_player.options.auto_start_video_on_play_selected.heading"
|
||||
subHeadingID="config.ui.scene_player.options.auto_start_video_on_play_selected.description"
|
||||
checked={iface.autostartVideoOnPlaySelected ?? undefined}
|
||||
onChange={(v) => saveInterface({ autostartVideoOnPlaySelected: v })}
|
||||
/>
|
||||
|
||||
<BooleanSetting
|
||||
id="continue-playlist-default"
|
||||
headingID="config.ui.scene_player.options.continue_playlist_default.heading"
|
||||
subHeadingID="config.ui.scene_player.options.continue_playlist_default.description"
|
||||
checked={iface.continuePlaylistDefault ?? undefined}
|
||||
onChange={(v) => saveInterface({ continuePlaylistDefault: v })}
|
||||
/>
|
||||
|
||||
<ModalSetting<number>
|
||||
id="max-loop-duration"
|
||||
headingID="config.ui.max_loop_duration.heading"
|
||||
subHeadingID="config.ui.max_loop_duration.description"
|
||||
value={iface.maximumLoopDuration ?? undefined}
|
||||
onChange={(v) => saveInterface({ maximumLoopDuration: v })}
|
||||
renderField={(value, setValue) => (
|
||||
<DurationInput
|
||||
numericValue={value}
|
||||
onValueChange={(duration) => setValue(duration ?? 0)}
|
||||
/>
|
||||
)}
|
||||
renderValue={(v) => {
|
||||
return <span>{DurationUtils.secondsToString(v ?? 0)}</span>;
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
</SettingSection>
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.scene_player.heading" })}</h5>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="auto-start-video"
|
||||
checked={autostartVideo}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.scene_player.options.auto_start_video",
|
||||
})}
|
||||
onChange={() => {
|
||||
setAutostartVideo(!autostartVideo);
|
||||
}}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group id="auto-start-video-on-play-selected">
|
||||
<Form.Check
|
||||
checked={autostartVideoOnPlaySelected}
|
||||
label={intl.formatMessage({
|
||||
id:
|
||||
"config.ui.scene_player.options.auto_start_video_on_play_selected.heading",
|
||||
})}
|
||||
onChange={() => {
|
||||
setAutostartVideoOnPlaySelected(!autostartVideoOnPlaySelected);
|
||||
}}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id:
|
||||
"config.ui.scene_player.options.auto_start_video_on_play_selected.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="continue-playlist-default">
|
||||
<Form.Check
|
||||
checked={continuePlaylistDefault}
|
||||
label={intl.formatMessage({
|
||||
id:
|
||||
"config.ui.scene_player.options.continue_playlist_default.heading",
|
||||
})}
|
||||
onChange={() => {
|
||||
setContinuePlaylistDefault(!continuePlaylistDefault);
|
||||
}}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id:
|
||||
"config.ui.scene_player.options.continue_playlist_default.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="max-loop-duration">
|
||||
<h6>
|
||||
{intl.formatMessage({ id: "config.ui.max_loop_duration.heading" })}
|
||||
</h6>
|
||||
<DurationInput
|
||||
className="row col col-4"
|
||||
numericValue={maximumLoopDuration}
|
||||
onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.max_loop_duration.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="slideshow-delay">
|
||||
<h5>
|
||||
{intl.formatMessage({ id: "config.ui.slideshow_delay.heading" })}
|
||||
</h5>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={slideshowDelay / SECONDS_TO_MS}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setSlideshowDelay(
|
||||
Number.parseInt(e.currentTarget.value, 10) * SECONDS_TO_MS
|
||||
);
|
||||
}}
|
||||
<SettingSection headingID="config.ui.images.heading">
|
||||
<NumberSetting
|
||||
headingID="config.ui.slideshow_delay.heading"
|
||||
subHeadingID="config.ui.slideshow_delay.description"
|
||||
value={iface.slideshowDelay ?? undefined}
|
||||
onChange={(v) => saveInterface({ slideshowDelay: v })}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({ id: "config.ui.slideshow_delay.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</SettingSection>
|
||||
|
||||
<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
|
||||
<SettingSection headingID="config.ui.editing.heading">
|
||||
<div className="setting-group">
|
||||
<div className="setting">
|
||||
<div>
|
||||
<h3>
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.editing.disable_dropdown_create.heading",
|
||||
})}
|
||||
</h3>
|
||||
<div className="sub-heading">
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.editing.disable_dropdown_create.description",
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<BooleanSetting
|
||||
id="disableDropdownCreate_performer"
|
||||
checked={disableDropdownCreate.performer ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "performer",
|
||||
})}
|
||||
onChange={() => {
|
||||
setDisableDropdownCreate({
|
||||
...disableDropdownCreate,
|
||||
performer: !disableDropdownCreate.performer ?? true,
|
||||
});
|
||||
}}
|
||||
headingID="performer"
|
||||
checked={iface.disableDropdownCreate?.performer ?? undefined}
|
||||
onChange={(v) =>
|
||||
saveInterface({
|
||||
disableDropdownCreate: {
|
||||
...iface.disableDropdownCreate,
|
||||
performer: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Form.Check
|
||||
<BooleanSetting
|
||||
id="disableDropdownCreate_studio"
|
||||
checked={disableDropdownCreate.studio ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "studio",
|
||||
})}
|
||||
onChange={() => {
|
||||
setDisableDropdownCreate({
|
||||
...disableDropdownCreate,
|
||||
studio: !disableDropdownCreate.studio ?? true,
|
||||
});
|
||||
}}
|
||||
headingID="studio"
|
||||
checked={iface.disableDropdownCreate?.studio ?? undefined}
|
||||
onChange={(v) =>
|
||||
saveInterface({
|
||||
disableDropdownCreate: {
|
||||
...iface.disableDropdownCreate,
|
||||
studio: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
<Form.Check
|
||||
<BooleanSetting
|
||||
id="disableDropdownCreate_tag"
|
||||
checked={disableDropdownCreate.tag ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "tag",
|
||||
})}
|
||||
onChange={() => {
|
||||
setDisableDropdownCreate({
|
||||
...disableDropdownCreate,
|
||||
tag: !disableDropdownCreate.tag ?? true,
|
||||
});
|
||||
}}
|
||||
headingID="tag"
|
||||
checked={iface.disableDropdownCreate?.tag ?? undefined}
|
||||
onChange={(v) =>
|
||||
saveInterface({
|
||||
disableDropdownCreate: {
|
||||
...iface.disableDropdownCreate,
|
||||
tag: v,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.editing.disable_dropdown_create.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
</div>
|
||||
</SettingSection>
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.ui.custom_css.heading" })}</h5>
|
||||
<Form.Check
|
||||
<SettingSection headingID="config.ui.custom_css.heading">
|
||||
<BooleanSetting
|
||||
id="custom-css-enabled"
|
||||
headingID="config.ui.custom_css.option_label"
|
||||
checked={iface.cssEnabled ?? undefined}
|
||||
onChange={(v) => saveInterface({ cssEnabled: v })}
|
||||
/>
|
||||
|
||||
<ModalSetting<string>
|
||||
id="custom-css"
|
||||
checked={cssEnabled}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.custom_css.option_label",
|
||||
})}
|
||||
onChange={() => {
|
||||
setCSSEnabled(!cssEnabled);
|
||||
headingID="config.ui.custom_css.heading"
|
||||
subHeadingID="config.ui.custom_css.description"
|
||||
value={iface.css ?? undefined}
|
||||
onChange={(v) => saveInterface({ css: v })}
|
||||
renderField={(value, setValue) => (
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={value}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setValue(e.currentTarget.value)
|
||||
}
|
||||
rows={16}
|
||||
className="text-input code"
|
||||
/>
|
||||
)}
|
||||
renderValue={() => {
|
||||
return <></>;
|
||||
}}
|
||||
/>
|
||||
</SettingSection>
|
||||
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
value={css}
|
||||
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setCSS(e.currentTarget.value)
|
||||
}
|
||||
rows={16}
|
||||
className="col col-sm-6 text-input code"
|
||||
<SettingSection headingID="config.ui.interactive_options">
|
||||
<StringSetting
|
||||
headingID="config.ui.handy_connection_key.heading"
|
||||
subHeadingID="config.ui.handy_connection_key.description"
|
||||
value={iface.handyKey ?? undefined}
|
||||
onChange={(v) => saveInterface({ handyKey: v })}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({ id: "config.ui.custom_css.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>
|
||||
{intl.formatMessage({ id: "config.ui.handy_connection_key.heading" })}
|
||||
</h5>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
value={handyKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setHandyKey(e.currentTarget.value);
|
||||
}}
|
||||
<NumberSetting
|
||||
headingID="config.ui.funscript_offset.heading"
|
||||
subHeadingID="config.ui.funscript_offset.description"
|
||||
value={iface.funscriptOffset ?? undefined}
|
||||
onChange={(v) => saveInterface({ funscriptOffset: v })}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.handy_connection_key.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<h5>
|
||||
{intl.formatMessage({ id: "config.ui.funscript_offset.heading" })}
|
||||
</h5>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={funscriptOffset}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFunscriptOffset(Number.parseInt(e.currentTarget.value, 10));
|
||||
}}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({ id: "config.ui.funscript_offset.description" })}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<h5>
|
||||
{intl.formatMessage({ id: "config.ui.delete_options.heading" })}
|
||||
</h5>
|
||||
<Form.Check
|
||||
id="delete-file-default"
|
||||
checked={deleteFileDefault}
|
||||
label={intl.formatMessage({
|
||||
id: "config.ui.delete_options.options.delete_file",
|
||||
})}
|
||||
onChange={() => {
|
||||
setDeleteFileDefault(!deleteFileDefault);
|
||||
}}
|
||||
/>
|
||||
<Form.Check
|
||||
id="delete-generated-default"
|
||||
checked={deleteGeneratedDefault}
|
||||
label={intl.formatMessage({
|
||||
id:
|
||||
"config.ui.delete_options.options.delete_generated_supporting_files",
|
||||
})}
|
||||
onChange={() => {
|
||||
setDeleteGeneratedDefault(!deleteGeneratedDefault);
|
||||
}}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "config.ui.delete_options.description",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
<Button variant="primary" onClick={() => onSave()}>
|
||||
{intl.formatMessage({ id: "actions.save" })}
|
||||
</Button>
|
||||
</SettingSection>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user