mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 00:13:46 +01:00
Add SFW content mode option (#6262)
* Use more neutral language for content * Add sfw mode setting * Make configuration context mandatory * Add sfw class when sfw mode active * Hide nsfw performer fields in sfw mode * Hide nsfw sort options * Hide nsfw filter/sort options in sfw mode * Replace o-count with like counter in sfw mode * Use sfw label for o-counter filter in sfw mode * Use likes instead of o-count in sfw mode in other places * Rename sfw mode to sfw content mode * Use sfw image for default performers in sfw mode * Document SFW content mode * Add SFW mode setting to setup * Clarify README * Change wording of sfw mode description * Handle configuration loading error correctly * Hide age in performer cards
This commit is contained in:
parent
bb56b619f5
commit
51999135be
105 changed files with 843 additions and 370 deletions
|
|
@ -9,7 +9,7 @@
|
|||
[](https://github.com/stashapp/stash/releases/latest)
|
||||
[](https://github.com/stashapp/stash/labels/bounty)
|
||||
|
||||
### **Stash is a self-hosted webapp written in Go which organizes and serves your porn.**
|
||||
### **Stash is a self-hosted webapp written in Go which organizes and serves your diverse content collection, catering to both your SFW and NSFW needs.**
|
||||

|
||||
|
||||
* Stash gathers information about videos in your collection from the internet, and is extensible through the use of community-built plugins for a large number of content producers and sites.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ input SetupInput {
|
|||
"Empty to indicate $HOME/.stash/config.yml default"
|
||||
configLocation: String!
|
||||
stashes: [StashConfigInput!]!
|
||||
"True if SFW content mode is enabled"
|
||||
sfwContentMode: Boolean
|
||||
"Empty to indicate default"
|
||||
databaseFile: String!
|
||||
"Empty to indicate default"
|
||||
|
|
@ -341,6 +343,9 @@ type ConfigImageLightboxResult {
|
|||
}
|
||||
|
||||
input ConfigInterfaceInput {
|
||||
"True if SFW content mode is enabled"
|
||||
sfwContentMode: Boolean
|
||||
|
||||
"Ordered list of items that should be shown in the menu"
|
||||
menuItems: [String!]
|
||||
|
||||
|
|
@ -407,6 +412,9 @@ type ConfigDisableDropdownCreate {
|
|||
}
|
||||
|
||||
type ConfigInterfaceResult {
|
||||
"True if SFW content mode is enabled"
|
||||
sfwContentMode: Boolean!
|
||||
|
||||
"Ordered list of items that should be shown in the menu"
|
||||
menuItems: [String!]
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ func initCustomPerformerImages(customPath string) {
|
|||
}
|
||||
}
|
||||
|
||||
func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte {
|
||||
func getDefaultPerformerImage(name string, gender *models.GenderEnum, sfwMode bool) []byte {
|
||||
// try the custom box first if we have one
|
||||
if performerBoxCustom != nil {
|
||||
ret, err := performerBoxCustom.GetRandomImageByName(name)
|
||||
|
|
@ -111,6 +111,10 @@ func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte {
|
|||
logger.Warnf("error loading custom default performer image: %v", err)
|
||||
}
|
||||
|
||||
if sfwMode {
|
||||
return static.ReadAll(static.DefaultSFWPerformerImage)
|
||||
}
|
||||
|
||||
var g models.GenderEnum
|
||||
if gender != nil {
|
||||
g = *gender
|
||||
|
|
|
|||
|
|
@ -445,6 +445,8 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
|
||||
c := config.GetInstance()
|
||||
|
||||
r.setConfigBool(config.SFWContentMode, input.SfwContentMode)
|
||||
|
||||
if input.MenuItems != nil {
|
||||
c.SetInterface(config.MenuItems, input.MenuItems)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,6 +162,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
|||
disableDropdownCreate := config.GetDisableDropdownCreate()
|
||||
|
||||
return &ConfigInterfaceResult{
|
||||
SfwContentMode: config.GetSFWContentMode(),
|
||||
MenuItems: menuItems,
|
||||
SoundOnPreview: &soundOnPreview,
|
||||
WallShowTitle: &wallShowTitle,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,14 @@ type PerformerFinder interface {
|
|||
GetImage(ctx context.Context, performerID int) ([]byte, error)
|
||||
}
|
||||
|
||||
type sfwConfig interface {
|
||||
GetSFWContentMode() bool
|
||||
}
|
||||
|
||||
type performerRoutes struct {
|
||||
routes
|
||||
performerFinder PerformerFinder
|
||||
sfwConfig sfwConfig
|
||||
}
|
||||
|
||||
func (rs performerRoutes) Routes() chi.Router {
|
||||
|
|
@ -54,7 +59,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if len(image) == 0 {
|
||||
image = getDefaultPerformerImage(performer.Name, performer.Gender)
|
||||
image = getDefaultPerformerImage(performer.Name, performer.Gender, rs.sfwConfig.GetSFWContentMode())
|
||||
}
|
||||
|
||||
utils.ServeImage(w, r, image)
|
||||
|
|
|
|||
|
|
@ -322,6 +322,7 @@ func (s *Server) getPerformerRoutes() chi.Router {
|
|||
return performerRoutes{
|
||||
routes: routes{txnManager: repo.TxnManager},
|
||||
performerFinder: repo.Performer,
|
||||
sfwConfig: s.manager.Config,
|
||||
}.Routes()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ const (
|
|||
Password = "password"
|
||||
MaxSessionAge = "max_session_age"
|
||||
|
||||
// SFWContentMode mode config key
|
||||
SFWContentMode = "sfw_content_mode"
|
||||
|
||||
FFMpegPath = "ffmpeg_path"
|
||||
FFProbePath = "ffprobe_path"
|
||||
|
||||
|
|
@ -628,7 +631,15 @@ func (i *Config) getStringMapString(key string) map[string]string {
|
|||
return ret
|
||||
}
|
||||
|
||||
// GetStathPaths returns the configured stash library paths.
|
||||
// GetSFW returns true if SFW mode is enabled.
|
||||
// Default performer images are changed to more agnostic images when enabled.
|
||||
func (i *Config) GetSFWContentMode() bool {
|
||||
i.RLock()
|
||||
defer i.RUnlock()
|
||||
return i.getBool(SFWContentMode)
|
||||
}
|
||||
|
||||
// GetStashPaths returns the configured stash library paths.
|
||||
// Works opposite to the usual case - it will return the override
|
||||
// value only if the main value is not set.
|
||||
func (i *Config) GetStashPaths() StashConfigs {
|
||||
|
|
|
|||
|
|
@ -262,6 +262,10 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
|||
cfg.SetString(config.Cache, input.CacheLocation)
|
||||
}
|
||||
|
||||
if input.SFWContentMode {
|
||||
cfg.SetBool(config.SFWContentMode, true)
|
||||
}
|
||||
|
||||
if input.StoreBlobsInDatabase {
|
||||
cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase)
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type SetupInput struct {
|
|||
// Empty to indicate $HOME/.stash/config.yml default
|
||||
ConfigLocation string `json:"configLocation"`
|
||||
Stashes []*config.StashConfigInput `json:"stashes"`
|
||||
SFWContentMode bool `json:"sfwContentMode"`
|
||||
// Empty to indicate default
|
||||
DatabaseFile string `json:"databaseFile"`
|
||||
// Empty to indicate default
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ import (
|
|||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed performer performer_male scene image gallery tag studio group
|
||||
//go:embed performer performer_male performer_sfw scene image gallery tag studio group
|
||||
var data embed.FS
|
||||
|
||||
const (
|
||||
Performer = "performer"
|
||||
PerformerMale = "performer_male"
|
||||
Performer = "performer"
|
||||
PerformerMale = "performer_male"
|
||||
DefaultSFWPerformerImage = "performer_sfw/performer.svg"
|
||||
|
||||
Scene = "scene"
|
||||
DefaultSceneImage = "scene/scene.svg"
|
||||
|
|
|
|||
7
internal/static/performer_sfw/performer.svg
Normal file
7
internal/static/performer_sfw/performer.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-136 -284 720 1080">
|
||||
<!--!
|
||||
Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2024 Fonticons, Inc.
|
||||
Original from https://github.com/FortAwesome/Font-Awesome/blob/6.x/svgs/solid/user.svg
|
||||
Modified to change color and viewbox
|
||||
-->
|
||||
<path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l388.6 0c16.4 0 29.7-13.3 29.7-29.7C448 383.8 368.2 304 269.7 304l-91.4 0z" style="fill:#ffffff;fill-opacity:1" /></svg>
|
||||
|
After Width: | Height: | Size: 645 B |
|
|
@ -71,6 +71,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
}
|
||||
|
||||
fragment ConfigInterfaceData on ConfigInterfaceResult {
|
||||
sfwContentMode
|
||||
menuItems
|
||||
soundOnPreview
|
||||
wallShowTitle
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ import * as GQL from "./core/generated-graphql";
|
|||
import { makeTitleProps } from "./hooks/title";
|
||||
import { LoadingIndicator } from "./components/Shared/LoadingIndicator";
|
||||
|
||||
import { ConfigurationProvider } from "./hooks/Config";
|
||||
import {
|
||||
ConfigurationProvider,
|
||||
useConfigurationContextOptional,
|
||||
} from "./hooks/Config";
|
||||
import { ManualProvider } from "./components/Help/context";
|
||||
import { InteractiveProvider } from "./hooks/Interactive/context";
|
||||
import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog";
|
||||
|
|
@ -50,6 +53,7 @@ import { PatchFunction } from "./patch";
|
|||
|
||||
import moment from "moment/min/moment-with-locales";
|
||||
import { ErrorMessage } from "./components/Shared/ErrorMessage";
|
||||
import cx from "classnames";
|
||||
|
||||
const Performers = lazyComponent(
|
||||
() => import("./components/Performers/Performers")
|
||||
|
|
@ -104,8 +108,17 @@ const AppContainer: React.FC<React.PropsWithChildren<{}>> = PatchFunction(
|
|||
) as React.FC;
|
||||
|
||||
const MainContainer: React.FC = ({ children }) => {
|
||||
// use optional here because the configuration may have be loading or errored
|
||||
const { configuration } = useConfigurationContextOptional() || {};
|
||||
const { sfwContentMode } = configuration?.interface || {};
|
||||
|
||||
return (
|
||||
<div className={`main container-fluid ${appleRendering ? "apple" : ""}`}>
|
||||
<div
|
||||
className={cx("main container-fluid", {
|
||||
apple: appleRendering,
|
||||
"sfw-content-mode": sfwContentMode,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -300,28 +313,36 @@ export const App: React.FC = () => {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (config.error) {
|
||||
function renderSimple(content: React.ReactNode) {
|
||||
return (
|
||||
<IntlProvider
|
||||
locale={intlLanguage}
|
||||
messages={messages}
|
||||
formats={intlFormats}
|
||||
>
|
||||
<MainContainer>
|
||||
<ErrorMessage
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="errors.loading_type"
|
||||
values={{ type: "configuration" }}
|
||||
/>
|
||||
}
|
||||
error={config.error.message}
|
||||
/>
|
||||
</MainContainer>
|
||||
<MainContainer>{content}</MainContainer>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
if (config.loading) {
|
||||
return renderSimple(<LoadingIndicator />);
|
||||
}
|
||||
|
||||
if (config.error) {
|
||||
return renderSimple(
|
||||
<ErrorMessage
|
||||
message={
|
||||
<FormattedMessage
|
||||
id="errors.loading_type"
|
||||
values={{ type: "configuration" }}
|
||||
/>
|
||||
}
|
||||
error={config.error.message}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<IntlProvider
|
||||
|
|
@ -332,10 +353,7 @@ export const App: React.FC = () => {
|
|||
<ToastProvider>
|
||||
<PluginsLoader>
|
||||
<AppContainer>
|
||||
<ConfigurationProvider
|
||||
configuration={config.data?.configuration}
|
||||
loading={config.loading}
|
||||
>
|
||||
<ConfigurationProvider configuration={config.data!.configuration}>
|
||||
{maybeRenderReleaseNotes()}
|
||||
<ConnectionMonitor />
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Icon } from "src/components/Shared/Icon";
|
|||
import { useToast } from "src/hooks/Toast";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { Manual } from "../Help/Manual";
|
||||
import { withoutTypename } from "src/utils/data";
|
||||
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
||||
|
|
@ -25,7 +25,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
|||
onClose,
|
||||
type,
|
||||
}) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
function getDefaultOptions(): GQL.GenerateMetadataInput {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { FrontPageContent, ICustomFilter } from "src/core/config";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useFindSavedFilter } from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow";
|
||||
import { ImageRecommendationRow } from "../Images/ImageRecommendationRow";
|
||||
|
|
@ -105,7 +105,7 @@ interface ISavedFilterResults {
|
|||
const SavedFilterResults: React.FC<ISavedFilterResults> = ({
|
||||
savedFilterID,
|
||||
}) => {
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const { loading, data } = useFindSavedFilter(savedFilterID.toString());
|
||||
|
||||
const filter = useMemo(() => {
|
||||
|
|
@ -136,7 +136,7 @@ interface ICustomFilterProps {
|
|||
const CustomFilterResults: React.FC<ICustomFilterProps> = ({
|
||||
customFilter,
|
||||
}) => {
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
|
||||
const filter = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Button } from "react-bootstrap";
|
|||
import { FrontPageConfig } from "./FrontPageConfig";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { Control } from "./Control";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
FrontPageContent,
|
||||
generateDefaultFrontPageContent,
|
||||
|
|
@ -24,7 +24,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => {
|
|||
|
||||
const [saveUI] = useConfigureUI();
|
||||
|
||||
const { configuration, loading } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
useScrollToTopOnMount();
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => {
|
|||
setSaving(false);
|
||||
}
|
||||
|
||||
if (loading || saving) {
|
||||
if (saving) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useFindSavedFilters } from "src/core/StashService";
|
|||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
import { Button, Form, Modal } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
ISavedFilterRow,
|
||||
ICustomFilter,
|
||||
|
|
@ -277,11 +277,11 @@ interface IFrontPageConfigProps {
|
|||
export const FrontPageConfig: React.FC<IFrontPageConfigProps> = ({
|
||||
onClose,
|
||||
}) => {
|
||||
const { configuration, loading } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const ui = configuration?.ui;
|
||||
|
||||
const { data: allFilters, loading: loading2 } = useFindSavedFilters();
|
||||
const { data: allFilters, loading } = useFindSavedFilters();
|
||||
|
||||
const [isAdd, setIsAdd] = useState(false);
|
||||
const [currentContent, setCurrentContent] = useState<FrontPageContent[]>([]);
|
||||
|
|
@ -338,7 +338,7 @@ export const FrontPageConfig: React.FC<IFrontPageConfigProps> = ({
|
|||
setDragIndex(undefined);
|
||||
}
|
||||
|
||||
if (loading || loading2) {
|
||||
if (loading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "../Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
|
|||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(
|
||||
config?.defaults.deleteFile ?? false
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button, Tab, Nav, Dropdown } from "react-bootstrap";
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
useHistory,
|
||||
Link,
|
||||
|
|
@ -41,7 +41,7 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
|||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import cx from "classnames";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { goBackOrReplace } from "src/utils/history";
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
|
|||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters);
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
|
|
|
|||
|
|
@ -161,32 +161,38 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
|||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="title"
|
||||
title={intl.formatMessage({ id: "title" })}
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="code"
|
||||
title={intl.formatMessage({ id: "scene_code" })}
|
||||
result={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="urls"
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={urls}
|
||||
onChange={(value) => setURLs(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="photographer"
|
||||
title={intl.formatMessage({ id: "photographer" })}
|
||||
result={photographer}
|
||||
onChange={(value) => setPhotographer(value)}
|
||||
/>
|
||||
<ScrapedStudioRow
|
||||
field="studio"
|
||||
title={intl.formatMessage({ id: "studios" })}
|
||||
result={studio}
|
||||
onChange={(value) => setStudio(value)}
|
||||
|
|
@ -194,6 +200,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
|||
onCreateNew={createNewStudio}
|
||||
/>
|
||||
<ScrapedPerformersRow
|
||||
field="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
result={performers}
|
||||
onChange={(value) => setPerformers(value)}
|
||||
|
|
@ -203,6 +210,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
|||
/>
|
||||
{scrapedTagsRow}
|
||||
<ScrapedTextAreaRow
|
||||
field="details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
queryFindGalleriesForSelect,
|
||||
queryFindGalleriesByIDForSelect,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -70,7 +70,7 @@ const gallerySelectSort = PatchFunction(
|
|||
const _GallerySelect: React.FC<
|
||||
IFilterProps & IFilterValueProps<Gallery> & ExtraGalleryProps
|
||||
> = (props) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
|
|||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RelatedGroupPopoverButton } from "./RelatedGroupPopover";
|
||||
import { SweatDrops } from "../Shared/SweatDrops";
|
||||
import { OCounterButton } from "../Shared/CountButton";
|
||||
|
||||
const Description: React.FC<{
|
||||
sceneNumber?: number;
|
||||
|
|
@ -111,16 +111,7 @@ export const GroupCard: React.FC<IProps> = ({
|
|||
function maybeRenderOCounter() {
|
||||
if (!group.o_counter) return;
|
||||
|
||||
return (
|
||||
<div className="o-counter">
|
||||
<Button className="minimal">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{group.o_counter}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return <OCounterButton value={group.o_counter} />;
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import {
|
|||
import { GroupEditPanel } from "./GroupEditPanel";
|
||||
import { faRefresh, faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { DetailImage } from "src/components/Shared/DetailImage";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||
|
|
@ -146,7 +146,7 @@ const GroupPage: React.FC<IProps> = ({ group, tabKey }) => {
|
|||
const Toast = useToast();
|
||||
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const uiConfig = configuration?.ui;
|
||||
const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false;
|
||||
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||
|
|
|
|||
|
|
@ -149,37 +149,44 @@ export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
|
|||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="name"
|
||||
title={intl.formatMessage({ id: "name" })}
|
||||
result={name}
|
||||
onChange={(value) => setName(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="aliases"
|
||||
title={intl.formatMessage({ id: "aliases" })}
|
||||
result={aliases}
|
||||
onChange={(value) => setAliases(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="duration"
|
||||
title={intl.formatMessage({ id: "duration" })}
|
||||
result={duration}
|
||||
onChange={(value) => setDuration(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="director"
|
||||
title={intl.formatMessage({ id: "director" })}
|
||||
result={director}
|
||||
onChange={(value) => setDirector(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="synopsis"
|
||||
title={intl.formatMessage({ id: "synopsis" })}
|
||||
result={synopsis}
|
||||
onChange={(value) => setSynopsis(value)}
|
||||
/>
|
||||
<ScrapedStudioRow
|
||||
field="studio"
|
||||
title={intl.formatMessage({ id: "studios" })}
|
||||
result={studio}
|
||||
onChange={(value) => setStudio(value)}
|
||||
|
|
@ -187,18 +194,21 @@ export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
|
|||
onCreateNew={createNewStudio}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="urls"
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={urls}
|
||||
onChange={(value) => setURLs(value)}
|
||||
/>
|
||||
{scrapedTagsRow}
|
||||
<ScrapedImageRow
|
||||
field="front_image"
|
||||
title="Front Image"
|
||||
className="group-image"
|
||||
result={frontImage}
|
||||
onChange={(value) => setFrontImage(value)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
field="back_image"
|
||||
title="Back Image"
|
||||
className="group-image"
|
||||
result={backImage}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
queryFindGroupsByIDForSelect,
|
||||
useGroupCreate,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -66,7 +66,7 @@ export const GroupSelect: React.FC<
|
|||
> = PatchComponent("GroupSelect", (props) => {
|
||||
const [createGroup] = useGroupCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useImagesDestroy } from "src/core/StashService";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
|
|
@ -33,7 +33,7 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
|
|||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(
|
||||
config?.defaults.deleteFile ?? false
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { GalleryLink, TagLink } from "src/components/Shared/TagLink";
|
||||
import { HoverPopover } from "src/components/Shared/HoverPopover";
|
||||
import { SweatDrops } from "src/components/Shared/SweatDrops";
|
||||
import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton";
|
||||
import { GridCard } from "src/components/Shared/GridCard/GridCard";
|
||||
import { RatingBanner } from "src/components/Shared/RatingBanner";
|
||||
|
|
@ -18,6 +17,7 @@ import {
|
|||
import { imageTitle } from "src/core/files";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
import { StudioOverlay } from "../Shared/GridCard/StudioOverlay";
|
||||
import { OCounterButton } from "../Shared/CountButton";
|
||||
|
||||
interface IImageCardProps {
|
||||
image: GQL.SlimImageDataFragment;
|
||||
|
|
@ -74,16 +74,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||
|
||||
function maybeRenderOCounter() {
|
||||
if (props.image.o_counter) {
|
||||
return (
|
||||
<div className="o-count">
|
||||
<Button className="minimal">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{props.image.o_counter}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return <OCounterButton value={props.image.o_counter} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Tab, Nav, Dropdown } from "react-bootstrap";
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
|
||||
import { useHistory, Link, RouteComponentProps } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
|
@ -29,7 +29,7 @@ import { imagePath, imageTitle } from "src/core/files";
|
|||
import { isVideo } from "src/utils/visualFile";
|
||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import cx from "classnames";
|
||||
|
|
@ -48,7 +48,7 @@ const ImagePage: React.FC<IProps> = ({ image }) => {
|
|||
const history = useHistory();
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const [incrementO] = useImageIncrementO(image.id);
|
||||
const [decrementO] = useImageDecrementO(image.id);
|
||||
|
|
|
|||
|
|
@ -163,32 +163,38 @@ export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
|
|||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="title"
|
||||
title={intl.formatMessage({ id: "title" })}
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="code"
|
||||
title={intl.formatMessage({ id: "scene_code" })}
|
||||
result={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="urls"
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={urls}
|
||||
onChange={(value) => setURLs(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="photographer"
|
||||
title={intl.formatMessage({ id: "photographer" })}
|
||||
result={photographer}
|
||||
onChange={(value) => setPhotographer(value)}
|
||||
/>
|
||||
<ScrapedStudioRow
|
||||
field="studio"
|
||||
title={intl.formatMessage({ id: "studios" })}
|
||||
result={studio}
|
||||
onChange={(value) => setStudio(value)}
|
||||
|
|
@ -196,6 +202,7 @@ export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
|
|||
onCreateNew={createNewStudio}
|
||||
/>
|
||||
<ScrapedPerformersRow
|
||||
field="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
result={performers}
|
||||
onChange={(value) => setPerformers(value)}
|
||||
|
|
@ -204,6 +211,7 @@ export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
|
|||
/>
|
||||
{scrapedTagsRow}
|
||||
<ScrapedTextAreaRow
|
||||
field="details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
useMemo,
|
||||
MouseEvent,
|
||||
useContext,
|
||||
} from "react";
|
||||
import React, { useCallback, useState, useMemo, MouseEvent } from "react";
|
||||
import { FormattedNumber, useIntl } from "react-intl";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
|
@ -23,7 +17,7 @@ import "flexbin/flexbin.css";
|
|||
import Gallery, { RenderImageProps } from "react-photo-gallery";
|
||||
import { ExportDialog } from "../Shared/ExportDialog";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { ImageGridCard } from "./ImageGridCard";
|
||||
import { View } from "../List/views";
|
||||
import { IItemListOperation } from "../List/FilteredListToolbar";
|
||||
|
|
@ -51,7 +45,7 @@ const ImageWall: React.FC<IImageWallProps> = ({
|
|||
zoomIndex,
|
||||
handleImageOpen,
|
||||
}) => {
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const uiConfig = configuration?.ui;
|
||||
|
||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
|
|
@ -14,7 +13,7 @@ import {
|
|||
CriterionOption,
|
||||
} from "src/models/list-filter/criteria/criterion";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { getFilterOptions } from "src/models/list-filter/factory";
|
||||
import { FilterTags } from "./FilterTags";
|
||||
|
|
@ -65,6 +64,9 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
|
|||
onTogglePin,
|
||||
externallySelected = false,
|
||||
}) => {
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const prevCriterion = usePrevious(currentCriterion);
|
||||
|
||||
const scrolled = useRef(false);
|
||||
|
|
@ -148,7 +150,9 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
|
|||
className="collapse-icon fa-fw"
|
||||
icon={type === c.type ? faChevronDown : faChevronRight}
|
||||
/>
|
||||
<FormattedMessage id={c.messageID} />
|
||||
<FormattedMessage
|
||||
id={!sfwContentMode ? c.messageID : c.sfwMessageID ?? c.messageID}
|
||||
/>
|
||||
</span>
|
||||
{criteria.some((cc) => c.type === cc) && (
|
||||
<Button
|
||||
|
|
@ -233,7 +237,8 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
|||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
|
||||
|
|
@ -265,10 +270,16 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
|||
.filter((c) => !c.hidden)
|
||||
.sort((a, b) => {
|
||||
return intl
|
||||
.formatMessage({ id: a.messageID })
|
||||
.localeCompare(intl.formatMessage({ id: b.messageID }));
|
||||
.formatMessage({
|
||||
id: !sfwContentMode ? a.messageID : a.sfwMessageID ?? a.messageID,
|
||||
})
|
||||
.localeCompare(
|
||||
intl.formatMessage({
|
||||
id: !sfwContentMode ? b.messageID : b.sfwMessageID ?? b.messageID,
|
||||
})
|
||||
);
|
||||
});
|
||||
}, [intl, filterOptions.criterionOptions]);
|
||||
}, [intl, sfwContentMode, filterOptions.criterionOptions]);
|
||||
|
||||
const optionSelected = useCallback(
|
||||
(option?: CriterionOption) => {
|
||||
|
|
@ -302,11 +313,13 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
|||
|
||||
return criterionOptions.filter((c) => {
|
||||
return intl
|
||||
.formatMessage({ id: c.messageID })
|
||||
.formatMessage({
|
||||
id: !sfwContentMode ? c.messageID : c.sfwMessageID ?? c.messageID,
|
||||
})
|
||||
.toLowerCase()
|
||||
.includes(trimmedSearch);
|
||||
});
|
||||
}, [intl, searchValue, criterionOptions]);
|
||||
}, [intl, sfwContentMode, searchValue, criterionOptions]);
|
||||
|
||||
const pinnedFilters = useMemo(
|
||||
() => ui.pinnedFilters?.[filterModeToConfigKey(currentFilter.mode)] ?? [],
|
||||
|
|
@ -517,7 +530,10 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
|
|||
<Modal
|
||||
show={!showSaveDialog && !showLoadDialog}
|
||||
onHide={() => onCancel()}
|
||||
className="edit-filter-dialog"
|
||||
// need sfw mode class because dialog is outside body
|
||||
className={cx("edit-filter-dialog", {
|
||||
"sfw-content-mode": sfwContentMode,
|
||||
})}
|
||||
>
|
||||
<Modal.Header>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { BsPrefixProps, ReplaceProps } from "react-bootstrap/esm/helpers";
|
|||
import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
import cx from "classnames";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
type TagItemProps = PropsWithChildren<
|
||||
ReplaceProps<"span", BsPrefixProps<"span"> & BadgeProps>
|
||||
|
|
@ -125,6 +126,9 @@ export const FilterTags: React.FC<IFilterTagsProps> = ({
|
|||
const intl = useIntl();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const [cutoff, setCutoff] = React.useState<number | undefined>();
|
||||
const elementGap = 10; // Adjust this value based on your CSS gap or margin
|
||||
const moreTagWidth = 80; // reserve space for the "more" tag
|
||||
|
|
@ -270,7 +274,7 @@ export const FilterTags: React.FC<IFilterTagsProps> = ({
|
|||
return (
|
||||
<FilterTag
|
||||
key={criterion.getId()}
|
||||
label={criterion.getLabel(intl)}
|
||||
label={criterion.getLabel(intl, sfwContentMode)}
|
||||
onClick={() => onClickCriterionTag(criterion)}
|
||||
onRemove={($event) => onRemoveCriterionTag(criterion, $event)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import { Form } from "react-bootstrap";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
import { CriterionModifier } from "src/core/generated-graphql";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
ModifierCriterion,
|
||||
CriterionValue,
|
||||
|
|
@ -17,7 +17,7 @@ export const PathFilter: React.FC<IInputFilterProps> = ({
|
|||
criterion,
|
||||
onValueChanged,
|
||||
}) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const libraryPaths = configuration?.general.stashes.map((s) => s.path);
|
||||
|
||||
// don't show folder select for regex
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
defaultRatingStarPrecision,
|
||||
defaultRatingSystemOptions,
|
||||
} from "src/utils/rating";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { RatingCriterion } from "src/models/list-filter/criteria/rating";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { Option, SidebarListFilter } from "./SidebarListFilter";
|
||||
|
|
@ -117,7 +117,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
|
|||
[noneLabel]
|
||||
);
|
||||
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const ratingSystemOptions =
|
||||
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
|
|
@ -43,7 +42,7 @@ import {
|
|||
IItemListOperation,
|
||||
} from "./FilteredListToolbar";
|
||||
import { PagedList } from "./PagedList";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> {
|
||||
filterStateProps: IFilterStateHook;
|
||||
|
|
@ -55,7 +54,7 @@ export function useFilteredItemList<
|
|||
T extends QueryResult,
|
||||
E extends IHasID = IHasID
|
||||
>(props: IFilteredItemList<T, E>) {
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
// States
|
||||
const filterState = useFilterState({
|
||||
|
|
@ -393,7 +392,7 @@ export const ItemListContext = <T extends QueryResult, E extends IHasID>(
|
|||
children,
|
||||
} = props;
|
||||
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const emptyFilter = useMemo(
|
||||
() =>
|
||||
|
|
@ -409,12 +408,7 @@ export const ItemListContext = <T extends QueryResult, E extends IHasID>(
|
|||
new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort })
|
||||
);
|
||||
|
||||
const { defaultFilter, loading: defaultFilterLoading } = useDefaultFilter(
|
||||
emptyFilter,
|
||||
view
|
||||
);
|
||||
|
||||
if (defaultFilterLoading) return null;
|
||||
const { defaultFilter } = useDefaultFilter(emptyFilter, view);
|
||||
|
||||
return (
|
||||
<FilterContext filter={filter} setFilter={setFilterState}>
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { View } from "./views";
|
|||
import { ClearableInput } from "../Shared/ClearableInput";
|
||||
import { useStopWheelScroll } from "src/utils/form";
|
||||
import { ISortByOption } from "src/models/list-filter/filter-options";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
export function useDebouncedSearchInput(
|
||||
filter: ListFilterModel,
|
||||
|
|
@ -249,14 +250,24 @@ export const SortBySelect: React.FC<{
|
|||
onReshuffleRandomSort,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const currentSortBy = options.find((o) => o.value === sortBy);
|
||||
const currentSortByMessageID = currentSortBy
|
||||
? !sfwContentMode
|
||||
? currentSortBy.messageID
|
||||
: currentSortBy.sfwMessageID ?? currentSortBy.messageID
|
||||
: "";
|
||||
|
||||
function renderSortByOptions() {
|
||||
return options
|
||||
.map((o) => {
|
||||
const messageID = !sfwContentMode
|
||||
? o.messageID
|
||||
: o.sfwMessageID ?? o.messageID;
|
||||
return {
|
||||
message: intl.formatMessage({ id: o.messageID }),
|
||||
message: intl.formatMessage({ id: messageID }),
|
||||
value: o.value,
|
||||
};
|
||||
})
|
||||
|
|
@ -267,6 +278,7 @@ export const SortBySelect: React.FC<{
|
|||
key={option.value}
|
||||
className="bg-secondary text-white"
|
||||
eventKey={option.value}
|
||||
data-value={option.value}
|
||||
>
|
||||
{option.message}
|
||||
</Dropdown.Item>
|
||||
|
|
@ -274,11 +286,11 @@ export const SortBySelect: React.FC<{
|
|||
}
|
||||
|
||||
return (
|
||||
<Dropdown as={ButtonGroup} className={className}>
|
||||
<Dropdown as={ButtonGroup} className={`${className ?? ""} sort-by-select`}>
|
||||
<InputGroup.Prepend>
|
||||
<Dropdown.Toggle variant="secondary">
|
||||
{currentSortBy
|
||||
? intl.formatMessage({ id: currentSortBy.messageID })
|
||||
? intl.formatMessage({ id: currentSortByMessageID })
|
||||
: ""}
|
||||
</Dropdown.Toggle>
|
||||
</InputGroup.Prepend>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { useHistory, useLocation } from "react-router-dom";
|
||||
import { isEqual, isFunction } from "lodash-es";
|
||||
import { QueryResult } from "@apollo/client";
|
||||
import { IHasID } from "src/utils/data";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { View } from "./views";
|
||||
import { usePrevious } from "src/hooks/state";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
|
@ -94,7 +94,7 @@ export function useFilterURL(
|
|||
}
|
||||
|
||||
export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) {
|
||||
const { configuration: config, loading } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const defaultFilter = useMemo(() => {
|
||||
if (view && config?.ui.defaultFilters?.[view]) {
|
||||
|
|
@ -114,9 +114,9 @@ export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) {
|
|||
}
|
||||
}, [view, config?.ui.defaultFilters, emptyFilter]);
|
||||
|
||||
const retFilter = loading ? undefined : defaultFilter ?? emptyFilter;
|
||||
const retFilter = defaultFilter ?? emptyFilter;
|
||||
|
||||
return { defaultFilter: retFilter, loading };
|
||||
return { defaultFilter: retFilter };
|
||||
}
|
||||
|
||||
function useEmptyFilter(props: {
|
||||
|
|
@ -158,14 +158,14 @@ export function useFilterState(
|
|||
|
||||
const emptyFilter = useEmptyFilter({ filterMode, defaultSort, config });
|
||||
|
||||
const { defaultFilter, loading } = useDefaultFilter(emptyFilter, view);
|
||||
const { defaultFilter } = useDefaultFilter(emptyFilter, view);
|
||||
|
||||
const { setFilter } = useFilterURL(filter, setFilterState, {
|
||||
defaultFilter,
|
||||
active: useURL,
|
||||
});
|
||||
|
||||
return { loading, filter, setFilter };
|
||||
return { filter, setFilter };
|
||||
}
|
||||
|
||||
export function useFilterOperations(props: {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import {
|
|||
MessageDescriptor,
|
||||
useIntl,
|
||||
} from "react-intl";
|
||||
import { Nav, Navbar, Button, Fade } from "react-bootstrap";
|
||||
import { Nav, Navbar, Button } from "react-bootstrap";
|
||||
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
|
||||
import { LinkContainer } from "react-router-bootstrap";
|
||||
import { Link, NavLink, useLocation, useHistory } from "react-router-dom";
|
||||
|
|
@ -19,7 +19,7 @@ import Mousetrap from "mousetrap";
|
|||
|
||||
import SessionUtils from "src/utils/session";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { ManualStateContext } from "./Help/context";
|
||||
import { SettingsButton } from "./SettingsButton";
|
||||
import {
|
||||
|
|
@ -181,7 +181,7 @@ const MainNavbarUtilityItems = PatchComponent(
|
|||
export const MainNavbar: React.FC = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { configuration, loading } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { openManual } = React.useContext(ManualStateContext);
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
|
@ -359,35 +359,31 @@ export const MainNavbar: React.FC = () => {
|
|||
ref={navbarRef}
|
||||
>
|
||||
<Navbar.Collapse className="bg-dark order-sm-1">
|
||||
<Fade in={!loading}>
|
||||
<>
|
||||
<MainNavbarMenuItems>
|
||||
{menuItems.map(({ href, icon, message }) => (
|
||||
<Nav.Link
|
||||
eventKey={href}
|
||||
as="div"
|
||||
key={href}
|
||||
className="col-4 col-sm-3 col-md-2 col-lg-auto"
|
||||
>
|
||||
<LinkContainer activeClassName="active" exact to={href}>
|
||||
<Button className="minimal p-4 p-xl-2 d-flex d-xl-inline-block flex-column justify-content-between align-items-center">
|
||||
<Icon
|
||||
{...{ icon }}
|
||||
className="nav-menu-icon d-block d-xl-inline mb-2 mb-xl-0"
|
||||
/>
|
||||
<span>{intl.formatMessage(message)}</span>
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</Nav.Link>
|
||||
))}
|
||||
</MainNavbarMenuItems>
|
||||
<Nav>
|
||||
<MainNavbarUtilityItems>
|
||||
{renderUtilityButtons()}
|
||||
</MainNavbarUtilityItems>
|
||||
</Nav>
|
||||
</>
|
||||
</Fade>
|
||||
<MainNavbarMenuItems>
|
||||
{menuItems.map(({ href, icon, message }) => (
|
||||
<Nav.Link
|
||||
eventKey={href}
|
||||
as="div"
|
||||
key={href}
|
||||
className="col-4 col-sm-3 col-md-2 col-lg-auto"
|
||||
>
|
||||
<LinkContainer activeClassName="active" exact to={href}>
|
||||
<Button className="minimal p-4 p-xl-2 d-flex d-xl-inline-block flex-column justify-content-between align-items-center">
|
||||
<Icon
|
||||
{...{ icon }}
|
||||
className="nav-menu-icon d-block d-xl-inline mb-2 mb-xl-0"
|
||||
/>
|
||||
<span>{intl.formatMessage(message)}</span>
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
</Nav.Link>
|
||||
))}
|
||||
</MainNavbarMenuItems>
|
||||
<Nav>
|
||||
<MainNavbarUtilityItems>
|
||||
{renderUtilityButtons()}
|
||||
</MainNavbarUtilityItems>
|
||||
</Nav>
|
||||
</Navbar.Collapse>
|
||||
|
||||
<Navbar.Brand as="div" onClick={handleDismiss}>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
|
|||
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import * as FormUtils from "src/utils/form";
|
||||
import { CountrySelect } from "../Shared/CountrySelect";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import cx from "classnames";
|
||||
|
||||
interface IListOperationProps {
|
||||
selected: GQL.SlimPerformerDataFragment[];
|
||||
|
|
@ -61,6 +63,10 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
|
||||
mode: GQL.BulkUpdateIdMode.Add,
|
||||
});
|
||||
|
|
@ -204,7 +210,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
setter: (newValue: string | undefined) => void
|
||||
) {
|
||||
return (
|
||||
<Form.Group controlId={name}>
|
||||
<Form.Group controlId={name} data-field={name}>
|
||||
<Form.Label>
|
||||
<FormattedMessage id={name} />
|
||||
</Form.Label>
|
||||
|
|
@ -218,9 +224,13 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
}
|
||||
|
||||
function render() {
|
||||
// sfw class needs to be set because it is outside body
|
||||
|
||||
return (
|
||||
<ModalComponent
|
||||
dialogClassName="edit-performers-dialog"
|
||||
dialogClassName={cx("edit-performers-dialog", {
|
||||
"sfw-content-mode": sfwContentMode,
|
||||
})}
|
||||
show
|
||||
icon={faPencilAlt}
|
||||
header={intl.formatMessage(
|
||||
|
|
@ -238,7 +248,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
}}
|
||||
isRunning={isUpdating}
|
||||
>
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
<Form.Group controlId="rating" as={Row} data-field={name}>
|
||||
{FormUtils.renderLabel({
|
||||
title: intl.formatMessage({ id: "rating" }),
|
||||
})}
|
||||
|
|
@ -322,7 +332,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||
setPenisLength(v)
|
||||
)}
|
||||
|
||||
<Form.Group>
|
||||
<Form.Group data-field="circumcised">
|
||||
<Form.Label>
|
||||
<FormattedMessage id="circumcised" />
|
||||
</Form.Label>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import NavUtils from "src/utils/navigation";
|
|||
import TextUtils from "src/utils/text";
|
||||
import { GridCard } from "../Shared/GridCard/GridCard";
|
||||
import { CountryFlag } from "../Shared/CountryFlag";
|
||||
import { SweatDrops } from "../Shared/SweatDrops";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
|
|
@ -25,7 +24,8 @@ import { ILabeledId } from "src/models/list-filter/types";
|
|||
import { FavoriteIcon } from "../Shared/FavoriteIcon";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { ExternalLinksButton } from "../Shared/ExternalLinksButton";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { OCounterButton } from "../Shared/CountButton";
|
||||
|
||||
export interface IPerformerCardExtraCriteria {
|
||||
scenes?: ModifierCriterion<CriterionValue>[];
|
||||
|
|
@ -103,16 +103,7 @@ const PerformerCardPopovers: React.FC<IPerformerCardProps> = PatchComponent(
|
|||
function maybeRenderOCounter() {
|
||||
if (!performer.o_counter) return;
|
||||
|
||||
return (
|
||||
<div className="o-counter">
|
||||
<Button className="minimal">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{performer.o_counter}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return <OCounterButton value={performer.o_counter} />;
|
||||
}
|
||||
|
||||
function maybeRenderTagPopoverButton() {
|
||||
|
|
@ -179,7 +170,7 @@ const PerformerCardPopovers: React.FC<IPerformerCardProps> = PatchComponent(
|
|||
const PerformerCardOverlays: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard.Overlays",
|
||||
({ performer }) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const uiConfig = configuration?.ui;
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
|||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
import {
|
||||
CompressedPerformerDetailsPanel,
|
||||
|
|
@ -42,13 +42,13 @@ import {
|
|||
import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle";
|
||||
import { ExpandCollapseButton } from "src/components/Shared/CollapseButton";
|
||||
import { FavoriteIcon } from "src/components/Shared/FavoriteIcon";
|
||||
import { SweatDrops } from "src/components/Shared/SweatDrops";
|
||||
import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
|
||||
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
|
||||
import { LightboxLink } from "src/hooks/Lightbox/LightboxLink";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { ILightboxImage } from "src/hooks/Lightbox/types";
|
||||
import { goBackOrReplace } from "src/utils/history";
|
||||
import { OCounterButton } from "src/components/Shared/CountButton";
|
||||
|
||||
interface IProps {
|
||||
performer: GQL.PerformerDataFragment;
|
||||
|
|
@ -240,7 +240,7 @@ const PerformerPage: React.FC<IProps> = PatchComponent(
|
|||
const intl = useIntl();
|
||||
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const uiConfig = configuration?.ui;
|
||||
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||
const enableBackgroundImage =
|
||||
|
|
@ -432,12 +432,7 @@ const PerformerPage: React.FC<IProps> = PatchComponent(
|
|||
withoutContext
|
||||
/>
|
||||
{!!performer.o_counter && (
|
||||
<span className="o-counter">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{performer.o_counter}</span>
|
||||
</span>
|
||||
<OCounterButton value={performer.o_counter} />
|
||||
)}
|
||||
</div>
|
||||
{!isEditing && (
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import {
|
|||
stringCircumMap,
|
||||
stringToCircumcised,
|
||||
} from "src/utils/circumcised";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
|
||||
import PerformerScrapeModal from "./PerformerScrapeModal";
|
||||
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
|
||||
|
|
@ -97,7 +97,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||
|
||||
const [scrapedPerformer, setScrapedPerformer] =
|
||||
useState<GQL.ScrapedPerformer>();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
|
||||
const intl = useIntl();
|
||||
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ function renderScrapedGenderRow(
|
|||
) {
|
||||
return (
|
||||
<ScrapeDialogRow
|
||||
field="gender"
|
||||
title={title}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedGender(result)}
|
||||
|
|
@ -113,6 +114,7 @@ function renderScrapedCircumcisedRow(
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
field="circumcised"
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedCircumcised(result)}
|
||||
renderNewField={() =>
|
||||
|
|
@ -401,16 +403,19 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="name"
|
||||
title={intl.formatMessage({ id: "name" })}
|
||||
result={name}
|
||||
onChange={(value) => setName(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="disambiguation"
|
||||
title={intl.formatMessage({ id: "disambiguation" })}
|
||||
result={disambiguation}
|
||||
onChange={(value) => setDisambiguation(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="aliases"
|
||||
title={intl.formatMessage({ id: "aliases" })}
|
||||
result={aliases}
|
||||
onChange={(value) => setAliases(value)}
|
||||
|
|
@ -421,46 +426,55 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
(value) => setGender(value)
|
||||
)}
|
||||
<ScrapedInputGroupRow
|
||||
field="birthdate"
|
||||
title={intl.formatMessage({ id: "birthdate" })}
|
||||
result={birthdate}
|
||||
onChange={(value) => setBirthdate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="death_date"
|
||||
title={intl.formatMessage({ id: "death_date" })}
|
||||
result={deathDate}
|
||||
onChange={(value) => setDeathDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="ethnicity"
|
||||
title={intl.formatMessage({ id: "ethnicity" })}
|
||||
result={ethnicity}
|
||||
onChange={(value) => setEthnicity(value)}
|
||||
/>
|
||||
<ScrapedCountryRow
|
||||
field="country"
|
||||
title={intl.formatMessage({ id: "country" })}
|
||||
result={country}
|
||||
onChange={(value) => setCountry(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="hair_color"
|
||||
title={intl.formatMessage({ id: "hair_color" })}
|
||||
result={hairColor}
|
||||
onChange={(value) => setHairColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="eye_color"
|
||||
title={intl.formatMessage({ id: "eye_color" })}
|
||||
result={eyeColor}
|
||||
onChange={(value) => setEyeColor(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="weight"
|
||||
title={intl.formatMessage({ id: "weight" })}
|
||||
result={weight}
|
||||
onChange={(value) => setWeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="height"
|
||||
title={intl.formatMessage({ id: "height" })}
|
||||
result={height}
|
||||
onChange={(value) => setHeight(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="penis_length"
|
||||
title={intl.formatMessage({ id: "penis_length" })}
|
||||
result={penisLength}
|
||||
onChange={(value) => setPenisLength(value)}
|
||||
|
|
@ -471,42 +485,50 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
(value) => setCircumcised(value)
|
||||
)}
|
||||
<ScrapedInputGroupRow
|
||||
field="measurements"
|
||||
title={intl.formatMessage({ id: "measurements" })}
|
||||
result={measurements}
|
||||
onChange={(value) => setMeasurements(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="fake_tits"
|
||||
title={intl.formatMessage({ id: "fake_tits" })}
|
||||
result={fakeTits}
|
||||
onChange={(value) => setFakeTits(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="career_length"
|
||||
title={intl.formatMessage({ id: "career_length" })}
|
||||
result={careerLength}
|
||||
onChange={(value) => setCareerLength(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="tattoos"
|
||||
title={intl.formatMessage({ id: "tattoos" })}
|
||||
result={tattoos}
|
||||
onChange={(value) => setTattoos(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="piercings"
|
||||
title={intl.formatMessage({ id: "piercings" })}
|
||||
result={piercings}
|
||||
onChange={(value) => setPiercings(value)}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="urls"
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={urls}
|
||||
onChange={(value) => setURLs(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
{scrapedTagsRow}
|
||||
<ScrapedImagesRow
|
||||
field="image"
|
||||
title={intl.formatMessage({ id: "performer_image" })}
|
||||
className="performer-image"
|
||||
result={image}
|
||||
|
|
@ -514,6 +536,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||
onChange={(value) => setImage(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="remote_site_id"
|
||||
title={intl.formatMessage({ id: "stash_id" })}
|
||||
result={remoteSiteID}
|
||||
locked
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
|||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { useFindPerformer } from "../../core/StashService";
|
||||
import { PerformerCard } from "./PerformerCard";
|
||||
import { ConfigurationContext } from "../../hooks/Config";
|
||||
import { useConfigurationContext } from "../../hooks/Config";
|
||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
|
||||
interface IPeromerPopoverCardProps {
|
||||
|
|
@ -49,7 +49,7 @@ export const PerformerPopover: React.FC<IPeroformerPopoverProps> = ({
|
|||
placement = "top",
|
||||
target,
|
||||
}) => {
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
queryFindPerformersByIDForSelect,
|
||||
queryFindPerformersForSelect,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -82,7 +82,7 @@ const _PerformerSelect: React.FC<
|
|||
> = (props) => {
|
||||
const [createPerformer] = usePerformerCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React, {
|
||||
KeyboardEvent,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
|
|
@ -31,7 +30,7 @@ import {
|
|||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
ConnectionState,
|
||||
InteractiveContext,
|
||||
|
|
@ -240,7 +239,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = PatchComponent(
|
|||
onNext,
|
||||
onPrevious,
|
||||
}) => {
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const interfaceConfig = configuration?.interface;
|
||||
const uiConfig = configuration?.ui;
|
||||
const videoRef = useRef<HTMLDivElement>(null);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useScenesDestroy } from "src/core/StashService";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { objectPath } from "src/core/files";
|
||||
|
|
@ -34,7 +34,7 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
|
|||
{ count: props.selected.length, singularEntity, pluralEntity }
|
||||
);
|
||||
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const [deleteFile, setDeleteFile] = useState<boolean>(
|
||||
config?.defaults.deleteFile ?? false
|
||||
|
|
|
|||
|
|
@ -6,12 +6,11 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { Icon } from "../Shared/Icon";
|
||||
import { GalleryLink, TagLink, SceneMarkerLink } from "../Shared/TagLink";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { SweatDrops } from "../Shared/SweatDrops";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
import { GridCard } from "../Shared/GridCard/GridCard";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
|
|
@ -30,6 +29,7 @@ import { PatchComponent } from "src/patch";
|
|||
import { StudioOverlay } from "../Shared/GridCard/StudioOverlay";
|
||||
import { GroupTag } from "../Groups/GroupTag";
|
||||
import { FileSize } from "../Shared/FileSize";
|
||||
import { OCounterButton } from "../Shared/CountButton";
|
||||
|
||||
interface IScenePreviewProps {
|
||||
isPortrait: boolean;
|
||||
|
|
@ -218,16 +218,7 @@ const SceneCardPopovers = PatchComponent(
|
|||
|
||||
function maybeRenderOCounter() {
|
||||
if (props.scene.o_counter) {
|
||||
return (
|
||||
<div className="o-count">
|
||||
<Button className="minimal">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{props.scene.o_counter}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
return <OCounterButton value={props.scene.o_counter} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -353,7 +344,7 @@ const SceneCardImage = PatchComponent(
|
|||
"SceneCard.Image",
|
||||
(props: ISceneCardProps) => {
|
||||
const history = useHistory();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const cont = configuration?.interface.continuePlaylistDefault ?? false;
|
||||
|
||||
const file = useMemo(
|
||||
|
|
@ -437,7 +428,7 @@ const SceneCardImage = PatchComponent(
|
|||
export const SceneCard = PatchComponent(
|
||||
"SceneCard",
|
||||
(props: ISceneCardProps) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const file = useMemo(
|
||||
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { faBan, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBan, faMinus, faThumbsUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import React, { useState } from "react";
|
||||
import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { SweatDrops } from "src/components/Shared/SweatDrops";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
export interface IOCounterButtonProps {
|
||||
value: number;
|
||||
|
|
@ -17,6 +18,12 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
|||
props: IOCounterButtonProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const icon = !sfwContentMode ? <SweatDrops /> : <Icon icon={faThumbsUp} />;
|
||||
const messageID = !sfwContentMode ? "o_count" : "o_count_sfw";
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function increment() {
|
||||
|
|
@ -44,9 +51,9 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
|||
className="minimal pr-1"
|
||||
onClick={increment}
|
||||
variant="secondary"
|
||||
title={intl.formatMessage({ id: "o_counter" })}
|
||||
title={intl.formatMessage({ id: messageID })}
|
||||
>
|
||||
<SweatDrops />
|
||||
{icon}
|
||||
<span className="ml-2">{props.value}</span>
|
||||
</Button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import React, {
|
|||
useEffect,
|
||||
useState,
|
||||
useMemo,
|
||||
useContext,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
|
|
@ -32,7 +31,7 @@ import SceneQueue, { QueuedScene } from "src/models/sceneQueue";
|
|||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import Mousetrap from "mousetrap";
|
||||
import { OrganizedButton } from "./OrganizedButton";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { getPlayerPosition } from "src/components/ScenePlayer/util";
|
||||
import {
|
||||
faEllipsisV,
|
||||
|
|
@ -184,7 +183,7 @@ const ScenePage: React.FC<IProps> = PatchComponent("ScenePage", (props) => {
|
|||
const intl = useIntl();
|
||||
const [updateScene] = useSceneUpdate();
|
||||
const [generateScreenshot] = useSceneGenerateScreenshot();
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const [showDraftModal, setShowDraftModal] = useState(false);
|
||||
const boxes = configuration?.general?.stashBoxes ?? [];
|
||||
|
|
@ -689,7 +688,7 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
|
|||
match,
|
||||
}) => {
|
||||
const { id } = match.params;
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { data, loading, error } = useFindScene(id);
|
||||
|
||||
const [scene, setScene] = useState<GQL.SceneDataFragment>();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import ImageUtils from "src/utils/image";
|
|||
import { getStashIDs } from "src/utils/stashIds";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router-dom";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
|
||||
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||
import { objectTitle } from "src/core/files";
|
||||
|
|
@ -103,7 +103,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||
setStudio(scene.studio ?? null);
|
||||
}, [scene.studio]);
|
||||
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
useSceneResetActivity,
|
||||
} from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { TextField } from "src/utils/field";
|
||||
import TextUtils from "src/utils/text";
|
||||
|
|
@ -172,6 +173,9 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
|
|||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const [dialogs, setDialogs] = React.useState({
|
||||
playHistory: false,
|
||||
oHistory: false,
|
||||
|
|
@ -299,6 +303,9 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
|
|||
}
|
||||
|
||||
function maybeRenderDialogs() {
|
||||
const clearHistoryMessageID = sfwContentMode
|
||||
? "dialogs.clear_o_history_confirm_sfw"
|
||||
: "dialogs.clear_play_history_confirm";
|
||||
return (
|
||||
<>
|
||||
<AlertModal
|
||||
|
|
@ -312,7 +319,7 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
|
|||
/>
|
||||
<AlertModal
|
||||
show={dialogs.oHistory}
|
||||
text={intl.formatMessage({ id: "dialogs.clear_o_history_confirm" })}
|
||||
text={intl.formatMessage({ id: clearHistoryMessageID })}
|
||||
confirmButtonText={intl.formatMessage({ id: "actions.clear" })}
|
||||
onConfirm={() => handleClearODates()}
|
||||
onCancel={() => setDialogPartial({ oHistory: false })}
|
||||
|
|
@ -351,6 +358,11 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
|
|||
) as string[];
|
||||
const oHistory = (scene.o_history ?? []).filter((h) => h != null) as string[];
|
||||
|
||||
const oHistoryMessageID = sfwContentMode ? "o_history_sfw" : "o_history";
|
||||
const noneMessageID = sfwContentMode
|
||||
? "odate_recorded_no_sfw"
|
||||
: "odate_recorded_no";
|
||||
|
||||
return (
|
||||
<div>
|
||||
{maybeRenderDialogs()}
|
||||
|
|
@ -401,7 +413,7 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
|
|||
<div className="history-header">
|
||||
<h5>
|
||||
<span>
|
||||
<FormattedMessage id="o_history" />
|
||||
<FormattedMessage id={oHistoryMessageID} />
|
||||
<Counter count={oHistory.length} hideZero />
|
||||
</span>
|
||||
<span>
|
||||
|
|
@ -427,7 +439,7 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
|
|||
</div>
|
||||
<History
|
||||
history={oHistory}
|
||||
noneID="odate_recorded_no"
|
||||
noneID={noneMessageID}
|
||||
unknownDate={scene.created_at}
|
||||
onRemove={(t) => handleDeleteODate(t)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -218,32 +218,38 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="title"
|
||||
title={intl.formatMessage({ id: "title" })}
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="code"
|
||||
title={intl.formatMessage({ id: "scene_code" })}
|
||||
result={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="urls"
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={urls}
|
||||
onChange={(value) => setURLs(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="director"
|
||||
title={intl.formatMessage({ id: "director" })}
|
||||
result={director}
|
||||
onChange={(value) => setDirector(value)}
|
||||
/>
|
||||
<ScrapedStudioRow
|
||||
field="studio"
|
||||
title={intl.formatMessage({ id: "studios" })}
|
||||
result={studio}
|
||||
onChange={(value) => setStudio(value)}
|
||||
|
|
@ -251,6 +257,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||
onCreateNew={createNewStudio}
|
||||
/>
|
||||
<ScrapedPerformersRow
|
||||
field="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
result={performers}
|
||||
onChange={(value) => setPerformers(value)}
|
||||
|
|
@ -259,6 +266,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||
ageFromDate={date.useNewValue ? date.newValue : date.originalValue}
|
||||
/>
|
||||
<ScrapedGroupsRow
|
||||
field="groups"
|
||||
title={intl.formatMessage({ id: "groups" })}
|
||||
result={groups}
|
||||
onChange={(value) => setGroups(value)}
|
||||
|
|
@ -267,17 +275,20 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||
/>
|
||||
{scrapedTagsRow}
|
||||
<ScrapedTextAreaRow
|
||||
field="details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="stash_ids"
|
||||
title={intl.formatMessage({ id: "stash_id" })}
|
||||
result={stashID}
|
||||
locked
|
||||
onChange={(value) => setStashID(value)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
field="cover_image"
|
||||
title={intl.formatMessage({ id: "cover_image" })}
|
||||
className="scene-cover"
|
||||
result={image}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useContext, useEffect, useMemo } from "react";
|
||||
import React, { useCallback, useEffect, useMemo } from "react";
|
||||
import cloneDeep from "lodash-es/cloneDeep";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { useHistory } from "react-router-dom";
|
||||
|
|
@ -18,7 +18,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
|
|||
import { SceneCardsGrid } from "./SceneCardsGrid";
|
||||
import { TaggerContext } from "../Tagger/context";
|
||||
import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
faPencil,
|
||||
faPlay,
|
||||
|
|
@ -110,7 +110,7 @@ function renderMetadataByline(result: GQL.FindScenesQueryResult) {
|
|||
function usePlayScene() {
|
||||
const history = useHistory();
|
||||
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const cont = config?.interface.continuePlaylistDefault ?? false;
|
||||
const autoPlay = config?.interface.autostartVideoOnPlaySelected ?? false;
|
||||
|
||||
|
|
@ -502,7 +502,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
},
|
||||
});
|
||||
|
||||
const { filter, setFilter, loading: filterLoading } = filterState;
|
||||
const { filter, setFilter } = filterState;
|
||||
|
||||
const { effectiveFilter, result, cachedResult, items, totalCount } =
|
||||
queryResult;
|
||||
|
|
@ -709,7 +709,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
|
|||
];
|
||||
|
||||
// render
|
||||
if (filterLoading || sidebarStateLoading) return null;
|
||||
if (sidebarStateLoading) return null;
|
||||
|
||||
const operations = (
|
||||
<SceneListOperations
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
|
|
@ -6,7 +6,7 @@ import { TagLink } from "../Shared/TagLink";
|
|||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { GridCard } from "../Shared/GridCard/GridCard";
|
||||
import { faTag } from "@fortawesome/free-solid-svg-icons";
|
||||
import { markerTitle } from "src/core/markers";
|
||||
|
|
@ -109,7 +109,7 @@ const SceneMarkerCardDetails = (props: ISceneMarkerCardProps) => {
|
|||
};
|
||||
|
||||
const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const file = useMemo(
|
||||
() =>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import Gallery, {
|
||||
GalleryI,
|
||||
PhotoProps,
|
||||
RenderImageProps,
|
||||
} from "react-photo-gallery";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
|
|
@ -46,7 +40,7 @@ interface IExtraProps {
|
|||
export const MarkerWallItem: React.FC<
|
||||
RenderImageProps<IMarkerPhoto> & IExtraProps
|
||||
> = (props: RenderImageProps<IMarkerPhoto> & IExtraProps) => {
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const playSound = configuration?.interface.soundOnPreview ?? false;
|
||||
const showTitle = configuration?.interface.wallShowTitle ?? false;
|
||||
|
||||
|
|
|
|||
|
|
@ -372,27 +372,32 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
return (
|
||||
<>
|
||||
<ScrapedInputGroupRow
|
||||
field="title"
|
||||
title={intl.formatMessage({ id: "title" })}
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="code"
|
||||
title={intl.formatMessage({ id: "scene_code" })}
|
||||
result={code}
|
||||
onChange={(value) => setCode(value)}
|
||||
/>
|
||||
<ScrapedStringListRow
|
||||
field="urls"
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
field="date"
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
placeholder="YYYY-MM-DD"
|
||||
result={date}
|
||||
onChange={(value) => setDate(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="rating"
|
||||
title={intl.formatMessage({ id: "rating" })}
|
||||
result={rating}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -404,6 +409,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setRating(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="o_count"
|
||||
title={intl.formatMessage({ id: "o_count" })}
|
||||
result={oCounter}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -425,6 +431,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setOCounter(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="play_count"
|
||||
title={intl.formatMessage({ id: "play_count" })}
|
||||
result={playCount}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -446,6 +453,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setPlayCount(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="play_duration"
|
||||
title={intl.formatMessage({ id: "play_duration" })}
|
||||
result={playDuration}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -469,6 +477,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setPlayDuration(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="galleries"
|
||||
title={intl.formatMessage({ id: "galleries" })}
|
||||
result={galleries}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -492,32 +501,38 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setGalleries(value)}
|
||||
/>
|
||||
<ScrapedStudioRow
|
||||
field="studio"
|
||||
title={intl.formatMessage({ id: "studios" })}
|
||||
result={studio}
|
||||
onChange={(value) => setStudio(value)}
|
||||
/>
|
||||
<ScrapedPerformersRow
|
||||
field="performers"
|
||||
title={intl.formatMessage({ id: "performers" })}
|
||||
result={performers}
|
||||
onChange={(value) => setPerformers(value)}
|
||||
ageFromDate={date.useNewValue ? date.newValue : date.originalValue}
|
||||
/>
|
||||
<ScrapedGroupsRow
|
||||
field="groups"
|
||||
title={intl.formatMessage({ id: "groups" })}
|
||||
result={groups}
|
||||
onChange={(value) => setGroups(value)}
|
||||
/>
|
||||
<ScrapedTagsRow
|
||||
field="tags"
|
||||
title={intl.formatMessage({ id: "tags" })}
|
||||
result={tags}
|
||||
onChange={(value) => setTags(value)}
|
||||
/>
|
||||
<ScrapedTextAreaRow
|
||||
field="details"
|
||||
title={intl.formatMessage({ id: "details" })}
|
||||
result={details}
|
||||
onChange={(value) => setDetails(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="organized"
|
||||
title={intl.formatMessage({ id: "organized" })}
|
||||
result={organized}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -539,6 +554,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setOrganized(value)}
|
||||
/>
|
||||
<ScrapeDialogRow
|
||||
field="stash_ids"
|
||||
title={intl.formatMessage({ id: "stash_id" })}
|
||||
result={stashIDs}
|
||||
renderOriginalField={() => (
|
||||
|
|
@ -550,6 +566,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||
onChange={(value) => setStashIDs(value)}
|
||||
/>
|
||||
<ScrapedImageRow
|
||||
field="cover_image"
|
||||
title={intl.formatMessage({ id: "cover_image" })}
|
||||
className="scene-cover"
|
||||
result={image}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
queryFindScenesForSelect,
|
||||
queryFindScenesByIDForSelect,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -66,7 +66,7 @@ const sceneSelectSort = PatchFunction(
|
|||
const _SceneSelect: React.FC<
|
||||
IFilterProps & IFilterValueProps<Scene> & ExtraSceneProps
|
||||
> = (props) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import Gallery, {
|
||||
|
|
@ -12,7 +6,7 @@ import Gallery, {
|
|||
PhotoProps,
|
||||
RenderImageProps,
|
||||
} from "react-photo-gallery";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
|
|
@ -35,7 +29,7 @@ export const SceneWallItem: React.FC<
|
|||
> = (props: RenderImageProps<IScenePhoto> & IExtraProps) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const playSound = configuration?.interface.soundOnPreview ?? false;
|
||||
const showTitle = configuration?.interface.wallShowTitle ?? false;
|
||||
|
||||
|
|
|
|||
|
|
@ -240,6 +240,14 @@ export const SettingsInterfacePanel: React.FC = PatchComponent(
|
|||
<option value="zh-CN">简体中文 (中国)</option>
|
||||
</SelectSetting>
|
||||
|
||||
<BooleanSetting
|
||||
id="sfw-content-mode"
|
||||
headingID="config.ui.sfw_mode.heading"
|
||||
subHeadingID="config.ui.sfw_mode.description"
|
||||
checked={iface.sfwContentMode ?? undefined}
|
||||
onChange={(v) => saveInterface({ sfwContentMode: v })}
|
||||
/>
|
||||
|
||||
<div className="setting-group">
|
||||
<div className="setting">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import { SettingSection } from "../SettingSection";
|
|||
import { BooleanSetting, Setting } from "../Inputs";
|
||||
import { ManualLink } from "src/components/Help/context";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
import {
|
||||
faMinus,
|
||||
|
|
@ -44,7 +44,7 @@ const CleanDialog: React.FC<ICleanDialog> = ({
|
|||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const libraryPaths = configuration?.general.stashes.map((s) => s.path);
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useIntl } from "react-intl";
|
|||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { ModalComponent } from "src/components/Shared/Modal";
|
||||
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IDirectorySelectionDialogProps {
|
||||
animation?: boolean;
|
||||
|
|
@ -22,7 +22,7 @@ export const DirectorySelectionDialog: React.FC<
|
|||
IDirectorySelectionDialogProps
|
||||
> = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const libraryPaths = configuration?.general.stashes.map((s) => s.path);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import {
|
|||
mutateMetadataGenerate,
|
||||
} from "src/core/StashService";
|
||||
import { withoutTypename } from "src/utils/data";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { IdentifyDialog } from "../../Dialogs/IdentifyDialog/IdentifyDialog";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { DirectorySelectionDialog } from "./DirectorySelectionDialog";
|
||||
|
|
@ -123,7 +123,7 @@ export const LibraryTasks: React.FC = () => {
|
|||
|
||||
type DialogOpenState = typeof dialogOpen;
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const [configRead, setConfigRead] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useContext, useCallback } from "react";
|
||||
import React, { useState, useCallback } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import {
|
||||
Alert,
|
||||
|
|
@ -15,7 +15,7 @@ import {
|
|||
useSystemStatus,
|
||||
} from "src/core/StashService";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import StashConfiguration from "../Settings/StashConfiguration";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||
|
|
@ -518,6 +518,10 @@ const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
|
|||
const [stashes, setStashes] = useState<GQL.StashConfig[]>(
|
||||
setupState.stashes ?? []
|
||||
);
|
||||
const [sfwContentMode, setSfwContentMode] = useState(
|
||||
setupState.sfwContentMode ?? false
|
||||
);
|
||||
|
||||
const [databaseFile, setDatabaseFile] = useState(
|
||||
setupState.databaseFile ?? ""
|
||||
);
|
||||
|
|
@ -555,6 +559,7 @@ const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
|
|||
cacheLocation,
|
||||
blobsLocation: storeBlobsInDatabase ? "" : blobsLocation,
|
||||
storeBlobsInDatabase,
|
||||
sfwContentMode,
|
||||
};
|
||||
next(input);
|
||||
}
|
||||
|
|
@ -594,6 +599,22 @@ const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
|
|||
/>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
<Form.Group id="sfw_content">
|
||||
<h3>
|
||||
<FormattedMessage id="setup.paths.sfw_content_settings" />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage id="setup.paths.sfw_content_settings_description" />
|
||||
</p>
|
||||
<Card>
|
||||
<Form.Check
|
||||
id="use-sfw-content-mode"
|
||||
checked={sfwContentMode}
|
||||
label={<FormattedMessage id="setup.paths.use_sfw_content_mode" />}
|
||||
onChange={() => setSfwContentMode(!sfwContentMode)}
|
||||
/>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
{overrideDatabase ? null : (
|
||||
<DatabaseSection
|
||||
databaseFile={databaseFile}
|
||||
|
|
@ -952,8 +973,7 @@ const FinishStep: React.FC<IWizardStep> = ({ goBack }) => {
|
|||
|
||||
export const Setup: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const { configuration, loading: configLoading } =
|
||||
useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const [saveUI] = useConfigureUI();
|
||||
|
||||
|
|
@ -1024,7 +1044,7 @@ export const Setup: React.FC = () => {
|
|||
}
|
||||
}
|
||||
|
||||
if (configLoading || statusLoading) {
|
||||
if (statusLoading) {
|
||||
return <LoadingIndicator />;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { faEye } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEye, faThumbsUp } from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { SweatDrops } from "./SweatDrops";
|
||||
import cx from "classnames";
|
||||
import { useIntl } from "react-intl";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface ICountButtonProps {
|
||||
value: number;
|
||||
|
|
@ -63,11 +64,17 @@ export const ViewCountButton: React.FC<CountButtonPropsNoIcon> = (props) => {
|
|||
|
||||
export const OCounterButton: React.FC<CountButtonPropsNoIcon> = (props) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
const icon = !sfwContentMode ? <SweatDrops /> : <Icon icon={faThumbsUp} />;
|
||||
const messageID = !sfwContentMode ? "o_count" : "o_count_sfw";
|
||||
|
||||
return (
|
||||
<CountButton
|
||||
{...props}
|
||||
icon={<SweatDrops />}
|
||||
title={intl.formatMessage({ id: "o_count" })}
|
||||
icon={icon}
|
||||
title={intl.formatMessage({ id: messageID })}
|
||||
countTitle={intl.formatMessage({ id: "actions.view_history" })}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { FormattedMessage } from "react-intl";
|
|||
|
||||
interface IDetailItem {
|
||||
id?: string | null;
|
||||
className?: string;
|
||||
label?: React.ReactNode;
|
||||
value?: React.ReactNode;
|
||||
labelTitle?: string;
|
||||
|
|
@ -13,6 +14,7 @@ interface IDetailItem {
|
|||
|
||||
export const DetailItem: React.FC<IDetailItem> = ({
|
||||
id,
|
||||
className = "",
|
||||
label,
|
||||
value,
|
||||
labelTitle,
|
||||
|
|
@ -30,7 +32,7 @@ export const DetailItem: React.FC<IDetailItem> = ({
|
|||
const sanitisedID = id.replace(/_/g, "-");
|
||||
|
||||
return (
|
||||
<div className={`detail-item ${id}`}>
|
||||
<div className={`detail-item ${id} ${className}`}>
|
||||
<span className={`detail-item-title ${sanitisedID}`} title={labelTitle}>
|
||||
{message}
|
||||
{fullWidth ? ":" : ""}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IStudio {
|
||||
id: string;
|
||||
|
|
@ -11,7 +11,7 @@ interface IStudio {
|
|||
export const StudioOverlay: React.FC<{
|
||||
studio: IStudio | null | undefined;
|
||||
}> = ({ studio }) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const configValue = configuration?.interface.showStudioAsText;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@ import React from "react";
|
|||
import { Button, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import { FormattedNumber, useIntl } from "react-intl";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export const Count: React.FC<{
|
||||
count: number;
|
||||
}> = ({ count }) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const abbreviateCounter = configuration?.ui.abbreviateCounters ?? false;
|
||||
|
||||
if (!abbreviateCounter) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import React from "react";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import {
|
||||
defaultRatingStarPrecision,
|
||||
defaultRatingSystemOptions,
|
||||
|
|
@ -23,7 +22,7 @@ export interface IRatingSystemProps {
|
|||
export const RatingSystem = PatchComponent(
|
||||
"RatingSystem",
|
||||
(props: IRatingSystemProps) => {
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const ratingSystemOptions =
|
||||
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useContext } from "react";
|
||||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import {
|
||||
convertToRatingFormat,
|
||||
|
|
@ -6,14 +6,14 @@ import {
|
|||
RatingStarPrecision,
|
||||
RatingSystemType,
|
||||
} from "src/utils/rating";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IProps {
|
||||
rating?: number | null;
|
||||
}
|
||||
|
||||
export const RatingBanner: React.FC<IProps> = ({ rating }) => {
|
||||
const { configuration: config } = useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const ratingSystemOptions =
|
||||
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
|
||||
const isLegacy =
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { CountrySelect } from "../CountrySelect";
|
|||
import { StringListInput } from "../StringListInput";
|
||||
import { ImageSelector } from "../ImageSelector";
|
||||
import { ScrapeResult } from "./scrapeResult";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IScrapedFieldProps<T> {
|
||||
result: ScrapeResult<T>;
|
||||
|
|
@ -31,6 +32,7 @@ interface IScrapedFieldProps<T> {
|
|||
|
||||
interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> {
|
||||
className?: string;
|
||||
field: string;
|
||||
title: string;
|
||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
|
|
@ -105,7 +107,10 @@ export const ScrapeDialogRow = <T, V>(props: IScrapedRowProps<T, V>) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Row className={`px-3 pt-3 ${props.className ?? ""}`}>
|
||||
<Row
|
||||
className={`px-3 pt-3 ${props.className ?? ""}`}
|
||||
data-field={props.field}
|
||||
>
|
||||
<Form.Label column lg="3">
|
||||
{props.title}
|
||||
</Form.Label>
|
||||
|
|
@ -175,6 +180,8 @@ function getNameString(value: string) {
|
|||
|
||||
interface IScrapedInputGroupRowProps {
|
||||
title: string;
|
||||
field: string;
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
result: ScrapeResult<string>;
|
||||
locked?: boolean;
|
||||
|
|
@ -187,6 +194,8 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
field={props.field}
|
||||
className={props.className}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedInputGroup
|
||||
|
|
@ -240,6 +249,7 @@ const ScrapedStringList: React.FC<IScrapedStringListProps> = (props) => {
|
|||
|
||||
interface IScrapedStringListRowProps {
|
||||
title: string;
|
||||
field: string;
|
||||
placeholder?: string;
|
||||
result: ScrapeResult<string[]>;
|
||||
locked?: boolean;
|
||||
|
|
@ -253,6 +263,7 @@ export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
|
|||
<ScrapeDialogRow
|
||||
className="string-list-row"
|
||||
title={props.title}
|
||||
field={props.field}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedStringList
|
||||
|
|
@ -300,6 +311,7 @@ export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
field={props.field}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedTextArea
|
||||
|
|
@ -346,6 +358,7 @@ const ScrapedImage: React.FC<IScrapedImageProps> = (props) => {
|
|||
|
||||
interface IScrapedImageRowProps {
|
||||
title: string;
|
||||
field: string;
|
||||
className?: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange: (value: ScrapeResult<string>) => void;
|
||||
|
|
@ -355,6 +368,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
field={props.field}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedImage
|
||||
|
|
@ -379,6 +393,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
|
|||
|
||||
interface IScrapedImagesRowProps {
|
||||
title: string;
|
||||
field: string;
|
||||
className?: string;
|
||||
result: ScrapeResult<string>;
|
||||
images: string[];
|
||||
|
|
@ -397,6 +412,7 @@ export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={props.title}
|
||||
field={props.field}
|
||||
result={props.result}
|
||||
renderOriginalField={() => (
|
||||
<ScrapedImage
|
||||
|
|
@ -433,6 +449,9 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
|||
props: IScrapeDialogProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { configuration } = useConfigurationContext();
|
||||
const { sfwContentMode } = configuration.interface;
|
||||
|
||||
return (
|
||||
<ModalComponent
|
||||
show
|
||||
|
|
@ -449,7 +468,10 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
|||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }}
|
||||
modalProps={{
|
||||
size: "lg",
|
||||
dialogClassName: `scrape-dialog ${sfwContentMode ? "sfw-mode" : ""}`,
|
||||
}}
|
||||
>
|
||||
<div className="dialog-container">
|
||||
<Form>
|
||||
|
|
@ -479,6 +501,7 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
|
|||
|
||||
interface IScrapedCountryRowProps {
|
||||
title: string;
|
||||
field: string;
|
||||
result: ScrapeResult<string>;
|
||||
onChange: (value: ScrapeResult<string>) => void;
|
||||
locked?: boolean;
|
||||
|
|
@ -487,6 +510,7 @@ interface IScrapedCountryRowProps {
|
|||
|
||||
export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
|
||||
title,
|
||||
field,
|
||||
result,
|
||||
onChange,
|
||||
locked,
|
||||
|
|
@ -494,6 +518,7 @@ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
|
|||
}) => (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
field={field}
|
||||
result={result}
|
||||
renderOriginalField={() => (
|
||||
<FormControl
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { uniq } from "lodash-es";
|
|||
|
||||
interface IScrapedStudioRow {
|
||||
title: string;
|
||||
field: string;
|
||||
result: ObjectScrapeResult<GQL.ScrapedStudio>;
|
||||
onChange: (value: ObjectScrapeResult<GQL.ScrapedStudio>) => void;
|
||||
newStudio?: GQL.ScrapedStudio;
|
||||
|
|
@ -25,6 +26,7 @@ function getObjectName<T extends { name: string }>(value: T) {
|
|||
|
||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||
title,
|
||||
field,
|
||||
result,
|
||||
onChange,
|
||||
newStudio,
|
||||
|
|
@ -73,6 +75,7 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
field={field}
|
||||
result={result}
|
||||
renderOriginalField={() => renderScrapedStudio(result)}
|
||||
renderNewField={() =>
|
||||
|
|
@ -92,6 +95,7 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
|||
|
||||
interface IScrapedObjectsRow<T> {
|
||||
title: string;
|
||||
field: string;
|
||||
result: ScrapeResult<T[]>;
|
||||
onChange: (value: ScrapeResult<T[]>) => void;
|
||||
newObjects?: T[];
|
||||
|
|
@ -107,6 +111,7 @@ interface IScrapedObjectsRow<T> {
|
|||
export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
|
||||
const {
|
||||
title,
|
||||
field,
|
||||
result,
|
||||
onChange,
|
||||
newObjects,
|
||||
|
|
@ -118,6 +123,7 @@ export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
|
|||
return (
|
||||
<ScrapeDialogRow
|
||||
title={title}
|
||||
field={field}
|
||||
result={result}
|
||||
renderOriginalField={() => renderObjects(result)}
|
||||
renderNewField={() =>
|
||||
|
|
@ -142,7 +148,15 @@ type IScrapedObjectRowImpl<T> = Omit<
|
|||
|
||||
export const ScrapedPerformersRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer> & { ageFromDate?: string | null }
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew, ageFromDate }) => {
|
||||
> = ({
|
||||
title,
|
||||
field,
|
||||
result,
|
||||
onChange,
|
||||
newObjects,
|
||||
onCreateNew,
|
||||
ageFromDate,
|
||||
}) => {
|
||||
const performersCopy = useMemo(() => {
|
||||
return (
|
||||
newObjects?.map((p) => {
|
||||
|
|
@ -191,6 +205,7 @@ export const ScrapedPerformersRow: React.FC<
|
|||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedPerformer>
|
||||
title={title}
|
||||
field={field}
|
||||
result={result}
|
||||
renderObjects={renderScrapedPerformers}
|
||||
onChange={onChange}
|
||||
|
|
@ -203,7 +218,7 @@ export const ScrapedPerformersRow: React.FC<
|
|||
|
||||
export const ScrapedGroupsRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedGroup>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
> = ({ title, field, result, onChange, newObjects, onCreateNew }) => {
|
||||
const groupsCopy = useMemo(() => {
|
||||
return (
|
||||
newObjects?.map((p) => {
|
||||
|
|
@ -251,6 +266,7 @@ export const ScrapedGroupsRow: React.FC<
|
|||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedGroup>
|
||||
title={title}
|
||||
field={field}
|
||||
result={result}
|
||||
renderObjects={renderScrapedGroups}
|
||||
onChange={onChange}
|
||||
|
|
@ -263,7 +279,7 @@ export const ScrapedGroupsRow: React.FC<
|
|||
|
||||
export const ScrapedTagsRow: React.FC<
|
||||
IScrapedObjectRowImpl<GQL.ScrapedTag>
|
||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||
> = ({ title, field, result, onChange, newObjects, onCreateNew }) => {
|
||||
function renderScrapedTags(
|
||||
scrapeResult: ScrapeResult<GQL.ScrapedTag[]>,
|
||||
isNew?: boolean,
|
||||
|
|
@ -297,6 +313,7 @@ export const ScrapedTagsRow: React.FC<
|
|||
return (
|
||||
<ScrapedObjectsRow<GQL.ScrapedTag>
|
||||
title={title}
|
||||
field={field}
|
||||
result={result}
|
||||
renderObjects={renderScrapedTags}
|
||||
onChange={onChange}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export function useScrapedTags(
|
|||
|
||||
const scrapedTagsRow = (
|
||||
<ScrapedTagsRow
|
||||
field="tags"
|
||||
title={intl.formatMessage({ id: "tags" })}
|
||||
result={tags}
|
||||
onChange={(value) => setTags(value)}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import CreatableSelect from "react-select/creatable";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useMarkerStrings } from "src/core/StashService";
|
||||
import { SelectComponents } from "react-select/dist/declarations/src/components";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
|
|
@ -108,7 +108,7 @@ const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|||
const LimitedSelectMenu = <T extends boolean>(
|
||||
props: MenuListProps<Option, T, GroupBase<Option>>
|
||||
) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
||||
|
|
@ -496,6 +496,7 @@ export const ListSelect = <T extends {}>(props: IListSelect<T>) => {
|
|||
|
||||
type DisableOption = Option & {
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
interface ICheckBoxSelectProps {
|
||||
|
|
@ -510,7 +511,17 @@ export const CheckBoxSelect: React.FC<ICheckBoxSelectProps> = ({
|
|||
onChange,
|
||||
}) => {
|
||||
const Option = (props: OptionProps<DisableOption, true>) => (
|
||||
<reactSelectComponents.Option {...props}>
|
||||
<reactSelectComponents.Option
|
||||
{...props}
|
||||
className={`${props.className || ""} ${props.data.className || ""}`}
|
||||
// data values don't seem to be included in props.innerProps by default
|
||||
innerProps={
|
||||
{ "data-value": props.data.value } as React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
disabled={props.isDisabled}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useMemo } from "react";
|
||||
import { StashId } from "src/core/generated-graphql";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { ExternalLink } from "./ExternalLink";
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ export const StashIDPill: React.FC<{
|
|||
stashID: Pick<StashId, "endpoint" | "stash_id">;
|
||||
linkType: LinkType;
|
||||
}> = ({ stashID, linkType }) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const { endpoint, stash_id } = stashID;
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
|
|||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { StudioScenesPanel } from "./StudioScenesPanel";
|
||||
import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
|
||||
import { StudioImagesPanel } from "./StudioImagesPanel";
|
||||
|
|
@ -264,7 +264,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||
const intl = useIntl();
|
||||
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const uiConfig = configuration?.ui;
|
||||
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||
const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
queryFindStudiosByIDForSelect,
|
||||
queryFindStudiosForSelect,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -65,7 +65,7 @@ const _StudioSelect: React.FC<
|
|||
> = (props) => {
|
||||
const [createStudio] = useStudioCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
(configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { useCallback, useContext } from "react";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useCallback } from "react";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { initialConfig, ITaggerConfig } from "./constants";
|
||||
import { useConfigureUISetting } from "src/core/StashService";
|
||||
|
||||
export function useTaggerConfig() {
|
||||
const { configuration: stashConfig } = useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
const [saveUISetting] = useConfigureUISetting();
|
||||
|
||||
const config = stashConfig?.ui.taggerConfig ?? initialConfig;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
useTagCreate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { ITaggerSource, SCRAPER_PREFIX, STASH_BOX_PREFIX } from "./constants";
|
||||
import { errorToString } from "src/utils";
|
||||
import { mergeStudioStashIDs } from "./utils";
|
||||
|
|
@ -117,7 +117,7 @@ export const TaggerContext: React.FC = ({ children }) => {
|
|||
|
||||
const stopping = useRef(false);
|
||||
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
const { config, setConfig } = useTaggerConfig();
|
||||
|
||||
const Scrapers = useListSceneScrapers();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Dispatch, useState } from "react";
|
||||
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
import { ITaggerConfig } from "../constants";
|
||||
import PerformerFieldSelector from "../PerformerFieldSelector";
|
||||
|
|
@ -13,7 +13,7 @@ interface IConfigProps {
|
|||
}
|
||||
|
||||
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
const [showExclusionModal, setShowExclusionModal] = useState(false);
|
||||
|
||||
const excludedFields = config.excludedPerformerFields ?? [];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import {
|
|||
performerMutationImpactedQueries,
|
||||
} from "src/core/StashService";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
import StashSearchResult from "./StashSearchResult";
|
||||
import PerformerConfig from "./Config";
|
||||
|
|
@ -620,7 +620,7 @@ interface ITaggerProps {
|
|||
export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
|
||||
const jobsSubscribe = useJobsSubscribe();
|
||||
const intl = useIntl();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
const { config, setConfig } = useTaggerConfig();
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import Config from "./Config";
|
|||
import { TaggerScene } from "./TaggerScene";
|
||||
import { SceneTaggerModals } from "./sceneTaggerModals";
|
||||
import { SceneSearchResults } from "./StashSearchResult";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { faCog } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useLightbox } from "src/hooks/Lightbox/hooks";
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ const Scene: React.FC<{
|
|||
const intl = useIntl();
|
||||
const { currentSource, doSceneQuery, doSceneFragmentScrape, loading } =
|
||||
useContext(TaggerStateContext);
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
|
||||
const cont = configuration?.interface.continuePlaylistDefault ?? false;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import {
|
|||
faImage,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { objectPath, objectTitle } from "src/core/files";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
|
||||
interface ITaggerSceneDetails {
|
||||
|
|
@ -154,7 +154,7 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
|
|||
|
||||
const history = useHistory();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const cont = configuration?.interface.continuePlaylistDefault ?? false;
|
||||
|
||||
async function query() {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { Dispatch, useState } from "react";
|
||||
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
import { ITaggerConfig } from "../constants";
|
||||
import StudioFieldSelector from "./StudioFieldSelector";
|
||||
|
|
@ -13,7 +13,7 @@ interface IConfigProps {
|
|||
}
|
||||
|
||||
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
const [showExclusionModal, setShowExclusionModal] = useState(false);
|
||||
|
||||
const excludedFields = config.excludedStudioFields ?? [];
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
evictQueries,
|
||||
} from "src/core/StashService";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
import StashSearchResult from "./StashSearchResult";
|
||||
import StudioConfig from "./Config";
|
||||
|
|
@ -669,7 +669,7 @@ interface ITaggerProps {
|
|||
export const StudioTagger: React.FC<ITaggerProps> = ({ studios }) => {
|
||||
const jobsSubscribe = useJobsSubscribe();
|
||||
const intl = useIntl();
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
const { config, setConfig } = useTaggerConfig();
|
||||
const [showConfig, setShowConfig] = useState(false);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
|
|||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { tagRelationHook } from "src/core/tags";
|
||||
import { TagScenesPanel } from "./TagScenesPanel";
|
||||
import { TagMarkersPanel } from "./TagMarkersPanel";
|
||||
|
|
@ -293,7 +293,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||
const intl = useIntl();
|
||||
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const uiConfig = configuration?.ui;
|
||||
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||
const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
|||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { useFindTag } from "../../core/StashService";
|
||||
import { TagCard } from "./TagCard";
|
||||
import { ConfigurationContext } from "../../hooks/Config";
|
||||
import { useConfigurationContext } from "../../hooks/Config";
|
||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
|
||||
interface ITagPopoverCardProps {
|
||||
|
|
@ -47,7 +47,7 @@ export const TagPopover: React.FC<ITagPopoverProps> = ({
|
|||
placement = "top",
|
||||
target,
|
||||
}) => {
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const showTagCardOnHover = config?.ui.showTagCardOnHover ?? true;
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import {
|
|||
queryFindTagsByIDForSelect,
|
||||
queryFindTagsForSelect,
|
||||
} from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useIntl } from "react-intl";
|
||||
import { defaultMaxOptionsShown } from "src/core/config";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
|
|
@ -67,7 +67,7 @@ export type TagSelectProps = IFilterProps &
|
|||
const _TagSelect: React.FC<TagSelectProps> = (props) => {
|
||||
const [createTag] = useTagCreate();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const intl = useIntl();
|
||||
const maxOptionsShown =
|
||||
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import TextUtils from "src/utils/text";
|
|||
import NavUtils from "src/utils/navigation";
|
||||
import cx from "classnames";
|
||||
import { SceneQueue } from "src/models/sceneQueue";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { markerTitle } from "src/core/markers";
|
||||
import { objectTitle } from "src/core/files";
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ export const WallItem = <T extends WallItemType>({
|
|||
}: IWallItemProps<T>) => {
|
||||
const [active, setActive] = useState(false);
|
||||
const itemEl = useRef<HTMLDivElement>(null);
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
const showTextContainer = config?.interface.wallShowTitle ?? true;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@
|
|||
|
||||
Setting the language affects the formatting of numbers and dates.
|
||||
|
||||
## SFW Content Mode
|
||||
|
||||
SFW Content Mode is used to indicate that the content being managed is _not_ adult content.
|
||||
|
||||
When SFW Content Mode is enabled, the following changes are made to the UI:
|
||||
- default performer images are changed to less adult-oriented images
|
||||
- certain adult-specific metadata fields are hidden (e.g. performer genital fields)
|
||||
- `O`-Counter is replaced with `Like`-counter
|
||||
|
||||
## Scene/Marker Wall Preview Type
|
||||
|
||||
The Scene Wall and Marker pages display scene preview videos (mp4) by default. This can be changed to animated image (webp) or static image.
|
||||
|
|
|
|||
|
|
@ -2,14 +2,28 @@ import React from "react";
|
|||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
export interface IContext {
|
||||
configuration?: GQL.ConfigDataFragment;
|
||||
loading?: boolean;
|
||||
configuration: GQL.ConfigDataFragment;
|
||||
}
|
||||
|
||||
export const ConfigurationContext = React.createContext<IContext>({});
|
||||
export const ConfigurationContext = React.createContext<IContext | null>(null);
|
||||
|
||||
export const useConfigurationContext = () => {
|
||||
const context = React.useContext(ConfigurationContext);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error(
|
||||
"useConfigurationContext must be used within a ConfigurationProvider"
|
||||
);
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useConfigurationContextOptional = () => {
|
||||
return React.useContext(ConfigurationContext);
|
||||
};
|
||||
|
||||
export const ConfigurationProvider: React.FC<IContext> = ({
|
||||
loading,
|
||||
configuration,
|
||||
children,
|
||||
}) => {
|
||||
|
|
@ -17,7 +31,6 @@ export const ConfigurationProvider: React.FC<IContext> = ({
|
|||
<ConfigurationContext.Provider
|
||||
value={{
|
||||
configuration,
|
||||
loading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { ConfigurationContext } from "../Config";
|
||||
import { useConfigurationContext } from "../Config";
|
||||
import { useLocalForage } from "../LocalForage";
|
||||
import { Interactive as InteractiveAPI } from "./interactive";
|
||||
import InteractiveUtils, {
|
||||
|
|
@ -86,7 +86,7 @@ export const InteractiveProvider: React.FC = ({ children }) => {
|
|||
{ serverOffset: 0, lastSyncTime: 0 }
|
||||
);
|
||||
|
||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||
const { configuration: stashConfig } = useConfigurationContext();
|
||||
|
||||
const [state, setState] = useState<ConnectionState>(ConnectionState.Missing);
|
||||
const [handyKey, setHandyKey] = useState<string | undefined>(undefined);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import usePageVisibility from "../PageVisibility";
|
|||
import { useToast } from "../Toast";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { LightboxImage } from "./LightboxImage";
|
||||
import { ConfigurationContext } from "../Config";
|
||||
import { useConfigurationContext } from "../Config";
|
||||
import { Link } from "react-router-dom";
|
||||
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
||||
import {
|
||||
|
|
@ -154,7 +154,7 @@ export const LightboxComponent: React.FC<IProps> = ({
|
|||
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const { configuration: config } = React.useContext(ConfigurationContext);
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
const [interfaceLocalForage, setInterfaceLocalForage] =
|
||||
useInterfaceLocalForage();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
import { useConfigureUI } from "src/core/StashService";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { useConfigurationContext } from "src/hooks/Config";
|
||||
import { useToast } from "./Toast";
|
||||
|
||||
export const useTableColumns = (
|
||||
|
|
@ -9,7 +8,7 @@ export const useTableColumns = (
|
|||
) => {
|
||||
const Toast = useToast();
|
||||
|
||||
const { configuration } = useContext(ConfigurationContext);
|
||||
const { configuration } = useConfigurationContext();
|
||||
const [saveUI] = useConfigureUI();
|
||||
|
||||
const ui = configuration?.ui;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ $sidebar-width: 250px;
|
|||
@import "styles/theme";
|
||||
@import "styles/range";
|
||||
@import "styles/scrollbars";
|
||||
@import "sfw-mode.scss";
|
||||
@import "src/components/Changelog/styles.scss";
|
||||
@import "src/components/Galleries/styles.scss";
|
||||
@import "src/components/Help/styles.scss";
|
||||
|
|
|
|||
|
|
@ -784,6 +784,10 @@
|
|||
"description": "Number of times to attempt to scroll before moving to the next/previous item. Only applies for Pan Y scroll mode.",
|
||||
"heading": "Scroll attempts before transition"
|
||||
},
|
||||
"sfw_mode": {
|
||||
"description": "Enable if using stash to store SFW content. Hides or changes some adult-content-related aspects of the UI.",
|
||||
"heading": "SFW Content Mode"
|
||||
},
|
||||
"show_tag_card_on_hover": {
|
||||
"description": "Show tag card when hovering tag badges",
|
||||
"heading": "Tag card tooltips"
|
||||
|
|
@ -897,6 +901,7 @@
|
|||
"developmentVersion": "Development Version",
|
||||
"dialogs": {
|
||||
"clear_o_history_confirm": "Are you sure you want to clear the O history?",
|
||||
"clear_o_history_confirm_sfw": "Are you sure you want to clear the like history?",
|
||||
"clear_play_history_confirm": "Are you sure you want to clear the play history?",
|
||||
"create_new_entity": "Create new {entity}",
|
||||
"delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:",
|
||||
|
|
@ -1158,6 +1163,7 @@
|
|||
"interactive_speed": "Interactive Speed",
|
||||
"isMissing": "Is Missing",
|
||||
"last_o_at": "Last O At",
|
||||
"last_o_at_sfw": "Last Like At",
|
||||
"last_played_at": "Last Played At",
|
||||
"library": "Library",
|
||||
"loading": {
|
||||
|
|
@ -1198,9 +1204,11 @@
|
|||
"new": "New",
|
||||
"none": "None",
|
||||
"o_count": "O Count",
|
||||
"o_counter": "O-Counter",
|
||||
"o_count_sfw": "Likes",
|
||||
"o_history": "O History",
|
||||
"o_history_sfw": "Like History",
|
||||
"odate_recorded_no": "No O Date Recorded",
|
||||
"odate_recorded_no_sfw": "No Like Date Recorded",
|
||||
"operations": "Operations",
|
||||
"organized": "Organised",
|
||||
"orientation": "Orientation",
|
||||
|
|
@ -1377,25 +1385,28 @@
|
|||
},
|
||||
"paths": {
|
||||
"database_filename_empty_for_default": "database filename (empty for default)",
|
||||
"description": "Next up, we need to determine where to find your porn collection, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.",
|
||||
"description": "Next up, we need to determine where to find your content, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.",
|
||||
"path_to_blobs_directory_empty_for_default": "path to blobs directory (empty for default)",
|
||||
"path_to_cache_directory_empty_for_default": "path to cache directory (empty for default)",
|
||||
"path_to_generated_directory_empty_for_default": "path to generated directory (empty for default)",
|
||||
"set_up_your_paths": "Set up your paths",
|
||||
"sfw_content_settings": "Using stash for SFW content?",
|
||||
"sfw_content_settings_description": "stash can be used to manage SFW content such as photography, art, comics, and more. Enabling this option will adjust some UI behaviour to be more appropriate for SFW content.",
|
||||
"stash_alert": "No library paths have been selected. No media will be able to be scanned into Stash. Are you sure?",
|
||||
"store_blobs_in_database": "Store blobs in database",
|
||||
"use_sfw_content_mode": "Use SFW content mode",
|
||||
"where_can_stash_store_blobs": "Where can Stash store database binary data?",
|
||||
"where_can_stash_store_blobs_description": "Stash can store binary data such as scene covers, performer, studio and tag images either in the database or in the filesystem. By default, it will store this data in the filesystem in the subdirectory <code>blobs</code> within the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.",
|
||||
"where_can_stash_store_blobs_description_addendum": "Alternatively, you can store this data in the database. <strong>Note:</strong> This will increase the size of your database file, and will increase database migration times.",
|
||||
"where_can_stash_store_cache_files": "Where can Stash store cache files?",
|
||||
"where_can_stash_store_cache_files_description": "In order for some functionality like HLS/DASH live transcoding to function, Stash requires a cache directory for temporary files. By default, Stash will create a <code>cache</code> directory within the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.",
|
||||
"where_can_stash_store_its_database": "Where can Stash store its database?",
|
||||
"where_can_stash_store_its_database_description": "Stash uses an SQLite database to store your porn metadata. By default, this will be created as <code>stash-go.sqlite</code> in the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) filename.",
|
||||
"where_can_stash_store_its_database_description": "Stash uses an SQLite database to store your content metadata. By default, this will be created as <code>stash-go.sqlite</code> in the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) filename.",
|
||||
"where_can_stash_store_its_database_warning": "WARNING: storing the database on a different system to where Stash is run from (e.g. storing the database on a NAS while running the Stash server on another computer) is <strong>unsupported</strong>! SQLite is not intended for use across a network, and attempting to do so can very easily cause your entire database to become corrupted.",
|
||||
"where_can_stash_store_its_generated_content": "Where can Stash store its generated content?",
|
||||
"where_can_stash_store_its_generated_content_description": "In order to provide thumbnails, previews and sprites, Stash generates images and videos. This also includes transcodes for unsupported file formats. By default, Stash will create a <code>generated</code> directory within the directory containing your config file. If you want to change where this generated media will be stored, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.",
|
||||
"where_is_your_porn_located": "Where is your porn located?",
|
||||
"where_is_your_porn_located_description": "Add directories containing your porn videos and images. Stash will use these directories to find videos and images during scanning."
|
||||
"where_is_your_porn_located": "Where is your content located?",
|
||||
"where_is_your_porn_located_description": "Add directories containing your videos and images. Stash will use these directories to find videos and images during scanning."
|
||||
},
|
||||
"stash_setup_wizard": "Stash Setup Wizard",
|
||||
"success": {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export abstract class Criterion {
|
|||
|
||||
protected cloneValues() {}
|
||||
|
||||
public abstract getLabel(intl: IntlShape): string;
|
||||
public abstract getLabel(intl: IntlShape, sfwContentMode?: boolean): string;
|
||||
|
||||
public getId(): string {
|
||||
return `${this.criterionOption.type}`;
|
||||
|
|
@ -148,7 +148,7 @@ export abstract class ModifierCriterion<
|
|||
: "";
|
||||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
public getLabel(intl: IntlShape, sfwContentMode: boolean = false): string {
|
||||
const modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
this.modifier
|
||||
|
|
@ -162,10 +162,14 @@ export abstract class ModifierCriterion<
|
|||
valueString = this.getLabelValue(intl);
|
||||
}
|
||||
|
||||
const messageID = !sfwContentMode
|
||||
? this.criterionOption.messageID
|
||||
: this.criterionOption.sfwMessageID ?? this.criterionOption.messageID;
|
||||
|
||||
return intl.formatMessage(
|
||||
{ id: "criterion_modifier.format_string" },
|
||||
{
|
||||
criterion: intl.formatMessage({ id: this.criterionOption.messageID }),
|
||||
criterion: intl.formatMessage({ id: messageID }),
|
||||
modifierString,
|
||||
valueString,
|
||||
}
|
||||
|
|
@ -257,12 +261,14 @@ interface ICriterionOptionParams {
|
|||
type: CriterionType;
|
||||
makeCriterion: MakeCriterionFn;
|
||||
hidden?: boolean;
|
||||
sfwMessageID?: string;
|
||||
}
|
||||
|
||||
export class CriterionOption {
|
||||
public readonly type: CriterionType;
|
||||
public readonly messageID: string;
|
||||
public readonly makeCriterionFn: MakeCriterionFn;
|
||||
public readonly sfwMessageID?: string;
|
||||
|
||||
// used for legacy criteria that are not shown in the UI
|
||||
public readonly hidden: boolean = false;
|
||||
|
|
@ -272,6 +278,7 @@ export class CriterionOption {
|
|||
this.messageID = options.messageID;
|
||||
this.makeCriterionFn = options.makeCriterion;
|
||||
this.hidden = options.hidden ?? false;
|
||||
this.sfwMessageID = options.sfwMessageID;
|
||||
}
|
||||
|
||||
public makeCriterion(config?: ConfigDataFragment) {
|
||||
|
|
@ -478,7 +485,7 @@ export class IHierarchicalLabeledIdCriterion extends ModifierCriterion<IHierarch
|
|||
);
|
||||
}
|
||||
|
||||
public getLabel(intl: IntlShape): string {
|
||||
public getLabel(intl: IntlShape, sfwContentMode?: boolean): string {
|
||||
let id = "criterion_modifier.format_string";
|
||||
let modifierString = ModifierCriterion.getModifierLabel(
|
||||
intl,
|
||||
|
|
@ -511,10 +518,14 @@ export class IHierarchicalLabeledIdCriterion extends ModifierCriterion<IHierarch
|
|||
}
|
||||
}
|
||||
|
||||
const messageID = !sfwContentMode
|
||||
? this.criterionOption.messageID
|
||||
: this.criterionOption.sfwMessageID ?? this.criterionOption.messageID;
|
||||
|
||||
return intl.formatMessage(
|
||||
{ id },
|
||||
{
|
||||
criterion: intl.formatMessage({ id: this.criterionOption.messageID }),
|
||||
criterion: intl.formatMessage({ id: messageID }),
|
||||
modifierString,
|
||||
valueString,
|
||||
excludedString,
|
||||
|
|
@ -552,9 +563,14 @@ export class StringCriterionOption extends ModifierCriterionOption {
|
|||
|
||||
export function createStringCriterionOption(
|
||||
type: CriterionType,
|
||||
messageID?: string
|
||||
messageID?: string,
|
||||
options?: { nsfw?: boolean }
|
||||
) {
|
||||
return new StringCriterionOption({ messageID: messageID ?? type, type });
|
||||
return new StringCriterionOption({
|
||||
messageID: messageID ?? type,
|
||||
type,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export class MandatoryStringCriterionOption extends ModifierCriterionOption {
|
||||
|
|
@ -755,7 +771,8 @@ export class MandatoryNumberCriterionOption extends ModifierCriterionOption {
|
|||
constructor(
|
||||
messageID: string,
|
||||
value: CriterionType,
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>
|
||||
makeCriterion?: () => ModifierCriterion<CriterionValue>,
|
||||
options?: { sfwMessageID?: string }
|
||||
) {
|
||||
super({
|
||||
messageID,
|
||||
|
|
@ -773,15 +790,22 @@ export class MandatoryNumberCriterionOption extends ModifierCriterionOption {
|
|||
makeCriterion: makeCriterion
|
||||
? makeCriterion
|
||||
: () => new NumberCriterion(this),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function createMandatoryNumberCriterionOption(
|
||||
value: CriterionType,
|
||||
messageID?: string
|
||||
messageID?: string,
|
||||
options?: { sfwMessageID?: string }
|
||||
) {
|
||||
return new MandatoryNumberCriterionOption(messageID ?? value, value);
|
||||
return new MandatoryNumberCriterionOption(
|
||||
messageID ?? value,
|
||||
value,
|
||||
undefined,
|
||||
options
|
||||
);
|
||||
}
|
||||
|
||||
export function encodeRangeValue<V>(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { DisplayMode } from "./types";
|
|||
export interface ISortByOption {
|
||||
messageID: string;
|
||||
value: string;
|
||||
sfwMessageID?: string;
|
||||
}
|
||||
|
||||
export const MediaSortByOptions = [
|
||||
|
|
@ -22,7 +23,7 @@ export class ListFilterOptions {
|
|||
public readonly displayModeOptions: DisplayMode[] = [];
|
||||
public readonly criterionOptions: CriterionOption[] = [];
|
||||
|
||||
public static createSortBy(value: string) {
|
||||
public static createSortBy(value: string): ISortByOption {
|
||||
return {
|
||||
messageID: value,
|
||||
value,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const sortByOptions = [
|
|||
{
|
||||
messageID: "o_count",
|
||||
value: "o_counter",
|
||||
sfwMessageID: "o_count_sfw",
|
||||
},
|
||||
]);
|
||||
const displayModeOptions = [DisplayMode.Grid];
|
||||
|
|
@ -53,7 +54,9 @@ const criterionOptions = [
|
|||
RatingCriterionOption,
|
||||
PerformersCriterionOption,
|
||||
createDateCriterionOption("date"),
|
||||
createMandatoryNumberCriterionOption("o_counter", "o_count"),
|
||||
createMandatoryNumberCriterionOption("o_counter", "o_count", {
|
||||
sfwMessageID: "o_count_sfw",
|
||||
}),
|
||||
ContainingGroupsCriterionOption,
|
||||
SubGroupsCriterionOption,
|
||||
createMandatoryNumberCriterionOption("containing_group_count"),
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue