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:
WithoutPants 2025-11-18 11:13:35 +11:00 committed by GitHub
parent bb56b619f5
commit 51999135be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
105 changed files with 843 additions and 370 deletions

View file

@ -9,7 +9,7 @@
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest) [![GitHub release (latest by date)](https://img.shields.io/github/v/release/stashapp/stash?logo=github)](https://github.com/stashapp/stash/releases/latest)
[![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](https://github.com/stashapp/stash/labels/bounty) [![GitHub issues by-label](https://img.shields.io/github/issues-raw/stashapp/stash/bounty)](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.**
![demo image](docs/readme_assets/demo_image.png) ![demo image](docs/readme_assets/demo_image.png)
* 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. * 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.

View file

@ -2,6 +2,8 @@ input SetupInput {
"Empty to indicate $HOME/.stash/config.yml default" "Empty to indicate $HOME/.stash/config.yml default"
configLocation: String! configLocation: String!
stashes: [StashConfigInput!]! stashes: [StashConfigInput!]!
"True if SFW content mode is enabled"
sfwContentMode: Boolean
"Empty to indicate default" "Empty to indicate default"
databaseFile: String! databaseFile: String!
"Empty to indicate default" "Empty to indicate default"
@ -341,6 +343,9 @@ type ConfigImageLightboxResult {
} }
input ConfigInterfaceInput { input ConfigInterfaceInput {
"True if SFW content mode is enabled"
sfwContentMode: Boolean
"Ordered list of items that should be shown in the menu" "Ordered list of items that should be shown in the menu"
menuItems: [String!] menuItems: [String!]
@ -407,6 +412,9 @@ type ConfigDisableDropdownCreate {
} }
type ConfigInterfaceResult { type ConfigInterfaceResult {
"True if SFW content mode is enabled"
sfwContentMode: Boolean!
"Ordered list of items that should be shown in the menu" "Ordered list of items that should be shown in the menu"
menuItems: [String!] menuItems: [String!]

View file

@ -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 // try the custom box first if we have one
if performerBoxCustom != nil { if performerBoxCustom != nil {
ret, err := performerBoxCustom.GetRandomImageByName(name) 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) logger.Warnf("error loading custom default performer image: %v", err)
} }
if sfwMode {
return static.ReadAll(static.DefaultSFWPerformerImage)
}
var g models.GenderEnum var g models.GenderEnum
if gender != nil { if gender != nil {
g = *gender g = *gender

View file

@ -445,6 +445,8 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) { func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) {
c := config.GetInstance() c := config.GetInstance()
r.setConfigBool(config.SFWContentMode, input.SfwContentMode)
if input.MenuItems != nil { if input.MenuItems != nil {
c.SetInterface(config.MenuItems, input.MenuItems) c.SetInterface(config.MenuItems, input.MenuItems)
} }

View file

@ -162,6 +162,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
disableDropdownCreate := config.GetDisableDropdownCreate() disableDropdownCreate := config.GetDisableDropdownCreate()
return &ConfigInterfaceResult{ return &ConfigInterfaceResult{
SfwContentMode: config.GetSFWContentMode(),
MenuItems: menuItems, MenuItems: menuItems,
SoundOnPreview: &soundOnPreview, SoundOnPreview: &soundOnPreview,
WallShowTitle: &wallShowTitle, WallShowTitle: &wallShowTitle,

View file

@ -18,9 +18,14 @@ type PerformerFinder interface {
GetImage(ctx context.Context, performerID int) ([]byte, error) GetImage(ctx context.Context, performerID int) ([]byte, error)
} }
type sfwConfig interface {
GetSFWContentMode() bool
}
type performerRoutes struct { type performerRoutes struct {
routes routes
performerFinder PerformerFinder performerFinder PerformerFinder
sfwConfig sfwConfig
} }
func (rs performerRoutes) Routes() chi.Router { func (rs performerRoutes) Routes() chi.Router {
@ -54,7 +59,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
} }
if len(image) == 0 { if len(image) == 0 {
image = getDefaultPerformerImage(performer.Name, performer.Gender) image = getDefaultPerformerImage(performer.Name, performer.Gender, rs.sfwConfig.GetSFWContentMode())
} }
utils.ServeImage(w, r, image) utils.ServeImage(w, r, image)

View file

@ -322,6 +322,7 @@ func (s *Server) getPerformerRoutes() chi.Router {
return performerRoutes{ return performerRoutes{
routes: routes{txnManager: repo.TxnManager}, routes: routes{txnManager: repo.TxnManager},
performerFinder: repo.Performer, performerFinder: repo.Performer,
sfwConfig: s.manager.Config,
}.Routes() }.Routes()
} }

View file

@ -43,6 +43,9 @@ const (
Password = "password" Password = "password"
MaxSessionAge = "max_session_age" MaxSessionAge = "max_session_age"
// SFWContentMode mode config key
SFWContentMode = "sfw_content_mode"
FFMpegPath = "ffmpeg_path" FFMpegPath = "ffmpeg_path"
FFProbePath = "ffprobe_path" FFProbePath = "ffprobe_path"
@ -628,7 +631,15 @@ func (i *Config) getStringMapString(key string) map[string]string {
return ret 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 // Works opposite to the usual case - it will return the override
// value only if the main value is not set. // value only if the main value is not set.
func (i *Config) GetStashPaths() StashConfigs { func (i *Config) GetStashPaths() StashConfigs {

View file

@ -262,6 +262,10 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
cfg.SetString(config.Cache, input.CacheLocation) cfg.SetString(config.Cache, input.CacheLocation)
} }
if input.SFWContentMode {
cfg.SetBool(config.SFWContentMode, true)
}
if input.StoreBlobsInDatabase { if input.StoreBlobsInDatabase {
cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase) cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase)
} else { } else {

View file

@ -21,6 +21,7 @@ type SetupInput struct {
// Empty to indicate $HOME/.stash/config.yml default // Empty to indicate $HOME/.stash/config.yml default
ConfigLocation string `json:"configLocation"` ConfigLocation string `json:"configLocation"`
Stashes []*config.StashConfigInput `json:"stashes"` Stashes []*config.StashConfigInput `json:"stashes"`
SFWContentMode bool `json:"sfwContentMode"`
// Empty to indicate default // Empty to indicate default
DatabaseFile string `json:"databaseFile"` DatabaseFile string `json:"databaseFile"`
// Empty to indicate default // Empty to indicate default

View file

@ -8,12 +8,13 @@ import (
"io/fs" "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 var data embed.FS
const ( const (
Performer = "performer" Performer = "performer"
PerformerMale = "performer_male" PerformerMale = "performer_male"
DefaultSFWPerformerImage = "performer_sfw/performer.svg"
Scene = "scene" Scene = "scene"
DefaultSceneImage = "scene/scene.svg" DefaultSceneImage = "scene/scene.svg"

View 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

View file

@ -71,6 +71,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
} }
fragment ConfigInterfaceData on ConfigInterfaceResult { fragment ConfigInterfaceData on ConfigInterfaceResult {
sfwContentMode
menuItems menuItems
soundOnPreview soundOnPreview
wallShowTitle wallShowTitle

View file

@ -31,7 +31,10 @@ import * as GQL from "./core/generated-graphql";
import { makeTitleProps } from "./hooks/title"; import { makeTitleProps } from "./hooks/title";
import { LoadingIndicator } from "./components/Shared/LoadingIndicator"; import { LoadingIndicator } from "./components/Shared/LoadingIndicator";
import { ConfigurationProvider } from "./hooks/Config"; import {
ConfigurationProvider,
useConfigurationContextOptional,
} from "./hooks/Config";
import { ManualProvider } from "./components/Help/context"; import { ManualProvider } from "./components/Help/context";
import { InteractiveProvider } from "./hooks/Interactive/context"; import { InteractiveProvider } from "./hooks/Interactive/context";
import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog";
@ -50,6 +53,7 @@ import { PatchFunction } from "./patch";
import moment from "moment/min/moment-with-locales"; import moment from "moment/min/moment-with-locales";
import { ErrorMessage } from "./components/Shared/ErrorMessage"; import { ErrorMessage } from "./components/Shared/ErrorMessage";
import cx from "classnames";
const Performers = lazyComponent( const Performers = lazyComponent(
() => import("./components/Performers/Performers") () => import("./components/Performers/Performers")
@ -104,8 +108,17 @@ const AppContainer: React.FC<React.PropsWithChildren<{}>> = PatchFunction(
) as React.FC; ) as React.FC;
const MainContainer: React.FC = ({ children }) => { 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 ( return (
<div className={`main container-fluid ${appleRendering ? "apple" : ""}`}> <div
className={cx("main container-fluid", {
apple: appleRendering,
"sfw-content-mode": sfwContentMode,
})}
>
{children} {children}
</div> </div>
); );
@ -300,28 +313,36 @@ export const App: React.FC = () => {
return null; return null;
} }
if (config.error) { function renderSimple(content: React.ReactNode) {
return ( return (
<IntlProvider <IntlProvider
locale={intlLanguage} locale={intlLanguage}
messages={messages} messages={messages}
formats={intlFormats} formats={intlFormats}
> >
<MainContainer> <MainContainer>{content}</MainContainer>
<ErrorMessage
message={
<FormattedMessage
id="errors.loading_type"
values={{ type: "configuration" }}
/>
}
error={config.error.message}
/>
</MainContainer>
</IntlProvider> </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 ( return (
<ErrorBoundary> <ErrorBoundary>
<IntlProvider <IntlProvider
@ -332,10 +353,7 @@ export const App: React.FC = () => {
<ToastProvider> <ToastProvider>
<PluginsLoader> <PluginsLoader>
<AppContainer> <AppContainer>
<ConfigurationProvider <ConfigurationProvider configuration={config.data!.configuration}>
configuration={config.data?.configuration}
loading={config.loading}
>
{maybeRenderReleaseNotes()} {maybeRenderReleaseNotes()}
<ConnectionMonitor /> <ConnectionMonitor />
<Suspense fallback={<LoadingIndicator />}> <Suspense fallback={<LoadingIndicator />}>

View file

@ -6,7 +6,7 @@ import { Icon } from "src/components/Shared/Icon";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { Manual } from "../Help/Manual"; import { Manual } from "../Help/Manual";
import { withoutTypename } from "src/utils/data"; import { withoutTypename } from "src/utils/data";
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions"; import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
@ -25,7 +25,7 @@ export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
onClose, onClose,
type, type,
}) => { }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
function getDefaultOptions(): GQL.GenerateMetadataInput { function getDefaultOptions(): GQL.GenerateMetadataInput {
return { return {

View file

@ -1,9 +1,9 @@
import React, { useContext, useMemo } from "react"; import React, { useMemo } from "react";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { FrontPageContent, ICustomFilter } from "src/core/config"; import { FrontPageContent, ICustomFilter } from "src/core/config";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useFindSavedFilter } from "src/core/StashService"; 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 { ListFilterModel } from "src/models/list-filter/filter";
import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow"; import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow";
import { ImageRecommendationRow } from "../Images/ImageRecommendationRow"; import { ImageRecommendationRow } from "../Images/ImageRecommendationRow";
@ -105,7 +105,7 @@ interface ISavedFilterResults {
const SavedFilterResults: React.FC<ISavedFilterResults> = ({ const SavedFilterResults: React.FC<ISavedFilterResults> = ({
savedFilterID, savedFilterID,
}) => { }) => {
const { configuration: config } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const { loading, data } = useFindSavedFilter(savedFilterID.toString()); const { loading, data } = useFindSavedFilter(savedFilterID.toString());
const filter = useMemo(() => { const filter = useMemo(() => {
@ -136,7 +136,7 @@ interface ICustomFilterProps {
const CustomFilterResults: React.FC<ICustomFilterProps> = ({ const CustomFilterResults: React.FC<ICustomFilterProps> = ({
customFilter, customFilter,
}) => { }) => {
const { configuration: config } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const filter = useMemo(() => { const filter = useMemo(() => {

View file

@ -6,7 +6,7 @@ import { Button } from "react-bootstrap";
import { FrontPageConfig } from "./FrontPageConfig"; import { FrontPageConfig } from "./FrontPageConfig";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { Control } from "./Control"; import { Control } from "./Control";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { import {
FrontPageContent, FrontPageContent,
generateDefaultFrontPageContent, generateDefaultFrontPageContent,
@ -24,7 +24,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => {
const [saveUI] = useConfigureUI(); const [saveUI] = useConfigureUI();
const { configuration, loading } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
useScrollToTopOnMount(); useScrollToTopOnMount();
@ -51,7 +51,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => {
setSaving(false); setSaving(false);
} }
if (loading || saving) { if (saving) {
return <LoadingIndicator />; return <LoadingIndicator />;
} }

View file

@ -4,7 +4,7 @@ import { useFindSavedFilters } from "src/core/StashService";
import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { Button, Form, Modal } from "react-bootstrap"; import { Button, Form, Modal } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { import {
ISavedFilterRow, ISavedFilterRow,
ICustomFilter, ICustomFilter,
@ -277,11 +277,11 @@ interface IFrontPageConfigProps {
export const FrontPageConfig: React.FC<IFrontPageConfigProps> = ({ export const FrontPageConfig: React.FC<IFrontPageConfigProps> = ({
onClose, onClose,
}) => { }) => {
const { configuration, loading } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const ui = configuration?.ui; const ui = configuration?.ui;
const { data: allFilters, loading: loading2 } = useFindSavedFilters(); const { data: allFilters, loading } = useFindSavedFilters();
const [isAdd, setIsAdd] = useState(false); const [isAdd, setIsAdd] = useState(false);
const [currentContent, setCurrentContent] = useState<FrontPageContent[]>([]); const [currentContent, setCurrentContent] = useState<FrontPageContent[]>([]);
@ -338,7 +338,7 @@ export const FrontPageConfig: React.FC<IFrontPageConfigProps> = ({
setDragIndex(undefined); setDragIndex(undefined);
} }
if (loading || loading2) { if (loading) {
return <LoadingIndicator />; return <LoadingIndicator />;
} }

View file

@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "../Shared/Modal"; import { ModalComponent } from "../Shared/Modal";
import { useToast } from "src/hooks/Toast"; 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 { FormattedMessage, useIntl } from "react-intl";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
@ -33,7 +33,7 @@ export const DeleteGalleriesDialog: React.FC<IDeleteGalleryDialogProps> = (
{ count: props.selected.length, singularEntity, pluralEntity } { count: props.selected.length, singularEntity, pluralEntity }
); );
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const [deleteFile, setDeleteFile] = useState<boolean>( const [deleteFile, setDeleteFile] = useState<boolean>(
config?.defaults.deleteFile ?? false config?.defaults.deleteFile ?? false

View file

@ -1,5 +1,5 @@
import { Button, Tab, Nav, Dropdown } from "react-bootstrap"; import { Button, Tab, Nav, Dropdown } from "react-bootstrap";
import React, { useContext, useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { import {
useHistory, useHistory,
Link, Link,
@ -41,7 +41,7 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import cx from "classnames"; import cx from "classnames";
import { useRatingKeybinds } from "src/hooks/keybinds"; 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 { TruncatedText } from "src/components/Shared/TruncatedText";
import { goBackOrReplace } from "src/utils/history"; import { goBackOrReplace } from "src/utils/history";
@ -59,7 +59,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery, add }) => {
const history = useHistory(); const history = useHistory();
const Toast = useToast(); const Toast = useToast();
const intl = useIntl(); const intl = useIntl();
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters); const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);

View file

@ -161,32 +161,38 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
return ( return (
<> <>
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="title"
title={intl.formatMessage({ id: "title" })} title={intl.formatMessage({ id: "title" })}
result={title} result={title}
onChange={(value) => setTitle(value)} onChange={(value) => setTitle(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="code"
title={intl.formatMessage({ id: "scene_code" })} title={intl.formatMessage({ id: "scene_code" })}
result={code} result={code}
onChange={(value) => setCode(value)} onChange={(value) => setCode(value)}
/> />
<ScrapedStringListRow <ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })} title={intl.formatMessage({ id: "urls" })}
result={urls} result={urls}
onChange={(value) => setURLs(value)} onChange={(value) => setURLs(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="date"
title={intl.formatMessage({ id: "date" })} title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
result={date} result={date}
onChange={(value) => setDate(value)} onChange={(value) => setDate(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="photographer"
title={intl.formatMessage({ id: "photographer" })} title={intl.formatMessage({ id: "photographer" })}
result={photographer} result={photographer}
onChange={(value) => setPhotographer(value)} onChange={(value) => setPhotographer(value)}
/> />
<ScrapedStudioRow <ScrapedStudioRow
field="studio"
title={intl.formatMessage({ id: "studios" })} title={intl.formatMessage({ id: "studios" })}
result={studio} result={studio}
onChange={(value) => setStudio(value)} onChange={(value) => setStudio(value)}
@ -194,6 +200,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
onCreateNew={createNewStudio} onCreateNew={createNewStudio}
/> />
<ScrapedPerformersRow <ScrapedPerformersRow
field="performers"
title={intl.formatMessage({ id: "performers" })} title={intl.formatMessage({ id: "performers" })}
result={performers} result={performers}
onChange={(value) => setPerformers(value)} onChange={(value) => setPerformers(value)}
@ -203,6 +210,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
/> />
{scrapedTagsRow} {scrapedTagsRow}
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="details"
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}
result={details} result={details}
onChange={(value) => setDetails(value)} onChange={(value) => setDetails(value)}

View file

@ -12,7 +12,7 @@ import {
queryFindGalleriesForSelect, queryFindGalleriesForSelect,
queryFindGalleriesByIDForSelect, queryFindGalleriesByIDForSelect,
} from "src/core/StashService"; } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -70,7 +70,7 @@ const gallerySelectSort = PatchFunction(
const _GallerySelect: React.FC< const _GallerySelect: React.FC<
IFilterProps & IFilterValueProps<Gallery> & ExtraGalleryProps IFilterProps & IFilterValueProps<Gallery> & ExtraGalleryProps
> = (props) => { > = (props) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const maxOptionsShown = const maxOptionsShown =
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;

View file

@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons";
import { RelatedGroupPopoverButton } from "./RelatedGroupPopover"; import { RelatedGroupPopoverButton } from "./RelatedGroupPopover";
import { SweatDrops } from "../Shared/SweatDrops"; import { OCounterButton } from "../Shared/CountButton";
const Description: React.FC<{ const Description: React.FC<{
sceneNumber?: number; sceneNumber?: number;
@ -111,16 +111,7 @@ export const GroupCard: React.FC<IProps> = ({
function maybeRenderOCounter() { function maybeRenderOCounter() {
if (!group.o_counter) return; if (!group.o_counter) return;
return ( return <OCounterButton value={group.o_counter} />;
<div className="o-counter">
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{group.o_counter}</span>
</Button>
</div>
);
} }
function maybeRenderPopoverButtonGroup() { function maybeRenderPopoverButtonGroup() {

View file

@ -23,7 +23,7 @@ import {
import { GroupEditPanel } from "./GroupEditPanel"; import { GroupEditPanel } from "./GroupEditPanel";
import { faRefresh, faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { faRefresh, faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; 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 { DetailImage } from "src/components/Shared/DetailImage";
import { useRatingKeybinds } from "src/hooks/keybinds"; import { useRatingKeybinds } from "src/hooks/keybinds";
import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useLoadStickyHeader } from "src/hooks/detailsPanel";
@ -146,7 +146,7 @@ const GroupPage: React.FC<IProps> = ({ group, tabKey }) => {
const Toast = useToast(); const Toast = useToast();
// Configuration settings // Configuration settings
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false; const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false;
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;

View file

@ -149,37 +149,44 @@ export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
return ( return (
<> <>
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="name"
title={intl.formatMessage({ id: "name" })} title={intl.formatMessage({ id: "name" })}
result={name} result={name}
onChange={(value) => setName(value)} onChange={(value) => setName(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="aliases"
title={intl.formatMessage({ id: "aliases" })} title={intl.formatMessage({ id: "aliases" })}
result={aliases} result={aliases}
onChange={(value) => setAliases(value)} onChange={(value) => setAliases(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="duration"
title={intl.formatMessage({ id: "duration" })} title={intl.formatMessage({ id: "duration" })}
result={duration} result={duration}
onChange={(value) => setDuration(value)} onChange={(value) => setDuration(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="date"
title={intl.formatMessage({ id: "date" })} title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
result={date} result={date}
onChange={(value) => setDate(value)} onChange={(value) => setDate(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="director"
title={intl.formatMessage({ id: "director" })} title={intl.formatMessage({ id: "director" })}
result={director} result={director}
onChange={(value) => setDirector(value)} onChange={(value) => setDirector(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="synopsis"
title={intl.formatMessage({ id: "synopsis" })} title={intl.formatMessage({ id: "synopsis" })}
result={synopsis} result={synopsis}
onChange={(value) => setSynopsis(value)} onChange={(value) => setSynopsis(value)}
/> />
<ScrapedStudioRow <ScrapedStudioRow
field="studio"
title={intl.formatMessage({ id: "studios" })} title={intl.formatMessage({ id: "studios" })}
result={studio} result={studio}
onChange={(value) => setStudio(value)} onChange={(value) => setStudio(value)}
@ -187,18 +194,21 @@ export const GroupScrapeDialog: React.FC<IGroupScrapeDialogProps> = ({
onCreateNew={createNewStudio} onCreateNew={createNewStudio}
/> />
<ScrapedStringListRow <ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })} title={intl.formatMessage({ id: "urls" })}
result={urls} result={urls}
onChange={(value) => setURLs(value)} onChange={(value) => setURLs(value)}
/> />
{scrapedTagsRow} {scrapedTagsRow}
<ScrapedImageRow <ScrapedImageRow
field="front_image"
title="Front Image" title="Front Image"
className="group-image" className="group-image"
result={frontImage} result={frontImage}
onChange={(value) => setFrontImage(value)} onChange={(value) => setFrontImage(value)}
/> />
<ScrapedImageRow <ScrapedImageRow
field="back_image"
title="Back Image" title="Back Image"
className="group-image" className="group-image"
result={backImage} result={backImage}

View file

@ -13,7 +13,7 @@ import {
queryFindGroupsByIDForSelect, queryFindGroupsByIDForSelect,
useGroupCreate, useGroupCreate,
} from "src/core/StashService"; } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -66,7 +66,7 @@ export const GroupSelect: React.FC<
> = PatchComponent("GroupSelect", (props) => { > = PatchComponent("GroupSelect", (props) => {
const [createGroup] = useGroupCreate(); const [createGroup] = useGroupCreate();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const maxOptionsShown = const maxOptionsShown =
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;

View file

@ -4,7 +4,7 @@ import { useImagesDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "src/components/Shared/Modal"; import { ModalComponent } from "src/components/Shared/Modal";
import { useToast } from "src/hooks/Toast"; 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 { FormattedMessage, useIntl } from "react-intl";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
@ -33,7 +33,7 @@ export const DeleteImagesDialog: React.FC<IDeleteImageDialogProps> = (
{ count: props.selected.length, singularEntity, pluralEntity } { count: props.selected.length, singularEntity, pluralEntity }
); );
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const [deleteFile, setDeleteFile] = useState<boolean>( const [deleteFile, setDeleteFile] = useState<boolean>(
config?.defaults.deleteFile ?? false config?.defaults.deleteFile ?? false

View file

@ -5,7 +5,6 @@ import * as GQL from "src/core/generated-graphql";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { GalleryLink, TagLink } from "src/components/Shared/TagLink"; import { GalleryLink, TagLink } from "src/components/Shared/TagLink";
import { HoverPopover } from "src/components/Shared/HoverPopover"; import { HoverPopover } from "src/components/Shared/HoverPopover";
import { SweatDrops } from "src/components/Shared/SweatDrops";
import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton";
import { GridCard } from "src/components/Shared/GridCard/GridCard"; import { GridCard } from "src/components/Shared/GridCard/GridCard";
import { RatingBanner } from "src/components/Shared/RatingBanner"; import { RatingBanner } from "src/components/Shared/RatingBanner";
@ -18,6 +17,7 @@ import {
import { imageTitle } from "src/core/files"; import { imageTitle } from "src/core/files";
import { TruncatedText } from "../Shared/TruncatedText"; import { TruncatedText } from "../Shared/TruncatedText";
import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay";
import { OCounterButton } from "../Shared/CountButton";
interface IImageCardProps { interface IImageCardProps {
image: GQL.SlimImageDataFragment; image: GQL.SlimImageDataFragment;
@ -74,16 +74,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
function maybeRenderOCounter() { function maybeRenderOCounter() {
if (props.image.o_counter) { if (props.image.o_counter) {
return ( return <OCounterButton value={props.image.o_counter} />;
<div className="o-count">
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{props.image.o_counter}</span>
</Button>
</div>
);
} }
} }

View file

@ -1,5 +1,5 @@
import { Tab, Nav, Dropdown } from "react-bootstrap"; 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 { FormattedDate, FormattedMessage, useIntl } from "react-intl";
import { useHistory, Link, RouteComponentProps } from "react-router-dom"; import { useHistory, Link, RouteComponentProps } from "react-router-dom";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
@ -29,7 +29,7 @@ import { imagePath, imageTitle } from "src/core/files";
import { isVideo } from "src/utils/visualFile"; import { isVideo } from "src/utils/visualFile";
import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
import { useRatingKeybinds } from "src/hooks/keybinds"; 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 TextUtils from "src/utils/text";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import cx from "classnames"; import cx from "classnames";
@ -48,7 +48,7 @@ const ImagePage: React.FC<IProps> = ({ image }) => {
const history = useHistory(); const history = useHistory();
const Toast = useToast(); const Toast = useToast();
const intl = useIntl(); const intl = useIntl();
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const [incrementO] = useImageIncrementO(image.id); const [incrementO] = useImageIncrementO(image.id);
const [decrementO] = useImageDecrementO(image.id); const [decrementO] = useImageDecrementO(image.id);

View file

@ -163,32 +163,38 @@ export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
return ( return (
<> <>
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="title"
title={intl.formatMessage({ id: "title" })} title={intl.formatMessage({ id: "title" })}
result={title} result={title}
onChange={(value) => setTitle(value)} onChange={(value) => setTitle(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="code"
title={intl.formatMessage({ id: "scene_code" })} title={intl.formatMessage({ id: "scene_code" })}
result={code} result={code}
onChange={(value) => setCode(value)} onChange={(value) => setCode(value)}
/> />
<ScrapedStringListRow <ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })} title={intl.formatMessage({ id: "urls" })}
result={urls} result={urls}
onChange={(value) => setURLs(value)} onChange={(value) => setURLs(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="date"
title={intl.formatMessage({ id: "date" })} title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
result={date} result={date}
onChange={(value) => setDate(value)} onChange={(value) => setDate(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="photographer"
title={intl.formatMessage({ id: "photographer" })} title={intl.formatMessage({ id: "photographer" })}
result={photographer} result={photographer}
onChange={(value) => setPhotographer(value)} onChange={(value) => setPhotographer(value)}
/> />
<ScrapedStudioRow <ScrapedStudioRow
field="studio"
title={intl.formatMessage({ id: "studios" })} title={intl.formatMessage({ id: "studios" })}
result={studio} result={studio}
onChange={(value) => setStudio(value)} onChange={(value) => setStudio(value)}
@ -196,6 +202,7 @@ export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
onCreateNew={createNewStudio} onCreateNew={createNewStudio}
/> />
<ScrapedPerformersRow <ScrapedPerformersRow
field="performers"
title={intl.formatMessage({ id: "performers" })} title={intl.formatMessage({ id: "performers" })}
result={performers} result={performers}
onChange={(value) => setPerformers(value)} onChange={(value) => setPerformers(value)}
@ -204,6 +211,7 @@ export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
/> />
{scrapedTagsRow} {scrapedTagsRow}
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="details"
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}
result={details} result={details}
onChange={(value) => setDetails(value)} onChange={(value) => setDetails(value)}

View file

@ -1,10 +1,4 @@
import React, { import React, { useCallback, useState, useMemo, MouseEvent } from "react";
useCallback,
useState,
useMemo,
MouseEvent,
useContext,
} from "react";
import { FormattedNumber, useIntl } from "react-intl"; import { FormattedNumber, useIntl } from "react-intl";
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@ -23,7 +17,7 @@ import "flexbin/flexbin.css";
import Gallery, { RenderImageProps } from "react-photo-gallery"; import Gallery, { RenderImageProps } from "react-photo-gallery";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { ImageGridCard } from "./ImageGridCard"; import { ImageGridCard } from "./ImageGridCard";
import { View } from "../List/views"; import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar"; import { IItemListOperation } from "../List/FilteredListToolbar";
@ -51,7 +45,7 @@ const ImageWall: React.FC<IImageWallProps> = ({
zoomIndex, zoomIndex,
handleImageOpen, handleImageOpen,
}) => { }) => {
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const containerRef = React.useRef<HTMLDivElement>(null); const containerRef = React.useRef<HTMLDivElement>(null);

View file

@ -1,7 +1,6 @@
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
import React, { import React, {
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
@ -14,7 +13,7 @@ import {
CriterionOption, CriterionOption,
} from "src/models/list-filter/criteria/criterion"; } from "src/models/list-filter/criteria/criterion";
import { FormattedMessage, useIntl } from "react-intl"; 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 { ListFilterModel } from "src/models/list-filter/filter";
import { getFilterOptions } from "src/models/list-filter/factory"; import { getFilterOptions } from "src/models/list-filter/factory";
import { FilterTags } from "./FilterTags"; import { FilterTags } from "./FilterTags";
@ -65,6 +64,9 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
onTogglePin, onTogglePin,
externallySelected = false, externallySelected = false,
}) => { }) => {
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const prevCriterion = usePrevious(currentCriterion); const prevCriterion = usePrevious(currentCriterion);
const scrolled = useRef(false); const scrolled = useRef(false);
@ -148,7 +150,9 @@ const CriterionOptionList: React.FC<ICriterionList> = ({
className="collapse-icon fa-fw" className="collapse-icon fa-fw"
icon={type === c.type ? faChevronDown : faChevronRight} icon={type === c.type ? faChevronDown : faChevronRight}
/> />
<FormattedMessage id={c.messageID} /> <FormattedMessage
id={!sfwContentMode ? c.messageID : c.sfwMessageID ?? c.messageID}
/>
</span> </span>
{criteria.some((cc) => c.type === cc) && ( {criteria.some((cc) => c.type === cc) && (
<Button <Button
@ -233,7 +237,8 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
const Toast = useToast(); const Toast = useToast();
const intl = useIntl(); const intl = useIntl();
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const [searchValue, setSearchValue] = useState(""); const [searchValue, setSearchValue] = useState("");
const [currentFilter, setCurrentFilter] = useState<ListFilterModel>( const [currentFilter, setCurrentFilter] = useState<ListFilterModel>(
@ -265,10 +270,16 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
.filter((c) => !c.hidden) .filter((c) => !c.hidden)
.sort((a, b) => { .sort((a, b) => {
return intl return intl
.formatMessage({ id: a.messageID }) .formatMessage({
.localeCompare(intl.formatMessage({ id: b.messageID })); 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( const optionSelected = useCallback(
(option?: CriterionOption) => { (option?: CriterionOption) => {
@ -302,11 +313,13 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
return criterionOptions.filter((c) => { return criterionOptions.filter((c) => {
return intl return intl
.formatMessage({ id: c.messageID }) .formatMessage({
id: !sfwContentMode ? c.messageID : c.sfwMessageID ?? c.messageID,
})
.toLowerCase() .toLowerCase()
.includes(trimmedSearch); .includes(trimmedSearch);
}); });
}, [intl, searchValue, criterionOptions]); }, [intl, sfwContentMode, searchValue, criterionOptions]);
const pinnedFilters = useMemo( const pinnedFilters = useMemo(
() => ui.pinnedFilters?.[filterModeToConfigKey(currentFilter.mode)] ?? [], () => ui.pinnedFilters?.[filterModeToConfigKey(currentFilter.mode)] ?? [],
@ -517,7 +530,10 @@ export const EditFilterDialog: React.FC<IEditFilterProps> = ({
<Modal <Modal
show={!showSaveDialog && !showLoadDialog} show={!showSaveDialog && !showLoadDialog}
onHide={() => onCancel()} 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> <Modal.Header>
<div> <div>

View file

@ -14,6 +14,7 @@ import { BsPrefixProps, ReplaceProps } from "react-bootstrap/esm/helpers";
import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields"; import { CustomFieldsCriterion } from "src/models/list-filter/criteria/custom-fields";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
import cx from "classnames"; import cx from "classnames";
import { useConfigurationContext } from "src/hooks/Config";
type TagItemProps = PropsWithChildren< type TagItemProps = PropsWithChildren<
ReplaceProps<"span", BsPrefixProps<"span"> & BadgeProps> ReplaceProps<"span", BsPrefixProps<"span"> & BadgeProps>
@ -125,6 +126,9 @@ export const FilterTags: React.FC<IFilterTagsProps> = ({
const intl = useIntl(); const intl = useIntl();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const [cutoff, setCutoff] = React.useState<number | undefined>(); const [cutoff, setCutoff] = React.useState<number | undefined>();
const elementGap = 10; // Adjust this value based on your CSS gap or margin const elementGap = 10; // Adjust this value based on your CSS gap or margin
const moreTagWidth = 80; // reserve space for the "more" tag const moreTagWidth = 80; // reserve space for the "more" tag
@ -270,7 +274,7 @@ export const FilterTags: React.FC<IFilterTagsProps> = ({
return ( return (
<FilterTag <FilterTag
key={criterion.getId()} key={criterion.getId()}
label={criterion.getLabel(intl)} label={criterion.getLabel(intl, sfwContentMode)}
onClick={() => onClickCriterionTag(criterion)} onClick={() => onClickCriterionTag(criterion)}
onRemove={($event) => onRemoveCriterionTag(criterion, $event)} onRemove={($event) => onRemoveCriterionTag(criterion, $event)}
/> />

View file

@ -2,7 +2,7 @@ import React from "react";
import { Form } from "react-bootstrap"; import { Form } from "react-bootstrap";
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
import { CriterionModifier } from "src/core/generated-graphql"; import { CriterionModifier } from "src/core/generated-graphql";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { import {
ModifierCriterion, ModifierCriterion,
CriterionValue, CriterionValue,
@ -17,7 +17,7 @@ export const PathFilter: React.FC<IInputFilterProps> = ({
criterion, criterion,
onValueChanged, onValueChanged,
}) => { }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const libraryPaths = configuration?.general.stashes.map((s) => s.path); const libraryPaths = configuration?.general.stashes.map((s) => s.path);
// don't show folder select for regex // don't show folder select for regex

View file

@ -12,7 +12,7 @@ import {
defaultRatingStarPrecision, defaultRatingStarPrecision,
defaultRatingSystemOptions, defaultRatingSystemOptions,
} from "src/utils/rating"; } 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 { RatingCriterion } from "src/models/list-filter/criteria/rating";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { Option, SidebarListFilter } from "./SidebarListFilter"; import { Option, SidebarListFilter } from "./SidebarListFilter";
@ -117,7 +117,7 @@ export const SidebarRatingFilter: React.FC<ISidebarFilter> = ({
[noneLabel] [noneLabel]
); );
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const ratingSystemOptions = const ratingSystemOptions =
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;

View file

@ -1,7 +1,6 @@
import React, { import React, {
PropsWithChildren, PropsWithChildren,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useState, useState,
@ -43,7 +42,7 @@ import {
IItemListOperation, IItemListOperation,
} from "./FilteredListToolbar"; } from "./FilteredListToolbar";
import { PagedList } from "./PagedList"; 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> { interface IFilteredItemList<T extends QueryResult, E extends IHasID = IHasID> {
filterStateProps: IFilterStateHook; filterStateProps: IFilterStateHook;
@ -55,7 +54,7 @@ export function useFilteredItemList<
T extends QueryResult, T extends QueryResult,
E extends IHasID = IHasID E extends IHasID = IHasID
>(props: IFilteredItemList<T, E>) { >(props: IFilteredItemList<T, E>) {
const { configuration: config } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
// States // States
const filterState = useFilterState({ const filterState = useFilterState({
@ -393,7 +392,7 @@ export const ItemListContext = <T extends QueryResult, E extends IHasID>(
children, children,
} = props; } = props;
const { configuration: config } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const emptyFilter = useMemo( const emptyFilter = useMemo(
() => () =>
@ -409,12 +408,7 @@ export const ItemListContext = <T extends QueryResult, E extends IHasID>(
new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort }) new ListFilterModel(filterMode, config, { defaultSortBy: defaultSort })
); );
const { defaultFilter, loading: defaultFilterLoading } = useDefaultFilter( const { defaultFilter } = useDefaultFilter(emptyFilter, view);
emptyFilter,
view
);
if (defaultFilterLoading) return null;
return ( return (
<FilterContext filter={filter} setFilter={setFilterState}> <FilterContext filter={filter} setFilter={setFilterState}>

View file

@ -37,6 +37,7 @@ import { View } from "./views";
import { ClearableInput } from "../Shared/ClearableInput"; import { ClearableInput } from "../Shared/ClearableInput";
import { useStopWheelScroll } from "src/utils/form"; import { useStopWheelScroll } from "src/utils/form";
import { ISortByOption } from "src/models/list-filter/filter-options"; import { ISortByOption } from "src/models/list-filter/filter-options";
import { useConfigurationContext } from "src/hooks/Config";
export function useDebouncedSearchInput( export function useDebouncedSearchInput(
filter: ListFilterModel, filter: ListFilterModel,
@ -249,14 +250,24 @@ export const SortBySelect: React.FC<{
onReshuffleRandomSort, onReshuffleRandomSort,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const currentSortBy = options.find((o) => o.value === sortBy); const currentSortBy = options.find((o) => o.value === sortBy);
const currentSortByMessageID = currentSortBy
? !sfwContentMode
? currentSortBy.messageID
: currentSortBy.sfwMessageID ?? currentSortBy.messageID
: "";
function renderSortByOptions() { function renderSortByOptions() {
return options return options
.map((o) => { .map((o) => {
const messageID = !sfwContentMode
? o.messageID
: o.sfwMessageID ?? o.messageID;
return { return {
message: intl.formatMessage({ id: o.messageID }), message: intl.formatMessage({ id: messageID }),
value: o.value, value: o.value,
}; };
}) })
@ -267,6 +278,7 @@ export const SortBySelect: React.FC<{
key={option.value} key={option.value}
className="bg-secondary text-white" className="bg-secondary text-white"
eventKey={option.value} eventKey={option.value}
data-value={option.value}
> >
{option.message} {option.message}
</Dropdown.Item> </Dropdown.Item>
@ -274,11 +286,11 @@ export const SortBySelect: React.FC<{
} }
return ( return (
<Dropdown as={ButtonGroup} className={className}> <Dropdown as={ButtonGroup} className={`${className ?? ""} sort-by-select`}>
<InputGroup.Prepend> <InputGroup.Prepend>
<Dropdown.Toggle variant="secondary"> <Dropdown.Toggle variant="secondary">
{currentSortBy {currentSortBy
? intl.formatMessage({ id: currentSortBy.messageID }) ? intl.formatMessage({ id: currentSortByMessageID })
: ""} : ""}
</Dropdown.Toggle> </Dropdown.Toggle>
</InputGroup.Prepend> </InputGroup.Prepend>

View file

@ -1,11 +1,11 @@
import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import { useHistory, useLocation } from "react-router-dom"; import { useHistory, useLocation } from "react-router-dom";
import { isEqual, isFunction } from "lodash-es"; import { isEqual, isFunction } from "lodash-es";
import { QueryResult } from "@apollo/client"; import { QueryResult } from "@apollo/client";
import { IHasID } from "src/utils/data"; import { IHasID } from "src/utils/data";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { View } from "./views"; import { View } from "./views";
import { usePrevious } from "src/hooks/state"; import { usePrevious } from "src/hooks/state";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@ -94,7 +94,7 @@ export function useFilterURL(
} }
export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) { export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) {
const { configuration: config, loading } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const defaultFilter = useMemo(() => { const defaultFilter = useMemo(() => {
if (view && config?.ui.defaultFilters?.[view]) { if (view && config?.ui.defaultFilters?.[view]) {
@ -114,9 +114,9 @@ export function useDefaultFilter(emptyFilter: ListFilterModel, view?: View) {
} }
}, [view, config?.ui.defaultFilters, emptyFilter]); }, [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: { function useEmptyFilter(props: {
@ -158,14 +158,14 @@ export function useFilterState(
const emptyFilter = useEmptyFilter({ filterMode, defaultSort, config }); const emptyFilter = useEmptyFilter({ filterMode, defaultSort, config });
const { defaultFilter, loading } = useDefaultFilter(emptyFilter, view); const { defaultFilter } = useDefaultFilter(emptyFilter, view);
const { setFilter } = useFilterURL(filter, setFilterState, { const { setFilter } = useFilterURL(filter, setFilterState, {
defaultFilter, defaultFilter,
active: useURL, active: useURL,
}); });
return { loading, filter, setFilter }; return { filter, setFilter };
} }
export function useFilterOperations(props: { export function useFilterOperations(props: {

View file

@ -11,7 +11,7 @@ import {
MessageDescriptor, MessageDescriptor,
useIntl, useIntl,
} from "react-intl"; } 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 { IconDefinition } from "@fortawesome/fontawesome-svg-core";
import { LinkContainer } from "react-router-bootstrap"; import { LinkContainer } from "react-router-bootstrap";
import { Link, NavLink, useLocation, useHistory } from "react-router-dom"; import { Link, NavLink, useLocation, useHistory } from "react-router-dom";
@ -19,7 +19,7 @@ import Mousetrap from "mousetrap";
import SessionUtils from "src/utils/session"; import SessionUtils from "src/utils/session";
import { Icon } from "src/components/Shared/Icon"; 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 { ManualStateContext } from "./Help/context";
import { SettingsButton } from "./SettingsButton"; import { SettingsButton } from "./SettingsButton";
import { import {
@ -181,7 +181,7 @@ const MainNavbarUtilityItems = PatchComponent(
export const MainNavbar: React.FC = () => { export const MainNavbar: React.FC = () => {
const history = useHistory(); const history = useHistory();
const location = useLocation(); const location = useLocation();
const { configuration, loading } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const { openManual } = React.useContext(ManualStateContext); const { openManual } = React.useContext(ManualStateContext);
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
@ -359,35 +359,31 @@ export const MainNavbar: React.FC = () => {
ref={navbarRef} ref={navbarRef}
> >
<Navbar.Collapse className="bg-dark order-sm-1"> <Navbar.Collapse className="bg-dark order-sm-1">
<Fade in={!loading}> <MainNavbarMenuItems>
<> {menuItems.map(({ href, icon, message }) => (
<MainNavbarMenuItems> <Nav.Link
{menuItems.map(({ href, icon, message }) => ( eventKey={href}
<Nav.Link as="div"
eventKey={href} key={href}
as="div" className="col-4 col-sm-3 col-md-2 col-lg-auto"
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">
<LinkContainer activeClassName="active" exact to={href}> <Icon
<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"
{...{ icon }} />
className="nav-menu-icon d-block d-xl-inline mb-2 mb-xl-0" <span>{intl.formatMessage(message)}</span>
/> </Button>
<span>{intl.formatMessage(message)}</span> </LinkContainer>
</Button> </Nav.Link>
</LinkContainer> ))}
</Nav.Link> </MainNavbarMenuItems>
))} <Nav>
</MainNavbarMenuItems> <MainNavbarUtilityItems>
<Nav> {renderUtilityButtons()}
<MainNavbarUtilityItems> </MainNavbarUtilityItems>
{renderUtilityButtons()} </Nav>
</MainNavbarUtilityItems>
</Nav>
</>
</Fade>
</Navbar.Collapse> </Navbar.Collapse>
<Navbar.Brand as="div" onClick={handleDismiss}> <Navbar.Brand as="div" onClick={handleDismiss}>

View file

@ -27,6 +27,8 @@ import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput";
import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; import { faPencilAlt } from "@fortawesome/free-solid-svg-icons";
import * as FormUtils from "src/utils/form"; import * as FormUtils from "src/utils/form";
import { CountrySelect } from "../Shared/CountrySelect"; import { CountrySelect } from "../Shared/CountrySelect";
import { useConfigurationContext } from "src/hooks/Config";
import cx from "classnames";
interface IListOperationProps { interface IListOperationProps {
selected: GQL.SlimPerformerDataFragment[]; selected: GQL.SlimPerformerDataFragment[];
@ -61,6 +63,10 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
) => { ) => {
const intl = useIntl(); const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({ const [tagIds, setTagIds] = useState<GQL.BulkUpdateIds>({
mode: GQL.BulkUpdateIdMode.Add, mode: GQL.BulkUpdateIdMode.Add,
}); });
@ -204,7 +210,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
setter: (newValue: string | undefined) => void setter: (newValue: string | undefined) => void
) { ) {
return ( return (
<Form.Group controlId={name}> <Form.Group controlId={name} data-field={name}>
<Form.Label> <Form.Label>
<FormattedMessage id={name} /> <FormattedMessage id={name} />
</Form.Label> </Form.Label>
@ -218,9 +224,13 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
} }
function render() { function render() {
// sfw class needs to be set because it is outside body
return ( return (
<ModalComponent <ModalComponent
dialogClassName="edit-performers-dialog" dialogClassName={cx("edit-performers-dialog", {
"sfw-content-mode": sfwContentMode,
})}
show show
icon={faPencilAlt} icon={faPencilAlt}
header={intl.formatMessage( header={intl.formatMessage(
@ -238,7 +248,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
}} }}
isRunning={isUpdating} isRunning={isUpdating}
> >
<Form.Group controlId="rating" as={Row}> <Form.Group controlId="rating" as={Row} data-field={name}>
{FormUtils.renderLabel({ {FormUtils.renderLabel({
title: intl.formatMessage({ id: "rating" }), title: intl.formatMessage({ id: "rating" }),
})} })}
@ -322,7 +332,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
setPenisLength(v) setPenisLength(v)
)} )}
<Form.Group> <Form.Group data-field="circumcised">
<Form.Label> <Form.Label>
<FormattedMessage id="circumcised" /> <FormattedMessage id="circumcised" />
</Form.Label> </Form.Label>

View file

@ -6,7 +6,6 @@ import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { GridCard } from "../Shared/GridCard/GridCard"; import { GridCard } from "../Shared/GridCard/GridCard";
import { CountryFlag } from "../Shared/CountryFlag"; import { CountryFlag } from "../Shared/CountryFlag";
import { SweatDrops } from "../Shared/SweatDrops";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { TagLink } from "../Shared/TagLink"; import { TagLink } from "../Shared/TagLink";
@ -25,7 +24,8 @@ import { ILabeledId } from "src/models/list-filter/types";
import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { FavoriteIcon } from "../Shared/FavoriteIcon";
import { PatchComponent } from "src/patch"; import { PatchComponent } from "src/patch";
import { ExternalLinksButton } from "../Shared/ExternalLinksButton"; 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 { export interface IPerformerCardExtraCriteria {
scenes?: ModifierCriterion<CriterionValue>[]; scenes?: ModifierCriterion<CriterionValue>[];
@ -103,16 +103,7 @@ const PerformerCardPopovers: React.FC<IPerformerCardProps> = PatchComponent(
function maybeRenderOCounter() { function maybeRenderOCounter() {
if (!performer.o_counter) return; if (!performer.o_counter) return;
return ( return <OCounterButton value={performer.o_counter} />;
<div className="o-counter">
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{performer.o_counter}</span>
</Button>
</div>
);
} }
function maybeRenderTagPopoverButton() { function maybeRenderTagPopoverButton() {
@ -179,7 +170,7 @@ const PerformerCardPopovers: React.FC<IPerformerCardProps> = PatchComponent(
const PerformerCardOverlays: React.FC<IPerformerCardProps> = PatchComponent( const PerformerCardOverlays: React.FC<IPerformerCardProps> = PatchComponent(
"PerformerCard.Overlays", "PerformerCard.Overlays",
({ performer }) => { ({ performer }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const [updatePerformer] = usePerformerUpdate(); const [updatePerformer] = usePerformerUpdate();

View file

@ -16,7 +16,7 @@ import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { ErrorMessage } from "src/components/Shared/ErrorMessage";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useToast } from "src/hooks/Toast"; 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 { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { import {
CompressedPerformerDetailsPanel, CompressedPerformerDetailsPanel,
@ -42,13 +42,13 @@ import {
import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle";
import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; import { ExpandCollapseButton } from "src/components/Shared/CollapseButton";
import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; import { FavoriteIcon } from "src/components/Shared/FavoriteIcon";
import { SweatDrops } from "src/components/Shared/SweatDrops";
import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
import { LightboxLink } from "src/hooks/Lightbox/LightboxLink"; import { LightboxLink } from "src/hooks/Lightbox/LightboxLink";
import { PatchComponent } from "src/patch"; import { PatchComponent } from "src/patch";
import { ILightboxImage } from "src/hooks/Lightbox/types"; import { ILightboxImage } from "src/hooks/Lightbox/types";
import { goBackOrReplace } from "src/utils/history"; import { goBackOrReplace } from "src/utils/history";
import { OCounterButton } from "src/components/Shared/CountButton";
interface IProps { interface IProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
@ -240,7 +240,7 @@ const PerformerPage: React.FC<IProps> = PatchComponent(
const intl = useIntl(); const intl = useIntl();
// Configuration settings // Configuration settings
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
const enableBackgroundImage = const enableBackgroundImage =
@ -432,12 +432,7 @@ const PerformerPage: React.FC<IProps> = PatchComponent(
withoutContext withoutContext
/> />
{!!performer.o_counter && ( {!!performer.o_counter && (
<span className="o-counter"> <OCounterButton value={performer.o_counter} />
<span className="fa-icon">
<SweatDrops />
</span>
<span>{performer.o_counter}</span>
</span>
)} )}
</div> </div>
{!isEditing && ( {!isEditing && (

View file

@ -30,7 +30,7 @@ import {
stringCircumMap, stringCircumMap,
stringToCircumcised, stringToCircumcised,
} from "src/utils/circumcised"; } from "src/utils/circumcised";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerScrapeModal from "./PerformerScrapeModal";
import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal";
@ -97,7 +97,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const [scrapedPerformer, setScrapedPerformer] = const [scrapedPerformer, setScrapedPerformer] =
useState<GQL.ScrapedPerformer>(); useState<GQL.ScrapedPerformer>();
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();

View file

@ -63,6 +63,7 @@ function renderScrapedGenderRow(
) { ) {
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
field="gender"
title={title} title={title}
result={result} result={result}
renderOriginalField={() => renderScrapedGender(result)} renderOriginalField={() => renderScrapedGender(result)}
@ -113,6 +114,7 @@ function renderScrapedCircumcisedRow(
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={title} title={title}
field="circumcised"
result={result} result={result}
renderOriginalField={() => renderScrapedCircumcised(result)} renderOriginalField={() => renderScrapedCircumcised(result)}
renderNewField={() => renderNewField={() =>
@ -401,16 +403,19 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
return ( return (
<> <>
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="name"
title={intl.formatMessage({ id: "name" })} title={intl.formatMessage({ id: "name" })}
result={name} result={name}
onChange={(value) => setName(value)} onChange={(value) => setName(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="disambiguation"
title={intl.formatMessage({ id: "disambiguation" })} title={intl.formatMessage({ id: "disambiguation" })}
result={disambiguation} result={disambiguation}
onChange={(value) => setDisambiguation(value)} onChange={(value) => setDisambiguation(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="aliases"
title={intl.formatMessage({ id: "aliases" })} title={intl.formatMessage({ id: "aliases" })}
result={aliases} result={aliases}
onChange={(value) => setAliases(value)} onChange={(value) => setAliases(value)}
@ -421,46 +426,55 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
(value) => setGender(value) (value) => setGender(value)
)} )}
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="birthdate"
title={intl.formatMessage({ id: "birthdate" })} title={intl.formatMessage({ id: "birthdate" })}
result={birthdate} result={birthdate}
onChange={(value) => setBirthdate(value)} onChange={(value) => setBirthdate(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="death_date"
title={intl.formatMessage({ id: "death_date" })} title={intl.formatMessage({ id: "death_date" })}
result={deathDate} result={deathDate}
onChange={(value) => setDeathDate(value)} onChange={(value) => setDeathDate(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="ethnicity"
title={intl.formatMessage({ id: "ethnicity" })} title={intl.formatMessage({ id: "ethnicity" })}
result={ethnicity} result={ethnicity}
onChange={(value) => setEthnicity(value)} onChange={(value) => setEthnicity(value)}
/> />
<ScrapedCountryRow <ScrapedCountryRow
field="country"
title={intl.formatMessage({ id: "country" })} title={intl.formatMessage({ id: "country" })}
result={country} result={country}
onChange={(value) => setCountry(value)} onChange={(value) => setCountry(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="hair_color"
title={intl.formatMessage({ id: "hair_color" })} title={intl.formatMessage({ id: "hair_color" })}
result={hairColor} result={hairColor}
onChange={(value) => setHairColor(value)} onChange={(value) => setHairColor(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="eye_color"
title={intl.formatMessage({ id: "eye_color" })} title={intl.formatMessage({ id: "eye_color" })}
result={eyeColor} result={eyeColor}
onChange={(value) => setEyeColor(value)} onChange={(value) => setEyeColor(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="weight"
title={intl.formatMessage({ id: "weight" })} title={intl.formatMessage({ id: "weight" })}
result={weight} result={weight}
onChange={(value) => setWeight(value)} onChange={(value) => setWeight(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="height"
title={intl.formatMessage({ id: "height" })} title={intl.formatMessage({ id: "height" })}
result={height} result={height}
onChange={(value) => setHeight(value)} onChange={(value) => setHeight(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="penis_length"
title={intl.formatMessage({ id: "penis_length" })} title={intl.formatMessage({ id: "penis_length" })}
result={penisLength} result={penisLength}
onChange={(value) => setPenisLength(value)} onChange={(value) => setPenisLength(value)}
@ -471,42 +485,50 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
(value) => setCircumcised(value) (value) => setCircumcised(value)
)} )}
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="measurements"
title={intl.formatMessage({ id: "measurements" })} title={intl.formatMessage({ id: "measurements" })}
result={measurements} result={measurements}
onChange={(value) => setMeasurements(value)} onChange={(value) => setMeasurements(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="fake_tits"
title={intl.formatMessage({ id: "fake_tits" })} title={intl.formatMessage({ id: "fake_tits" })}
result={fakeTits} result={fakeTits}
onChange={(value) => setFakeTits(value)} onChange={(value) => setFakeTits(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="career_length"
title={intl.formatMessage({ id: "career_length" })} title={intl.formatMessage({ id: "career_length" })}
result={careerLength} result={careerLength}
onChange={(value) => setCareerLength(value)} onChange={(value) => setCareerLength(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="tattoos"
title={intl.formatMessage({ id: "tattoos" })} title={intl.formatMessage({ id: "tattoos" })}
result={tattoos} result={tattoos}
onChange={(value) => setTattoos(value)} onChange={(value) => setTattoos(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="piercings"
title={intl.formatMessage({ id: "piercings" })} title={intl.formatMessage({ id: "piercings" })}
result={piercings} result={piercings}
onChange={(value) => setPiercings(value)} onChange={(value) => setPiercings(value)}
/> />
<ScrapedStringListRow <ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })} title={intl.formatMessage({ id: "urls" })}
result={urls} result={urls}
onChange={(value) => setURLs(value)} onChange={(value) => setURLs(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="details"
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}
result={details} result={details}
onChange={(value) => setDetails(value)} onChange={(value) => setDetails(value)}
/> />
{scrapedTagsRow} {scrapedTagsRow}
<ScrapedImagesRow <ScrapedImagesRow
field="image"
title={intl.formatMessage({ id: "performer_image" })} title={intl.formatMessage({ id: "performer_image" })}
className="performer-image" className="performer-image"
result={image} result={image}
@ -514,6 +536,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
onChange={(value) => setImage(value)} onChange={(value) => setImage(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="remote_site_id"
title={intl.formatMessage({ id: "stash_id" })} title={intl.formatMessage({ id: "stash_id" })}
result={remoteSiteID} result={remoteSiteID}
locked locked

View file

@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import { useFindPerformer } from "../../core/StashService"; import { useFindPerformer } from "../../core/StashService";
import { PerformerCard } from "./PerformerCard"; import { PerformerCard } from "./PerformerCard";
import { ConfigurationContext } from "../../hooks/Config"; import { useConfigurationContext } from "../../hooks/Config";
import { Placement } from "react-bootstrap/esm/Overlay"; import { Placement } from "react-bootstrap/esm/Overlay";
interface IPeromerPopoverCardProps { interface IPeromerPopoverCardProps {
@ -49,7 +49,7 @@ export const PerformerPopover: React.FC<IPeroformerPopoverProps> = ({
placement = "top", placement = "top",
target, target,
}) => { }) => {
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true; const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true;

View file

@ -13,7 +13,7 @@ import {
queryFindPerformersByIDForSelect, queryFindPerformersByIDForSelect,
queryFindPerformersForSelect, queryFindPerformersForSelect,
} from "src/core/StashService"; } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -82,7 +82,7 @@ const _PerformerSelect: React.FC<
> = (props) => { > = (props) => {
const [createPerformer] = usePerformerCreate(); const [createPerformer] = usePerformerCreate();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const maxOptionsShown = const maxOptionsShown =
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;

View file

@ -1,7 +1,6 @@
import React, { import React, {
KeyboardEvent, KeyboardEvent,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useMemo, useMemo,
useRef, useRef,
@ -31,7 +30,7 @@ import {
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { import {
ConnectionState, ConnectionState,
InteractiveContext, InteractiveContext,
@ -240,7 +239,7 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = PatchComponent(
onNext, onNext,
onPrevious, onPrevious,
}) => { }) => {
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const interfaceConfig = configuration?.interface; const interfaceConfig = configuration?.interface;
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const videoRef = useRef<HTMLDivElement>(null); const videoRef = useRef<HTMLDivElement>(null);

View file

@ -4,7 +4,7 @@ import { useScenesDestroy } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "src/components/Shared/Modal"; import { ModalComponent } from "src/components/Shared/Modal";
import { useToast } from "src/hooks/Toast"; 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 { FormattedMessage, useIntl } from "react-intl";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { objectPath } from "src/core/files"; import { objectPath } from "src/core/files";
@ -34,7 +34,7 @@ export const DeleteScenesDialog: React.FC<IDeleteSceneDialogProps> = (
{ count: props.selected.length, singularEntity, pluralEntity } { count: props.selected.length, singularEntity, pluralEntity }
); );
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const [deleteFile, setDeleteFile] = useState<boolean>( const [deleteFile, setDeleteFile] = useState<boolean>(
config?.defaults.deleteFile ?? false config?.defaults.deleteFile ?? false

View file

@ -6,12 +6,11 @@ import * as GQL from "src/core/generated-graphql";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { GalleryLink, TagLink, SceneMarkerLink } from "../Shared/TagLink"; import { GalleryLink, TagLink, SceneMarkerLink } from "../Shared/TagLink";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import { SweatDrops } from "../Shared/SweatDrops";
import { TruncatedText } from "../Shared/TruncatedText"; import { TruncatedText } from "../Shared/TruncatedText";
import NavUtils from "src/utils/navigation"; import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard } from "../Shared/GridCard/GridCard"; import { GridCard } from "../Shared/GridCard/GridCard";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
@ -30,6 +29,7 @@ import { PatchComponent } from "src/patch";
import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay";
import { GroupTag } from "../Groups/GroupTag"; import { GroupTag } from "../Groups/GroupTag";
import { FileSize } from "../Shared/FileSize"; import { FileSize } from "../Shared/FileSize";
import { OCounterButton } from "../Shared/CountButton";
interface IScenePreviewProps { interface IScenePreviewProps {
isPortrait: boolean; isPortrait: boolean;
@ -218,16 +218,7 @@ const SceneCardPopovers = PatchComponent(
function maybeRenderOCounter() { function maybeRenderOCounter() {
if (props.scene.o_counter) { if (props.scene.o_counter) {
return ( return <OCounterButton value={props.scene.o_counter} />;
<div className="o-count">
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{props.scene.o_counter}</span>
</Button>
</div>
);
} }
} }
@ -353,7 +344,7 @@ const SceneCardImage = PatchComponent(
"SceneCard.Image", "SceneCard.Image",
(props: ISceneCardProps) => { (props: ISceneCardProps) => {
const history = useHistory(); const history = useHistory();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const cont = configuration?.interface.continuePlaylistDefault ?? false; const cont = configuration?.interface.continuePlaylistDefault ?? false;
const file = useMemo( const file = useMemo(
@ -437,7 +428,7 @@ const SceneCardImage = PatchComponent(
export const SceneCard = PatchComponent( export const SceneCard = PatchComponent(
"SceneCard", "SceneCard",
(props: ISceneCardProps) => { (props: ISceneCardProps) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const file = useMemo( const file = useMemo(
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined), () => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),

View file

@ -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 React, { useState } from "react";
import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap"; import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { SweatDrops } from "src/components/Shared/SweatDrops"; import { SweatDrops } from "src/components/Shared/SweatDrops";
import { useConfigurationContext } from "src/hooks/Config";
export interface IOCounterButtonProps { export interface IOCounterButtonProps {
value: number; value: number;
@ -17,6 +18,12 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
props: IOCounterButtonProps props: IOCounterButtonProps
) => { ) => {
const intl = useIntl(); 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); const [loading, setLoading] = useState(false);
async function increment() { async function increment() {
@ -44,9 +51,9 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
className="minimal pr-1" className="minimal pr-1"
onClick={increment} onClick={increment}
variant="secondary" variant="secondary"
title={intl.formatMessage({ id: "o_counter" })} title={intl.formatMessage({ id: messageID })}
> >
<SweatDrops /> {icon}
<span className="ml-2">{props.value}</span> <span className="ml-2">{props.value}</span>
</Button> </Button>
); );

View file

@ -3,7 +3,6 @@ import React, {
useEffect, useEffect,
useState, useState,
useMemo, useMemo,
useContext,
useRef, useRef,
useLayoutEffect, useLayoutEffect,
} from "react"; } from "react";
@ -32,7 +31,7 @@ import SceneQueue, { QueuedScene } from "src/models/sceneQueue";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import { OrganizedButton } from "./OrganizedButton"; import { OrganizedButton } from "./OrganizedButton";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { getPlayerPosition } from "src/components/ScenePlayer/util";
import { import {
faEllipsisV, faEllipsisV,
@ -184,7 +183,7 @@ const ScenePage: React.FC<IProps> = PatchComponent("ScenePage", (props) => {
const intl = useIntl(); const intl = useIntl();
const [updateScene] = useSceneUpdate(); const [updateScene] = useSceneUpdate();
const [generateScreenshot] = useSceneGenerateScreenshot(); const [generateScreenshot] = useSceneGenerateScreenshot();
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const [showDraftModal, setShowDraftModal] = useState(false); const [showDraftModal, setShowDraftModal] = useState(false);
const boxes = configuration?.general?.stashBoxes ?? []; const boxes = configuration?.general?.stashBoxes ?? [];
@ -689,7 +688,7 @@ const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
match, match,
}) => { }) => {
const { id } = match.params; const { id } = match.params;
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const { data, loading, error } = useFindScene(id); const { data, loading, error } = useFindScene(id);
const [scene, setScene] = useState<GQL.SceneDataFragment>(); const [scene, setScene] = useState<GQL.SceneDataFragment>();

View file

@ -19,7 +19,7 @@ import ImageUtils from "src/utils/image";
import { getStashIDs } from "src/utils/stashIds"; import { getStashIDs } from "src/utils/stashIds";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { Prompt } from "react-router-dom"; import { Prompt } from "react-router-dom";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable"; import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { faSearch } from "@fortawesome/free-solid-svg-icons";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
@ -103,7 +103,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
setStudio(scene.studio ?? null); setStudio(scene.studio ?? null);
}, [scene.studio]); }, [scene.studio]);
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);

View file

@ -21,6 +21,7 @@ import {
useSceneResetActivity, useSceneResetActivity,
} from "src/core/StashService"; } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useConfigurationContext } from "src/hooks/Config";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { TextField } from "src/utils/field"; import { TextField } from "src/utils/field";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
@ -172,6 +173,9 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
const intl = useIntl(); const intl = useIntl();
const Toast = useToast(); const Toast = useToast();
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const [dialogs, setDialogs] = React.useState({ const [dialogs, setDialogs] = React.useState({
playHistory: false, playHistory: false,
oHistory: false, oHistory: false,
@ -299,6 +303,9 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
} }
function maybeRenderDialogs() { function maybeRenderDialogs() {
const clearHistoryMessageID = sfwContentMode
? "dialogs.clear_o_history_confirm_sfw"
: "dialogs.clear_play_history_confirm";
return ( return (
<> <>
<AlertModal <AlertModal
@ -312,7 +319,7 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
/> />
<AlertModal <AlertModal
show={dialogs.oHistory} show={dialogs.oHistory}
text={intl.formatMessage({ id: "dialogs.clear_o_history_confirm" })} text={intl.formatMessage({ id: clearHistoryMessageID })}
confirmButtonText={intl.formatMessage({ id: "actions.clear" })} confirmButtonText={intl.formatMessage({ id: "actions.clear" })}
onConfirm={() => handleClearODates()} onConfirm={() => handleClearODates()}
onCancel={() => setDialogPartial({ oHistory: false })} onCancel={() => setDialogPartial({ oHistory: false })}
@ -351,6 +358,11 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
) as string[]; ) as string[];
const oHistory = (scene.o_history ?? []).filter((h) => h != null) 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 ( return (
<div> <div>
{maybeRenderDialogs()} {maybeRenderDialogs()}
@ -401,7 +413,7 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
<div className="history-header"> <div className="history-header">
<h5> <h5>
<span> <span>
<FormattedMessage id="o_history" /> <FormattedMessage id={oHistoryMessageID} />
<Counter count={oHistory.length} hideZero /> <Counter count={oHistory.length} hideZero />
</span> </span>
<span> <span>
@ -427,7 +439,7 @@ export const SceneHistoryPanel: React.FC<ISceneHistoryProps> = ({ scene }) => {
</div> </div>
<History <History
history={oHistory} history={oHistory}
noneID="odate_recorded_no" noneID={noneMessageID}
unknownDate={scene.created_at} unknownDate={scene.created_at}
onRemove={(t) => handleDeleteODate(t)} onRemove={(t) => handleDeleteODate(t)}
/> />

View file

@ -218,32 +218,38 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
return ( return (
<> <>
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="title"
title={intl.formatMessage({ id: "title" })} title={intl.formatMessage({ id: "title" })}
result={title} result={title}
onChange={(value) => setTitle(value)} onChange={(value) => setTitle(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="code"
title={intl.formatMessage({ id: "scene_code" })} title={intl.formatMessage({ id: "scene_code" })}
result={code} result={code}
onChange={(value) => setCode(value)} onChange={(value) => setCode(value)}
/> />
<ScrapedStringListRow <ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })} title={intl.formatMessage({ id: "urls" })}
result={urls} result={urls}
onChange={(value) => setURLs(value)} onChange={(value) => setURLs(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="date"
title={intl.formatMessage({ id: "date" })} title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
result={date} result={date}
onChange={(value) => setDate(value)} onChange={(value) => setDate(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="director"
title={intl.formatMessage({ id: "director" })} title={intl.formatMessage({ id: "director" })}
result={director} result={director}
onChange={(value) => setDirector(value)} onChange={(value) => setDirector(value)}
/> />
<ScrapedStudioRow <ScrapedStudioRow
field="studio"
title={intl.formatMessage({ id: "studios" })} title={intl.formatMessage({ id: "studios" })}
result={studio} result={studio}
onChange={(value) => setStudio(value)} onChange={(value) => setStudio(value)}
@ -251,6 +257,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
onCreateNew={createNewStudio} onCreateNew={createNewStudio}
/> />
<ScrapedPerformersRow <ScrapedPerformersRow
field="performers"
title={intl.formatMessage({ id: "performers" })} title={intl.formatMessage({ id: "performers" })}
result={performers} result={performers}
onChange={(value) => setPerformers(value)} onChange={(value) => setPerformers(value)}
@ -259,6 +266,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
ageFromDate={date.useNewValue ? date.newValue : date.originalValue} ageFromDate={date.useNewValue ? date.newValue : date.originalValue}
/> />
<ScrapedGroupsRow <ScrapedGroupsRow
field="groups"
title={intl.formatMessage({ id: "groups" })} title={intl.formatMessage({ id: "groups" })}
result={groups} result={groups}
onChange={(value) => setGroups(value)} onChange={(value) => setGroups(value)}
@ -267,17 +275,20 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
/> />
{scrapedTagsRow} {scrapedTagsRow}
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="details"
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}
result={details} result={details}
onChange={(value) => setDetails(value)} onChange={(value) => setDetails(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="stash_ids"
title={intl.formatMessage({ id: "stash_id" })} title={intl.formatMessage({ id: "stash_id" })}
result={stashID} result={stashID}
locked locked
onChange={(value) => setStashID(value)} onChange={(value) => setStashID(value)}
/> />
<ScrapedImageRow <ScrapedImageRow
field="cover_image"
title={intl.formatMessage({ id: "cover_image" })} title={intl.formatMessage({ id: "cover_image" })}
className="scene-cover" className="scene-cover"
result={image} result={image}

View file

@ -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 cloneDeep from "lodash-es/cloneDeep";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
@ -18,7 +18,7 @@ import { ExportDialog } from "../Shared/ExportDialog";
import { SceneCardsGrid } from "./SceneCardsGrid"; import { SceneCardsGrid } from "./SceneCardsGrid";
import { TaggerContext } from "../Tagger/context"; import { TaggerContext } from "../Tagger/context";
import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog"; import { IdentifyDialog } from "../Dialogs/IdentifyDialog/IdentifyDialog";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { import {
faPencil, faPencil,
faPlay, faPlay,
@ -110,7 +110,7 @@ function renderMetadataByline(result: GQL.FindScenesQueryResult) {
function usePlayScene() { function usePlayScene() {
const history = useHistory(); const history = useHistory();
const { configuration: config } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const cont = config?.interface.continuePlaylistDefault ?? false; const cont = config?.interface.continuePlaylistDefault ?? false;
const autoPlay = config?.interface.autostartVideoOnPlaySelected ?? 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 } = const { effectiveFilter, result, cachedResult, items, totalCount } =
queryResult; queryResult;
@ -709,7 +709,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => {
]; ];
// render // render
if (filterLoading || sidebarStateLoading) return null; if (sidebarStateLoading) return null;
const operations = ( const operations = (
<SceneListOperations <SceneListOperations

View file

@ -1,4 +1,4 @@
import React, { useMemo } from "react"; import { useMemo } from "react";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
@ -6,7 +6,7 @@ import { TagLink } from "../Shared/TagLink";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import NavUtils from "src/utils/navigation"; import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text"; 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 { GridCard } from "../Shared/GridCard/GridCard";
import { faTag } from "@fortawesome/free-solid-svg-icons"; import { faTag } from "@fortawesome/free-solid-svg-icons";
import { markerTitle } from "src/core/markers"; import { markerTitle } from "src/core/markers";
@ -109,7 +109,7 @@ const SceneMarkerCardDetails = (props: ISceneMarkerCardProps) => {
}; };
const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => { const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const file = useMemo( const file = useMemo(
() => () =>

View file

@ -1,17 +1,11 @@
import React, { import React, { useCallback, useEffect, useMemo, useState } from "react";
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import Gallery, { import Gallery, {
GalleryI, GalleryI,
PhotoProps, PhotoProps,
RenderImageProps, RenderImageProps,
} from "react-photo-gallery"; } from "react-photo-gallery";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { TruncatedText } from "../Shared/TruncatedText"; import { TruncatedText } from "../Shared/TruncatedText";
@ -46,7 +40,7 @@ interface IExtraProps {
export const MarkerWallItem: React.FC< export const MarkerWallItem: React.FC<
RenderImageProps<IMarkerPhoto> & IExtraProps RenderImageProps<IMarkerPhoto> & IExtraProps
> = (props: RenderImageProps<IMarkerPhoto> & IExtraProps) => { > = (props: RenderImageProps<IMarkerPhoto> & IExtraProps) => {
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const playSound = configuration?.interface.soundOnPreview ?? false; const playSound = configuration?.interface.soundOnPreview ?? false;
const showTitle = configuration?.interface.wallShowTitle ?? false; const showTitle = configuration?.interface.wallShowTitle ?? false;

View file

@ -372,27 +372,32 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
return ( return (
<> <>
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="title"
title={intl.formatMessage({ id: "title" })} title={intl.formatMessage({ id: "title" })}
result={title} result={title}
onChange={(value) => setTitle(value)} onChange={(value) => setTitle(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="code"
title={intl.formatMessage({ id: "scene_code" })} title={intl.formatMessage({ id: "scene_code" })}
result={code} result={code}
onChange={(value) => setCode(value)} onChange={(value) => setCode(value)}
/> />
<ScrapedStringListRow <ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })} title={intl.formatMessage({ id: "urls" })}
result={url} result={url}
onChange={(value) => setURL(value)} onChange={(value) => setURL(value)}
/> />
<ScrapedInputGroupRow <ScrapedInputGroupRow
field="date"
title={intl.formatMessage({ id: "date" })} title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD" placeholder="YYYY-MM-DD"
result={date} result={date}
onChange={(value) => setDate(value)} onChange={(value) => setDate(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="rating"
title={intl.formatMessage({ id: "rating" })} title={intl.formatMessage({ id: "rating" })}
result={rating} result={rating}
renderOriginalField={() => ( renderOriginalField={() => (
@ -404,6 +409,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setRating(value)} onChange={(value) => setRating(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="o_count"
title={intl.formatMessage({ id: "o_count" })} title={intl.formatMessage({ id: "o_count" })}
result={oCounter} result={oCounter}
renderOriginalField={() => ( renderOriginalField={() => (
@ -425,6 +431,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setOCounter(value)} onChange={(value) => setOCounter(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="play_count"
title={intl.formatMessage({ id: "play_count" })} title={intl.formatMessage({ id: "play_count" })}
result={playCount} result={playCount}
renderOriginalField={() => ( renderOriginalField={() => (
@ -446,6 +453,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setPlayCount(value)} onChange={(value) => setPlayCount(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="play_duration"
title={intl.formatMessage({ id: "play_duration" })} title={intl.formatMessage({ id: "play_duration" })}
result={playDuration} result={playDuration}
renderOriginalField={() => ( renderOriginalField={() => (
@ -469,6 +477,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setPlayDuration(value)} onChange={(value) => setPlayDuration(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="galleries"
title={intl.formatMessage({ id: "galleries" })} title={intl.formatMessage({ id: "galleries" })}
result={galleries} result={galleries}
renderOriginalField={() => ( renderOriginalField={() => (
@ -492,32 +501,38 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setGalleries(value)} onChange={(value) => setGalleries(value)}
/> />
<ScrapedStudioRow <ScrapedStudioRow
field="studio"
title={intl.formatMessage({ id: "studios" })} title={intl.formatMessage({ id: "studios" })}
result={studio} result={studio}
onChange={(value) => setStudio(value)} onChange={(value) => setStudio(value)}
/> />
<ScrapedPerformersRow <ScrapedPerformersRow
field="performers"
title={intl.formatMessage({ id: "performers" })} title={intl.formatMessage({ id: "performers" })}
result={performers} result={performers}
onChange={(value) => setPerformers(value)} onChange={(value) => setPerformers(value)}
ageFromDate={date.useNewValue ? date.newValue : date.originalValue} ageFromDate={date.useNewValue ? date.newValue : date.originalValue}
/> />
<ScrapedGroupsRow <ScrapedGroupsRow
field="groups"
title={intl.formatMessage({ id: "groups" })} title={intl.formatMessage({ id: "groups" })}
result={groups} result={groups}
onChange={(value) => setGroups(value)} onChange={(value) => setGroups(value)}
/> />
<ScrapedTagsRow <ScrapedTagsRow
field="tags"
title={intl.formatMessage({ id: "tags" })} title={intl.formatMessage({ id: "tags" })}
result={tags} result={tags}
onChange={(value) => setTags(value)} onChange={(value) => setTags(value)}
/> />
<ScrapedTextAreaRow <ScrapedTextAreaRow
field="details"
title={intl.formatMessage({ id: "details" })} title={intl.formatMessage({ id: "details" })}
result={details} result={details}
onChange={(value) => setDetails(value)} onChange={(value) => setDetails(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="organized"
title={intl.formatMessage({ id: "organized" })} title={intl.formatMessage({ id: "organized" })}
result={organized} result={organized}
renderOriginalField={() => ( renderOriginalField={() => (
@ -539,6 +554,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setOrganized(value)} onChange={(value) => setOrganized(value)}
/> />
<ScrapeDialogRow <ScrapeDialogRow
field="stash_ids"
title={intl.formatMessage({ id: "stash_id" })} title={intl.formatMessage({ id: "stash_id" })}
result={stashIDs} result={stashIDs}
renderOriginalField={() => ( renderOriginalField={() => (
@ -550,6 +566,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={(value) => setStashIDs(value)} onChange={(value) => setStashIDs(value)}
/> />
<ScrapedImageRow <ScrapedImageRow
field="cover_image"
title={intl.formatMessage({ id: "cover_image" })} title={intl.formatMessage({ id: "cover_image" })}
className="scene-cover" className="scene-cover"
result={image} result={image}

View file

@ -12,7 +12,7 @@ import {
queryFindScenesForSelect, queryFindScenesForSelect,
queryFindScenesByIDForSelect, queryFindScenesByIDForSelect,
} from "src/core/StashService"; } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -66,7 +66,7 @@ const sceneSelectSort = PatchFunction(
const _SceneSelect: React.FC< const _SceneSelect: React.FC<
IFilterProps & IFilterValueProps<Scene> & ExtraSceneProps IFilterProps & IFilterValueProps<Scene> & ExtraSceneProps
> = (props) => { > = (props) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const maxOptionsShown = const maxOptionsShown =
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;

View file

@ -1,10 +1,4 @@
import React, { import React, { useCallback, useEffect, useMemo, useState } from "react";
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import Gallery, { import Gallery, {
@ -12,7 +6,7 @@ import Gallery, {
PhotoProps, PhotoProps,
RenderImageProps, RenderImageProps,
} from "react-photo-gallery"; } from "react-photo-gallery";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { Link, useHistory } from "react-router-dom"; import { Link, useHistory } from "react-router-dom";
import { TruncatedText } from "../Shared/TruncatedText"; import { TruncatedText } from "../Shared/TruncatedText";
@ -35,7 +29,7 @@ export const SceneWallItem: React.FC<
> = (props: RenderImageProps<IScenePhoto> & IExtraProps) => { > = (props: RenderImageProps<IScenePhoto> & IExtraProps) => {
const intl = useIntl(); const intl = useIntl();
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const playSound = configuration?.interface.soundOnPreview ?? false; const playSound = configuration?.interface.soundOnPreview ?? false;
const showTitle = configuration?.interface.wallShowTitle ?? false; const showTitle = configuration?.interface.wallShowTitle ?? false;

View file

@ -240,6 +240,14 @@ export const SettingsInterfacePanel: React.FC = PatchComponent(
<option value="zh-CN"> ()</option> <option value="zh-CN"> ()</option>
</SelectSetting> </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-group">
<div className="setting"> <div className="setting">
<div> <div>

View file

@ -22,7 +22,7 @@ import { SettingSection } from "../SettingSection";
import { BooleanSetting, Setting } from "../Inputs"; import { BooleanSetting, Setting } from "../Inputs";
import { ManualLink } from "src/components/Help/context"; import { ManualLink } from "src/components/Help/context";
import { Icon } from "src/components/Shared/Icon"; 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 { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
import { import {
faMinus, faMinus,
@ -44,7 +44,7 @@ const CleanDialog: React.FC<ICleanDialog> = ({
onClose, onClose,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const libraryPaths = configuration?.general.stashes.map((s) => s.path); const libraryPaths = configuration?.general.stashes.map((s) => s.path);

View file

@ -9,7 +9,7 @@ import { useIntl } from "react-intl";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { ModalComponent } from "src/components/Shared/Modal"; import { ModalComponent } from "src/components/Shared/Modal";
import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
interface IDirectorySelectionDialogProps { interface IDirectorySelectionDialogProps {
animation?: boolean; animation?: boolean;
@ -22,7 +22,7 @@ export const DirectorySelectionDialog: React.FC<
IDirectorySelectionDialogProps IDirectorySelectionDialogProps
> = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => { > = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => {
const intl = useIntl(); const intl = useIntl();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const libraryPaths = configuration?.general.stashes.map((s) => s.path); const libraryPaths = configuration?.general.stashes.map((s) => s.path);

View file

@ -7,7 +7,7 @@ import {
mutateMetadataGenerate, mutateMetadataGenerate,
} from "src/core/StashService"; } from "src/core/StashService";
import { withoutTypename } from "src/utils/data"; 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 { IdentifyDialog } from "../../Dialogs/IdentifyDialog/IdentifyDialog";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { DirectorySelectionDialog } from "./DirectorySelectionDialog"; import { DirectorySelectionDialog } from "./DirectorySelectionDialog";
@ -123,7 +123,7 @@ export const LibraryTasks: React.FC = () => {
type DialogOpenState = typeof dialogOpen; type DialogOpenState = typeof dialogOpen;
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const [configRead, setConfigRead] = useState(false); const [configRead, setConfigRead] = useState(false);
useEffect(() => { useEffect(() => {

View file

@ -1,4 +1,4 @@
import React, { useState, useContext, useCallback } from "react"; import React, { useState, useCallback } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { import {
Alert, Alert,
@ -15,7 +15,7 @@ import {
useSystemStatus, useSystemStatus,
} from "src/core/StashService"; } from "src/core/StashService";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import StashConfiguration from "../Settings/StashConfiguration"; import StashConfiguration from "../Settings/StashConfiguration";
import { Icon } from "../Shared/Icon"; import { Icon } from "../Shared/Icon";
import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { LoadingIndicator } from "../Shared/LoadingIndicator";
@ -518,6 +518,10 @@ const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
const [stashes, setStashes] = useState<GQL.StashConfig[]>( const [stashes, setStashes] = useState<GQL.StashConfig[]>(
setupState.stashes ?? [] setupState.stashes ?? []
); );
const [sfwContentMode, setSfwContentMode] = useState(
setupState.sfwContentMode ?? false
);
const [databaseFile, setDatabaseFile] = useState( const [databaseFile, setDatabaseFile] = useState(
setupState.databaseFile ?? "" setupState.databaseFile ?? ""
); );
@ -555,6 +559,7 @@ const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
cacheLocation, cacheLocation,
blobsLocation: storeBlobsInDatabase ? "" : blobsLocation, blobsLocation: storeBlobsInDatabase ? "" : blobsLocation,
storeBlobsInDatabase, storeBlobsInDatabase,
sfwContentMode,
}; };
next(input); next(input);
} }
@ -594,6 +599,22 @@ const SetPathsStep: React.FC<IWizardStep> = ({ goBack, next }) => {
/> />
</Card> </Card>
</Form.Group> </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 : ( {overrideDatabase ? null : (
<DatabaseSection <DatabaseSection
databaseFile={databaseFile} databaseFile={databaseFile}
@ -952,8 +973,7 @@ const FinishStep: React.FC<IWizardStep> = ({ goBack }) => {
export const Setup: React.FC = () => { export const Setup: React.FC = () => {
const intl = useIntl(); const intl = useIntl();
const { configuration, loading: configLoading } = const { configuration } = useConfigurationContext();
useContext(ConfigurationContext);
const [saveUI] = useConfigureUI(); const [saveUI] = useConfigureUI();
@ -1024,7 +1044,7 @@ export const Setup: React.FC = () => {
} }
} }
if (configLoading || statusLoading) { if (statusLoading) {
return <LoadingIndicator />; return <LoadingIndicator />;
} }

View file

@ -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 React from "react";
import { Button, ButtonGroup } from "react-bootstrap"; import { Button, ButtonGroup } from "react-bootstrap";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { SweatDrops } from "./SweatDrops"; import { SweatDrops } from "./SweatDrops";
import cx from "classnames"; import cx from "classnames";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useConfigurationContext } from "src/hooks/Config";
interface ICountButtonProps { interface ICountButtonProps {
value: number; value: number;
@ -63,11 +64,17 @@ export const ViewCountButton: React.FC<CountButtonPropsNoIcon> = (props) => {
export const OCounterButton: React.FC<CountButtonPropsNoIcon> = (props) => { export const OCounterButton: React.FC<CountButtonPropsNoIcon> = (props) => {
const intl = useIntl(); 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 ( return (
<CountButton <CountButton
{...props} {...props}
icon={<SweatDrops />} icon={icon}
title={intl.formatMessage({ id: "o_count" })} title={intl.formatMessage({ id: messageID })}
countTitle={intl.formatMessage({ id: "actions.view_history" })} countTitle={intl.formatMessage({ id: "actions.view_history" })}
/> />
); );

View file

@ -3,6 +3,7 @@ import { FormattedMessage } from "react-intl";
interface IDetailItem { interface IDetailItem {
id?: string | null; id?: string | null;
className?: string;
label?: React.ReactNode; label?: React.ReactNode;
value?: React.ReactNode; value?: React.ReactNode;
labelTitle?: string; labelTitle?: string;
@ -13,6 +14,7 @@ interface IDetailItem {
export const DetailItem: React.FC<IDetailItem> = ({ export const DetailItem: React.FC<IDetailItem> = ({
id, id,
className = "",
label, label,
value, value,
labelTitle, labelTitle,
@ -30,7 +32,7 @@ export const DetailItem: React.FC<IDetailItem> = ({
const sanitisedID = id.replace(/_/g, "-"); const sanitisedID = id.replace(/_/g, "-");
return ( return (
<div className={`detail-item ${id}`}> <div className={`detail-item ${id} ${className}`}>
<span className={`detail-item-title ${sanitisedID}`} title={labelTitle}> <span className={`detail-item-title ${sanitisedID}`} title={labelTitle}>
{message} {message}
{fullWidth ? ":" : ""} {fullWidth ? ":" : ""}

View file

@ -1,6 +1,6 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
interface IStudio { interface IStudio {
id: string; id: string;
@ -11,7 +11,7 @@ interface IStudio {
export const StudioOverlay: React.FC<{ export const StudioOverlay: React.FC<{
studio: IStudio | null | undefined; studio: IStudio | null | undefined;
}> = ({ studio }) => { }> = ({ studio }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const configValue = configuration?.interface.showStudioAsText; const configValue = configuration?.interface.showStudioAsText;

View file

@ -11,14 +11,14 @@ import React from "react";
import { Button, OverlayTrigger, Tooltip } from "react-bootstrap"; import { Button, OverlayTrigger, Tooltip } from "react-bootstrap";
import { FormattedNumber, useIntl } from "react-intl"; import { FormattedNumber, useIntl } from "react-intl";
import { Link } from "react-router-dom"; 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 TextUtils from "src/utils/text";
import { Icon } from "./Icon"; import { Icon } from "./Icon";
export const Count: React.FC<{ export const Count: React.FC<{
count: number; count: number;
}> = ({ count }) => { }> = ({ count }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const abbreviateCounter = configuration?.ui.abbreviateCounters ?? false; const abbreviateCounter = configuration?.ui.abbreviateCounters ?? false;
if (!abbreviateCounter) { if (!abbreviateCounter) {

View file

@ -1,5 +1,4 @@
import React from "react"; import { useConfigurationContext } from "src/hooks/Config";
import { ConfigurationContext } from "src/hooks/Config";
import { import {
defaultRatingStarPrecision, defaultRatingStarPrecision,
defaultRatingSystemOptions, defaultRatingSystemOptions,
@ -23,7 +22,7 @@ export interface IRatingSystemProps {
export const RatingSystem = PatchComponent( export const RatingSystem = PatchComponent(
"RatingSystem", "RatingSystem",
(props: IRatingSystemProps) => { (props: IRatingSystemProps) => {
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const ratingSystemOptions = const ratingSystemOptions =
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;

View file

@ -1,4 +1,4 @@
import React, { useContext } from "react"; import React from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { import {
convertToRatingFormat, convertToRatingFormat,
@ -6,14 +6,14 @@ import {
RatingStarPrecision, RatingStarPrecision,
RatingSystemType, RatingSystemType,
} from "src/utils/rating"; } from "src/utils/rating";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
interface IProps { interface IProps {
rating?: number | null; rating?: number | null;
} }
export const RatingBanner: React.FC<IProps> = ({ rating }) => { export const RatingBanner: React.FC<IProps> = ({ rating }) => {
const { configuration: config } = useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const ratingSystemOptions = const ratingSystemOptions =
config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions;
const isLegacy = const isLegacy =

View file

@ -24,6 +24,7 @@ import { CountrySelect } from "../CountrySelect";
import { StringListInput } from "../StringListInput"; import { StringListInput } from "../StringListInput";
import { ImageSelector } from "../ImageSelector"; import { ImageSelector } from "../ImageSelector";
import { ScrapeResult } from "./scrapeResult"; import { ScrapeResult } from "./scrapeResult";
import { useConfigurationContext } from "src/hooks/Config";
interface IScrapedFieldProps<T> { interface IScrapedFieldProps<T> {
result: ScrapeResult<T>; result: ScrapeResult<T>;
@ -31,6 +32,7 @@ interface IScrapedFieldProps<T> {
interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> { interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> {
className?: string; className?: string;
field: string;
title: string; title: string;
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined; renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
renderNewField: (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 ( 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"> <Form.Label column lg="3">
{props.title} {props.title}
</Form.Label> </Form.Label>
@ -175,6 +180,8 @@ function getNameString(value: string) {
interface IScrapedInputGroupRowProps { interface IScrapedInputGroupRowProps {
title: string; title: string;
field: string;
className?: string;
placeholder?: string; placeholder?: string;
result: ScrapeResult<string>; result: ScrapeResult<string>;
locked?: boolean; locked?: boolean;
@ -187,6 +194,8 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={props.title} title={props.title}
field={props.field}
className={props.className}
result={props.result} result={props.result}
renderOriginalField={() => ( renderOriginalField={() => (
<ScrapedInputGroup <ScrapedInputGroup
@ -240,6 +249,7 @@ const ScrapedStringList: React.FC<IScrapedStringListProps> = (props) => {
interface IScrapedStringListRowProps { interface IScrapedStringListRowProps {
title: string; title: string;
field: string;
placeholder?: string; placeholder?: string;
result: ScrapeResult<string[]>; result: ScrapeResult<string[]>;
locked?: boolean; locked?: boolean;
@ -253,6 +263,7 @@ export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
<ScrapeDialogRow <ScrapeDialogRow
className="string-list-row" className="string-list-row"
title={props.title} title={props.title}
field={props.field}
result={props.result} result={props.result}
renderOriginalField={() => ( renderOriginalField={() => (
<ScrapedStringList <ScrapedStringList
@ -300,6 +311,7 @@ export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={props.title} title={props.title}
field={props.field}
result={props.result} result={props.result}
renderOriginalField={() => ( renderOriginalField={() => (
<ScrapedTextArea <ScrapedTextArea
@ -346,6 +358,7 @@ const ScrapedImage: React.FC<IScrapedImageProps> = (props) => {
interface IScrapedImageRowProps { interface IScrapedImageRowProps {
title: string; title: string;
field: string;
className?: string; className?: string;
result: ScrapeResult<string>; result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void; onChange: (value: ScrapeResult<string>) => void;
@ -355,6 +368,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={props.title} title={props.title}
field={props.field}
result={props.result} result={props.result}
renderOriginalField={() => ( renderOriginalField={() => (
<ScrapedImage <ScrapedImage
@ -379,6 +393,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
interface IScrapedImagesRowProps { interface IScrapedImagesRowProps {
title: string; title: string;
field: string;
className?: string; className?: string;
result: ScrapeResult<string>; result: ScrapeResult<string>;
images: string[]; images: string[];
@ -397,6 +412,7 @@ export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={props.title} title={props.title}
field={props.field}
result={props.result} result={props.result}
renderOriginalField={() => ( renderOriginalField={() => (
<ScrapedImage <ScrapedImage
@ -433,6 +449,9 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
props: IScrapeDialogProps props: IScrapeDialogProps
) => { ) => {
const intl = useIntl(); const intl = useIntl();
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
return ( return (
<ModalComponent <ModalComponent
show show
@ -449,7 +468,10 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
text: intl.formatMessage({ id: "actions.cancel" }), text: intl.formatMessage({ id: "actions.cancel" }),
variant: "secondary", variant: "secondary",
}} }}
modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }} modalProps={{
size: "lg",
dialogClassName: `scrape-dialog ${sfwContentMode ? "sfw-mode" : ""}`,
}}
> >
<div className="dialog-container"> <div className="dialog-container">
<Form> <Form>
@ -479,6 +501,7 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
interface IScrapedCountryRowProps { interface IScrapedCountryRowProps {
title: string; title: string;
field: string;
result: ScrapeResult<string>; result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void; onChange: (value: ScrapeResult<string>) => void;
locked?: boolean; locked?: boolean;
@ -487,6 +510,7 @@ interface IScrapedCountryRowProps {
export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
title, title,
field,
result, result,
onChange, onChange,
locked, locked,
@ -494,6 +518,7 @@ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
}) => ( }) => (
<ScrapeDialogRow <ScrapeDialogRow
title={title} title={title}
field={field}
result={result} result={result}
renderOriginalField={() => ( renderOriginalField={() => (
<FormControl <FormControl

View file

@ -13,6 +13,7 @@ import { uniq } from "lodash-es";
interface IScrapedStudioRow { interface IScrapedStudioRow {
title: string; title: string;
field: string;
result: ObjectScrapeResult<GQL.ScrapedStudio>; result: ObjectScrapeResult<GQL.ScrapedStudio>;
onChange: (value: ObjectScrapeResult<GQL.ScrapedStudio>) => void; onChange: (value: ObjectScrapeResult<GQL.ScrapedStudio>) => void;
newStudio?: GQL.ScrapedStudio; newStudio?: GQL.ScrapedStudio;
@ -25,6 +26,7 @@ function getObjectName<T extends { name: string }>(value: T) {
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
title, title,
field,
result, result,
onChange, onChange,
newStudio, newStudio,
@ -73,6 +75,7 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={title} title={title}
field={field}
result={result} result={result}
renderOriginalField={() => renderScrapedStudio(result)} renderOriginalField={() => renderScrapedStudio(result)}
renderNewField={() => renderNewField={() =>
@ -92,6 +95,7 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
interface IScrapedObjectsRow<T> { interface IScrapedObjectsRow<T> {
title: string; title: string;
field: string;
result: ScrapeResult<T[]>; result: ScrapeResult<T[]>;
onChange: (value: ScrapeResult<T[]>) => void; onChange: (value: ScrapeResult<T[]>) => void;
newObjects?: T[]; newObjects?: T[];
@ -107,6 +111,7 @@ interface IScrapedObjectsRow<T> {
export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => { export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
const { const {
title, title,
field,
result, result,
onChange, onChange,
newObjects, newObjects,
@ -118,6 +123,7 @@ export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
return ( return (
<ScrapeDialogRow <ScrapeDialogRow
title={title} title={title}
field={field}
result={result} result={result}
renderOriginalField={() => renderObjects(result)} renderOriginalField={() => renderObjects(result)}
renderNewField={() => renderNewField={() =>
@ -142,7 +148,15 @@ type IScrapedObjectRowImpl<T> = Omit<
export const ScrapedPerformersRow: React.FC< export const ScrapedPerformersRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedPerformer> & { ageFromDate?: string | null } IScrapedObjectRowImpl<GQL.ScrapedPerformer> & { ageFromDate?: string | null }
> = ({ title, result, onChange, newObjects, onCreateNew, ageFromDate }) => { > = ({
title,
field,
result,
onChange,
newObjects,
onCreateNew,
ageFromDate,
}) => {
const performersCopy = useMemo(() => { const performersCopy = useMemo(() => {
return ( return (
newObjects?.map((p) => { newObjects?.map((p) => {
@ -191,6 +205,7 @@ export const ScrapedPerformersRow: React.FC<
return ( return (
<ScrapedObjectsRow<GQL.ScrapedPerformer> <ScrapedObjectsRow<GQL.ScrapedPerformer>
title={title} title={title}
field={field}
result={result} result={result}
renderObjects={renderScrapedPerformers} renderObjects={renderScrapedPerformers}
onChange={onChange} onChange={onChange}
@ -203,7 +218,7 @@ export const ScrapedPerformersRow: React.FC<
export const ScrapedGroupsRow: React.FC< export const ScrapedGroupsRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedGroup> IScrapedObjectRowImpl<GQL.ScrapedGroup>
> = ({ title, result, onChange, newObjects, onCreateNew }) => { > = ({ title, field, result, onChange, newObjects, onCreateNew }) => {
const groupsCopy = useMemo(() => { const groupsCopy = useMemo(() => {
return ( return (
newObjects?.map((p) => { newObjects?.map((p) => {
@ -251,6 +266,7 @@ export const ScrapedGroupsRow: React.FC<
return ( return (
<ScrapedObjectsRow<GQL.ScrapedGroup> <ScrapedObjectsRow<GQL.ScrapedGroup>
title={title} title={title}
field={field}
result={result} result={result}
renderObjects={renderScrapedGroups} renderObjects={renderScrapedGroups}
onChange={onChange} onChange={onChange}
@ -263,7 +279,7 @@ export const ScrapedGroupsRow: React.FC<
export const ScrapedTagsRow: React.FC< export const ScrapedTagsRow: React.FC<
IScrapedObjectRowImpl<GQL.ScrapedTag> IScrapedObjectRowImpl<GQL.ScrapedTag>
> = ({ title, result, onChange, newObjects, onCreateNew }) => { > = ({ title, field, result, onChange, newObjects, onCreateNew }) => {
function renderScrapedTags( function renderScrapedTags(
scrapeResult: ScrapeResult<GQL.ScrapedTag[]>, scrapeResult: ScrapeResult<GQL.ScrapedTag[]>,
isNew?: boolean, isNew?: boolean,
@ -297,6 +313,7 @@ export const ScrapedTagsRow: React.FC<
return ( return (
<ScrapedObjectsRow<GQL.ScrapedTag> <ScrapedObjectsRow<GQL.ScrapedTag>
title={title} title={title}
field={field}
result={result} result={result}
renderObjects={renderScrapedTags} renderObjects={renderScrapedTags}
onChange={onChange} onChange={onChange}

View file

@ -39,6 +39,7 @@ export function useScrapedTags(
const scrapedTagsRow = ( const scrapedTagsRow = (
<ScrapedTagsRow <ScrapedTagsRow
field="tags"
title={intl.formatMessage({ id: "tags" })} title={intl.formatMessage({ id: "tags" })}
result={tags} result={tags}
onChange={(value) => setTags(value)} onChange={(value) => setTags(value)}

View file

@ -15,7 +15,7 @@ import CreatableSelect from "react-select/creatable";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useMarkerStrings } from "src/core/StashService"; import { useMarkerStrings } from "src/core/StashService";
import { SelectComponents } from "react-select/dist/declarations/src/components"; 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 { objectTitle } from "src/core/files";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
import { useDebounce } from "src/hooks/debounce"; import { useDebounce } from "src/hooks/debounce";
@ -108,7 +108,7 @@ const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
const LimitedSelectMenu = <T extends boolean>( const LimitedSelectMenu = <T extends boolean>(
props: MenuListProps<Option, T, GroupBase<Option>> props: MenuListProps<Option, T, GroupBase<Option>>
) => { ) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const maxOptionsShown = const maxOptionsShown =
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
@ -496,6 +496,7 @@ export const ListSelect = <T extends {}>(props: IListSelect<T>) => {
type DisableOption = Option & { type DisableOption = Option & {
isDisabled?: boolean; isDisabled?: boolean;
className?: string;
}; };
interface ICheckBoxSelectProps { interface ICheckBoxSelectProps {
@ -510,7 +511,17 @@ export const CheckBoxSelect: React.FC<ICheckBoxSelectProps> = ({
onChange, onChange,
}) => { }) => {
const Option = (props: OptionProps<DisableOption, true>) => ( 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 <input
type="checkbox" type="checkbox"
disabled={props.isDisabled} disabled={props.isDisabled}

View file

@ -1,6 +1,6 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { StashId } from "src/core/generated-graphql"; 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 { getStashboxBase } from "src/utils/stashbox";
import { ExternalLink } from "./ExternalLink"; import { ExternalLink } from "./ExternalLink";
@ -10,7 +10,7 @@ export const StashIDPill: React.FC<{
stashID: Pick<StashId, "endpoint" | "stash_id">; stashID: Pick<StashId, "endpoint" | "stash_id">;
linkType: LinkType; linkType: LinkType;
}> = ({ stashID, linkType }) => { }> = ({ stashID, linkType }) => {
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const { endpoint, stash_id } = stashID; const { endpoint, stash_id } = stashID;

View file

@ -18,7 +18,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { ErrorMessage } from "src/components/Shared/ErrorMessage";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioScenesPanel } from "./StudioScenesPanel";
import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
import { StudioImagesPanel } from "./StudioImagesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel";
@ -264,7 +264,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
const intl = useIntl(); const intl = useIntl();
// Configuration settings // Configuration settings
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false;

View file

@ -13,7 +13,7 @@ import {
queryFindStudiosByIDForSelect, queryFindStudiosByIDForSelect,
queryFindStudiosForSelect, queryFindStudiosForSelect,
} from "src/core/StashService"; } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defaultMaxOptionsShown, IUIConfig } from "src/core/config"; import { defaultMaxOptionsShown, IUIConfig } from "src/core/config";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -65,7 +65,7 @@ const _StudioSelect: React.FC<
> = (props) => { > = (props) => {
const [createStudio] = useStudioCreate(); const [createStudio] = useStudioCreate();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const maxOptionsShown = const maxOptionsShown =
(configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown; (configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown;

View file

@ -1,10 +1,10 @@
import { useCallback, useContext } from "react"; import { useCallback } from "react";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { initialConfig, ITaggerConfig } from "./constants"; import { initialConfig, ITaggerConfig } from "./constants";
import { useConfigureUISetting } from "src/core/StashService"; import { useConfigureUISetting } from "src/core/StashService";
export function useTaggerConfig() { export function useTaggerConfig() {
const { configuration: stashConfig } = useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const [saveUISetting] = useConfigureUISetting(); const [saveUISetting] = useConfigureUISetting();
const config = stashConfig?.ui.taggerConfig ?? initialConfig; const config = stashConfig?.ui.taggerConfig ?? initialConfig;

View file

@ -17,7 +17,7 @@ import {
useTagCreate, useTagCreate,
} from "src/core/StashService"; } from "src/core/StashService";
import { useToast } from "src/hooks/Toast"; 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 { ITaggerSource, SCRAPER_PREFIX, STASH_BOX_PREFIX } from "./constants";
import { errorToString } from "src/utils"; import { errorToString } from "src/utils";
import { mergeStudioStashIDs } from "./utils"; import { mergeStudioStashIDs } from "./utils";
@ -117,7 +117,7 @@ export const TaggerContext: React.FC = ({ children }) => {
const stopping = useRef(false); const stopping = useRef(false);
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const { config, setConfig } = useTaggerConfig(); const { config, setConfig } = useTaggerConfig();
const Scrapers = useListSceneScrapers(); const Scrapers = useListSceneScrapers();

View file

@ -1,7 +1,7 @@
import React, { Dispatch, useState } from "react"; import React, { Dispatch, useState } from "react";
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { ITaggerConfig } from "../constants"; import { ITaggerConfig } from "../constants";
import PerformerFieldSelector from "../PerformerFieldSelector"; import PerformerFieldSelector from "../PerformerFieldSelector";
@ -13,7 +13,7 @@ interface IConfigProps {
} }
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => { const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const [showExclusionModal, setShowExclusionModal] = useState(false); const [showExclusionModal, setShowExclusionModal] = useState(false);
const excludedFields = config.excludedPerformerFields ?? []; const excludedFields = config.excludedPerformerFields ?? [];

View file

@ -16,7 +16,7 @@ import {
performerMutationImpactedQueries, performerMutationImpactedQueries,
} from "src/core/StashService"; } from "src/core/StashService";
import { Manual } from "src/components/Help/Manual"; import { Manual } from "src/components/Help/Manual";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import StashSearchResult from "./StashSearchResult"; import StashSearchResult from "./StashSearchResult";
import PerformerConfig from "./Config"; import PerformerConfig from "./Config";
@ -620,7 +620,7 @@ interface ITaggerProps {
export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => { export const PerformerTagger: React.FC<ITaggerProps> = ({ performers }) => {
const jobsSubscribe = useJobsSubscribe(); const jobsSubscribe = useJobsSubscribe();
const intl = useIntl(); const intl = useIntl();
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const { config, setConfig } = useTaggerConfig(); const { config, setConfig } = useTaggerConfig();
const [showConfig, setShowConfig] = useState(false); const [showConfig, setShowConfig] = useState(false);
const [showManual, setShowManual] = useState(false); const [showManual, setShowManual] = useState(false);

View file

@ -12,7 +12,7 @@ import Config from "./Config";
import { TaggerScene } from "./TaggerScene"; import { TaggerScene } from "./TaggerScene";
import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneTaggerModals } from "./sceneTaggerModals";
import { SceneSearchResults } from "./StashSearchResult"; 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 { faCog } from "@fortawesome/free-solid-svg-icons";
import { useLightbox } from "src/hooks/Lightbox/hooks"; import { useLightbox } from "src/hooks/Lightbox/hooks";
@ -26,7 +26,7 @@ const Scene: React.FC<{
const intl = useIntl(); const intl = useIntl();
const { currentSource, doSceneQuery, doSceneFragmentScrape, loading } = const { currentSource, doSceneQuery, doSceneFragmentScrape, loading } =
useContext(TaggerStateContext); useContext(TaggerStateContext);
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const cont = configuration?.interface.continuePlaylistDefault ?? false; const cont = configuration?.interface.continuePlaylistDefault ?? false;

View file

@ -19,7 +19,7 @@ import {
faImage, faImage,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { objectPath, objectTitle } from "src/core/files"; 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"; import { SceneQueue } from "src/models/sceneQueue";
interface ITaggerSceneDetails { interface ITaggerSceneDetails {
@ -154,7 +154,7 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
const history = useHistory(); const history = useHistory();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const cont = configuration?.interface.continuePlaylistDefault ?? false; const cont = configuration?.interface.continuePlaylistDefault ?? false;
async function query() { async function query() {

View file

@ -1,7 +1,7 @@
import React, { Dispatch, useState } from "react"; import React, { Dispatch, useState } from "react";
import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { ITaggerConfig } from "../constants"; import { ITaggerConfig } from "../constants";
import StudioFieldSelector from "./StudioFieldSelector"; import StudioFieldSelector from "./StudioFieldSelector";
@ -13,7 +13,7 @@ interface IConfigProps {
} }
const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => { const Config: React.FC<IConfigProps> = ({ show, config, setConfig }) => {
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const [showExclusionModal, setShowExclusionModal] = useState(false); const [showExclusionModal, setShowExclusionModal] = useState(false);
const excludedFields = config.excludedStudioFields ?? []; const excludedFields = config.excludedStudioFields ?? [];

View file

@ -17,7 +17,7 @@ import {
evictQueries, evictQueries,
} from "src/core/StashService"; } from "src/core/StashService";
import { Manual } from "src/components/Help/Manual"; import { Manual } from "src/components/Help/Manual";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import StashSearchResult from "./StashSearchResult"; import StashSearchResult from "./StashSearchResult";
import StudioConfig from "./Config"; import StudioConfig from "./Config";
@ -669,7 +669,7 @@ interface ITaggerProps {
export const StudioTagger: React.FC<ITaggerProps> = ({ studios }) => { export const StudioTagger: React.FC<ITaggerProps> = ({ studios }) => {
const jobsSubscribe = useJobsSubscribe(); const jobsSubscribe = useJobsSubscribe();
const intl = useIntl(); const intl = useIntl();
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const { config, setConfig } = useTaggerConfig(); const { config, setConfig } = useTaggerConfig();
const [showConfig, setShowConfig] = useState(false); const [showConfig, setShowConfig] = useState(false);
const [showManual, setShowManual] = useState(false); const [showManual, setShowManual] = useState(false);

View file

@ -19,7 +19,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { Icon } from "src/components/Shared/Icon"; import { Icon } from "src/components/Shared/Icon";
import { useToast } from "src/hooks/Toast"; 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 { tagRelationHook } from "src/core/tags";
import { TagScenesPanel } from "./TagScenesPanel"; import { TagScenesPanel } from "./TagScenesPanel";
import { TagMarkersPanel } from "./TagMarkersPanel"; import { TagMarkersPanel } from "./TagMarkersPanel";
@ -293,7 +293,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
const intl = useIntl(); const intl = useIntl();
// Configuration settings // Configuration settings
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const uiConfig = configuration?.ui; const uiConfig = configuration?.ui;
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false; const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false;

View file

@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator";
import { HoverPopover } from "../Shared/HoverPopover"; import { HoverPopover } from "../Shared/HoverPopover";
import { useFindTag } from "../../core/StashService"; import { useFindTag } from "../../core/StashService";
import { TagCard } from "./TagCard"; import { TagCard } from "./TagCard";
import { ConfigurationContext } from "../../hooks/Config"; import { useConfigurationContext } from "../../hooks/Config";
import { Placement } from "react-bootstrap/esm/Overlay"; import { Placement } from "react-bootstrap/esm/Overlay";
interface ITagPopoverCardProps { interface ITagPopoverCardProps {
@ -47,7 +47,7 @@ export const TagPopover: React.FC<ITagPopoverProps> = ({
placement = "top", placement = "top",
target, target,
}) => { }) => {
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const showTagCardOnHover = config?.ui.showTagCardOnHover ?? true; const showTagCardOnHover = config?.ui.showTagCardOnHover ?? true;

View file

@ -13,7 +13,7 @@ import {
queryFindTagsByIDForSelect, queryFindTagsByIDForSelect,
queryFindTagsForSelect, queryFindTagsForSelect,
} from "src/core/StashService"; } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { defaultMaxOptionsShown } from "src/core/config"; import { defaultMaxOptionsShown } from "src/core/config";
import { ListFilterModel } from "src/models/list-filter/filter"; import { ListFilterModel } from "src/models/list-filter/filter";
@ -67,7 +67,7 @@ export type TagSelectProps = IFilterProps &
const _TagSelect: React.FC<TagSelectProps> = (props) => { const _TagSelect: React.FC<TagSelectProps> = (props) => {
const [createTag] = useTagCreate(); const [createTag] = useTagCreate();
const { configuration } = React.useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const intl = useIntl(); const intl = useIntl();
const maxOptionsShown = const maxOptionsShown =
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;

View file

@ -12,7 +12,7 @@ import TextUtils from "src/utils/text";
import NavUtils from "src/utils/navigation"; import NavUtils from "src/utils/navigation";
import cx from "classnames"; import cx from "classnames";
import { SceneQueue } from "src/models/sceneQueue"; import { SceneQueue } from "src/models/sceneQueue";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { markerTitle } from "src/core/markers"; import { markerTitle } from "src/core/markers";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
@ -128,7 +128,7 @@ export const WallItem = <T extends WallItemType>({
}: IWallItemProps<T>) => { }: IWallItemProps<T>) => {
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
const itemEl = useRef<HTMLDivElement>(null); const itemEl = useRef<HTMLDivElement>(null);
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const showTextContainer = config?.interface.wallShowTitle ?? true; const showTextContainer = config?.interface.wallShowTitle ?? true;

View file

@ -4,6 +4,15 @@
Setting the language affects the formatting of numbers and dates. 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 ## 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. The Scene Wall and Marker pages display scene preview videos (mp4) by default. This can be changed to animated image (webp) or static image.

View file

@ -2,14 +2,28 @@ import React from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
export interface IContext { export interface IContext {
configuration?: GQL.ConfigDataFragment; configuration: GQL.ConfigDataFragment;
loading?: boolean;
} }
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> = ({ export const ConfigurationProvider: React.FC<IContext> = ({
loading,
configuration, configuration,
children, children,
}) => { }) => {
@ -17,7 +31,6 @@ export const ConfigurationProvider: React.FC<IContext> = ({
<ConfigurationContext.Provider <ConfigurationContext.Provider
value={{ value={{
configuration, configuration,
loading,
}} }}
> >
{children} {children}

View file

@ -1,5 +1,5 @@
import React, { useCallback, useContext, useEffect, useState } from "react"; import React, { useCallback, useContext, useEffect, useState } from "react";
import { ConfigurationContext } from "../Config"; import { useConfigurationContext } from "../Config";
import { useLocalForage } from "../LocalForage"; import { useLocalForage } from "../LocalForage";
import { Interactive as InteractiveAPI } from "./interactive"; import { Interactive as InteractiveAPI } from "./interactive";
import InteractiveUtils, { import InteractiveUtils, {
@ -86,7 +86,7 @@ export const InteractiveProvider: React.FC = ({ children }) => {
{ serverOffset: 0, lastSyncTime: 0 } { serverOffset: 0, lastSyncTime: 0 }
); );
const { configuration: stashConfig } = React.useContext(ConfigurationContext); const { configuration: stashConfig } = useConfigurationContext();
const [state, setState] = useState<ConnectionState>(ConnectionState.Missing); const [state, setState] = useState<ConnectionState>(ConnectionState.Missing);
const [handyKey, setHandyKey] = useState<string | undefined>(undefined); const [handyKey, setHandyKey] = useState<string | undefined>(undefined);

View file

@ -19,7 +19,7 @@ import usePageVisibility from "../PageVisibility";
import { useToast } from "../Toast"; import { useToast } from "../Toast";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { LightboxImage } from "./LightboxImage"; import { LightboxImage } from "./LightboxImage";
import { ConfigurationContext } from "../Config"; import { useConfigurationContext } from "../Config";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton"; import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
import { import {
@ -154,7 +154,7 @@ export const LightboxComponent: React.FC<IProps> = ({
const Toast = useToast(); const Toast = useToast();
const intl = useIntl(); const intl = useIntl();
const { configuration: config } = React.useContext(ConfigurationContext); const { configuration: config } = useConfigurationContext();
const [interfaceLocalForage, setInterfaceLocalForage] = const [interfaceLocalForage, setInterfaceLocalForage] =
useInterfaceLocalForage(); useInterfaceLocalForage();

View file

@ -1,6 +1,5 @@
import { useContext } from "react";
import { useConfigureUI } from "src/core/StashService"; import { useConfigureUI } from "src/core/StashService";
import { ConfigurationContext } from "src/hooks/Config"; import { useConfigurationContext } from "src/hooks/Config";
import { useToast } from "./Toast"; import { useToast } from "./Toast";
export const useTableColumns = ( export const useTableColumns = (
@ -9,7 +8,7 @@ export const useTableColumns = (
) => { ) => {
const Toast = useToast(); const Toast = useToast();
const { configuration } = useContext(ConfigurationContext); const { configuration } = useConfigurationContext();
const [saveUI] = useConfigureUI(); const [saveUI] = useConfigureUI();
const ui = configuration?.ui; const ui = configuration?.ui;

View file

@ -10,6 +10,7 @@ $sidebar-width: 250px;
@import "styles/theme"; @import "styles/theme";
@import "styles/range"; @import "styles/range";
@import "styles/scrollbars"; @import "styles/scrollbars";
@import "sfw-mode.scss";
@import "src/components/Changelog/styles.scss"; @import "src/components/Changelog/styles.scss";
@import "src/components/Galleries/styles.scss"; @import "src/components/Galleries/styles.scss";
@import "src/components/Help/styles.scss"; @import "src/components/Help/styles.scss";

View file

@ -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.", "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" "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": { "show_tag_card_on_hover": {
"description": "Show tag card when hovering tag badges", "description": "Show tag card when hovering tag badges",
"heading": "Tag card tooltips" "heading": "Tag card tooltips"
@ -897,6 +901,7 @@
"developmentVersion": "Development Version", "developmentVersion": "Development Version",
"dialogs": { "dialogs": {
"clear_o_history_confirm": "Are you sure you want to clear the O history?", "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?", "clear_play_history_confirm": "Are you sure you want to clear the play history?",
"create_new_entity": "Create new {entity}", "create_new_entity": "Create new {entity}",
"delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:", "delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:",
@ -1158,6 +1163,7 @@
"interactive_speed": "Interactive Speed", "interactive_speed": "Interactive Speed",
"isMissing": "Is Missing", "isMissing": "Is Missing",
"last_o_at": "Last O At", "last_o_at": "Last O At",
"last_o_at_sfw": "Last Like At",
"last_played_at": "Last Played At", "last_played_at": "Last Played At",
"library": "Library", "library": "Library",
"loading": { "loading": {
@ -1198,9 +1204,11 @@
"new": "New", "new": "New",
"none": "None", "none": "None",
"o_count": "O Count", "o_count": "O Count",
"o_counter": "O-Counter", "o_count_sfw": "Likes",
"o_history": "O History", "o_history": "O History",
"o_history_sfw": "Like History",
"odate_recorded_no": "No O Date Recorded", "odate_recorded_no": "No O Date Recorded",
"odate_recorded_no_sfw": "No Like Date Recorded",
"operations": "Operations", "operations": "Operations",
"organized": "Organised", "organized": "Organised",
"orientation": "Orientation", "orientation": "Orientation",
@ -1377,25 +1385,28 @@
}, },
"paths": { "paths": {
"database_filename_empty_for_default": "database filename (empty for default)", "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_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_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)", "path_to_generated_directory_empty_for_default": "path to generated directory (empty for default)",
"set_up_your_paths": "Set up your paths", "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?", "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", "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": "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": "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_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": "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_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": "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_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": "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_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": "Where is your content 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_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", "stash_setup_wizard": "Stash Setup Wizard",
"success": { "success": {

View file

@ -78,7 +78,7 @@ export abstract class Criterion {
protected cloneValues() {} protected cloneValues() {}
public abstract getLabel(intl: IntlShape): string; public abstract getLabel(intl: IntlShape, sfwContentMode?: boolean): string;
public getId(): string { public getId(): string {
return `${this.criterionOption.type}`; 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( const modifierString = ModifierCriterion.getModifierLabel(
intl, intl,
this.modifier this.modifier
@ -162,10 +162,14 @@ export abstract class ModifierCriterion<
valueString = this.getLabelValue(intl); valueString = this.getLabelValue(intl);
} }
const messageID = !sfwContentMode
? this.criterionOption.messageID
: this.criterionOption.sfwMessageID ?? this.criterionOption.messageID;
return intl.formatMessage( return intl.formatMessage(
{ id: "criterion_modifier.format_string" }, { id: "criterion_modifier.format_string" },
{ {
criterion: intl.formatMessage({ id: this.criterionOption.messageID }), criterion: intl.formatMessage({ id: messageID }),
modifierString, modifierString,
valueString, valueString,
} }
@ -257,12 +261,14 @@ interface ICriterionOptionParams {
type: CriterionType; type: CriterionType;
makeCriterion: MakeCriterionFn; makeCriterion: MakeCriterionFn;
hidden?: boolean; hidden?: boolean;
sfwMessageID?: string;
} }
export class CriterionOption { export class CriterionOption {
public readonly type: CriterionType; public readonly type: CriterionType;
public readonly messageID: string; public readonly messageID: string;
public readonly makeCriterionFn: MakeCriterionFn; public readonly makeCriterionFn: MakeCriterionFn;
public readonly sfwMessageID?: string;
// used for legacy criteria that are not shown in the UI // used for legacy criteria that are not shown in the UI
public readonly hidden: boolean = false; public readonly hidden: boolean = false;
@ -272,6 +278,7 @@ export class CriterionOption {
this.messageID = options.messageID; this.messageID = options.messageID;
this.makeCriterionFn = options.makeCriterion; this.makeCriterionFn = options.makeCriterion;
this.hidden = options.hidden ?? false; this.hidden = options.hidden ?? false;
this.sfwMessageID = options.sfwMessageID;
} }
public makeCriterion(config?: ConfigDataFragment) { 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 id = "criterion_modifier.format_string";
let modifierString = ModifierCriterion.getModifierLabel( let modifierString = ModifierCriterion.getModifierLabel(
intl, 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( return intl.formatMessage(
{ id }, { id },
{ {
criterion: intl.formatMessage({ id: this.criterionOption.messageID }), criterion: intl.formatMessage({ id: messageID }),
modifierString, modifierString,
valueString, valueString,
excludedString, excludedString,
@ -552,9 +563,14 @@ export class StringCriterionOption extends ModifierCriterionOption {
export function createStringCriterionOption( export function createStringCriterionOption(
type: CriterionType, 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 { export class MandatoryStringCriterionOption extends ModifierCriterionOption {
@ -755,7 +771,8 @@ export class MandatoryNumberCriterionOption extends ModifierCriterionOption {
constructor( constructor(
messageID: string, messageID: string,
value: CriterionType, value: CriterionType,
makeCriterion?: () => ModifierCriterion<CriterionValue> makeCriterion?: () => ModifierCriterion<CriterionValue>,
options?: { sfwMessageID?: string }
) { ) {
super({ super({
messageID, messageID,
@ -773,15 +790,22 @@ export class MandatoryNumberCriterionOption extends ModifierCriterionOption {
makeCriterion: makeCriterion makeCriterion: makeCriterion
? makeCriterion ? makeCriterion
: () => new NumberCriterion(this), : () => new NumberCriterion(this),
...options,
}); });
} }
} }
export function createMandatoryNumberCriterionOption( export function createMandatoryNumberCriterionOption(
value: CriterionType, 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>( export function encodeRangeValue<V>(

View file

@ -4,6 +4,7 @@ import { DisplayMode } from "./types";
export interface ISortByOption { export interface ISortByOption {
messageID: string; messageID: string;
value: string; value: string;
sfwMessageID?: string;
} }
export const MediaSortByOptions = [ export const MediaSortByOptions = [
@ -22,7 +23,7 @@ export class ListFilterOptions {
public readonly displayModeOptions: DisplayMode[] = []; public readonly displayModeOptions: DisplayMode[] = [];
public readonly criterionOptions: CriterionOption[] = []; public readonly criterionOptions: CriterionOption[] = [];
public static createSortBy(value: string) { public static createSortBy(value: string): ISortByOption {
return { return {
messageID: value, messageID: value,
value, value,

View file

@ -38,6 +38,7 @@ const sortByOptions = [
{ {
messageID: "o_count", messageID: "o_count",
value: "o_counter", value: "o_counter",
sfwMessageID: "o_count_sfw",
}, },
]); ]);
const displayModeOptions = [DisplayMode.Grid]; const displayModeOptions = [DisplayMode.Grid];
@ -53,7 +54,9 @@ const criterionOptions = [
RatingCriterionOption, RatingCriterionOption,
PerformersCriterionOption, PerformersCriterionOption,
createDateCriterionOption("date"), createDateCriterionOption("date"),
createMandatoryNumberCriterionOption("o_counter", "o_count"), createMandatoryNumberCriterionOption("o_counter", "o_count", {
sfwMessageID: "o_count_sfw",
}),
ContainingGroupsCriterionOption, ContainingGroupsCriterionOption,
SubGroupsCriterionOption, SubGroupsCriterionOption,
createMandatoryNumberCriterionOption("containing_group_count"), createMandatoryNumberCriterionOption("containing_group_count"),

Some files were not shown because too many files have changed in this diff Show more