diff --git a/README.md b/README.md index c54d94528..e47363395 100644 --- a/README.md +++ b/README.md @@ -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 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) * 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. diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 4d6d2080b..63ce3ea1c 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -2,6 +2,8 @@ input SetupInput { "Empty to indicate $HOME/.stash/config.yml default" configLocation: String! stashes: [StashConfigInput!]! + "True if SFW content mode is enabled" + sfwContentMode: Boolean "Empty to indicate default" databaseFile: String! "Empty to indicate default" @@ -341,6 +343,9 @@ type ConfigImageLightboxResult { } input ConfigInterfaceInput { + "True if SFW content mode is enabled" + sfwContentMode: Boolean + "Ordered list of items that should be shown in the menu" menuItems: [String!] @@ -407,6 +412,9 @@ type ConfigDisableDropdownCreate { } type ConfigInterfaceResult { + "True if SFW content mode is enabled" + sfwContentMode: Boolean! + "Ordered list of items that should be shown in the menu" menuItems: [String!] diff --git a/internal/api/images.go b/internal/api/images.go index 89a8e87b0..9e16fc0df 100644 --- a/internal/api/images.go +++ b/internal/api/images.go @@ -101,7 +101,7 @@ func initCustomPerformerImages(customPath string) { } } -func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte { +func getDefaultPerformerImage(name string, gender *models.GenderEnum, sfwMode bool) []byte { // try the custom box first if we have one if performerBoxCustom != nil { ret, err := performerBoxCustom.GetRandomImageByName(name) @@ -111,6 +111,10 @@ func getDefaultPerformerImage(name string, gender *models.GenderEnum) []byte { logger.Warnf("error loading custom default performer image: %v", err) } + if sfwMode { + return static.ReadAll(static.DefaultSFWPerformerImage) + } + var g models.GenderEnum if gender != nil { g = *gender diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index d9c71b09f..ba46a115a 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -445,6 +445,8 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigInterfaceInput) (*ConfigInterfaceResult, error) { c := config.GetInstance() + r.setConfigBool(config.SFWContentMode, input.SfwContentMode) + if input.MenuItems != nil { c.SetInterface(config.MenuItems, input.MenuItems) } diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index cfa22720b..5952dd41e 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -162,6 +162,7 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { disableDropdownCreate := config.GetDisableDropdownCreate() return &ConfigInterfaceResult{ + SfwContentMode: config.GetSFWContentMode(), MenuItems: menuItems, SoundOnPreview: &soundOnPreview, WallShowTitle: &wallShowTitle, diff --git a/internal/api/routes_performer.go b/internal/api/routes_performer.go index b27fdbd6c..8d5463d63 100644 --- a/internal/api/routes_performer.go +++ b/internal/api/routes_performer.go @@ -18,9 +18,14 @@ type PerformerFinder interface { GetImage(ctx context.Context, performerID int) ([]byte, error) } +type sfwConfig interface { + GetSFWContentMode() bool +} + type performerRoutes struct { routes performerFinder PerformerFinder + sfwConfig sfwConfig } func (rs performerRoutes) Routes() chi.Router { @@ -54,7 +59,7 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) { } if len(image) == 0 { - image = getDefaultPerformerImage(performer.Name, performer.Gender) + image = getDefaultPerformerImage(performer.Name, performer.Gender, rs.sfwConfig.GetSFWContentMode()) } utils.ServeImage(w, r, image) diff --git a/internal/api/server.go b/internal/api/server.go index 5059e9a2a..9290c6512 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -322,6 +322,7 @@ func (s *Server) getPerformerRoutes() chi.Router { return performerRoutes{ routes: routes{txnManager: repo.TxnManager}, performerFinder: repo.Performer, + sfwConfig: s.manager.Config, }.Routes() } diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 65e1bad51..a351cc872 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -43,6 +43,9 @@ const ( Password = "password" MaxSessionAge = "max_session_age" + // SFWContentMode mode config key + SFWContentMode = "sfw_content_mode" + FFMpegPath = "ffmpeg_path" FFProbePath = "ffprobe_path" @@ -628,7 +631,15 @@ func (i *Config) getStringMapString(key string) map[string]string { return ret } -// GetStathPaths returns the configured stash library paths. +// GetSFW returns true if SFW mode is enabled. +// Default performer images are changed to more agnostic images when enabled. +func (i *Config) GetSFWContentMode() bool { + i.RLock() + defer i.RUnlock() + return i.getBool(SFWContentMode) +} + +// GetStashPaths returns the configured stash library paths. // Works opposite to the usual case - it will return the override // value only if the main value is not set. func (i *Config) GetStashPaths() StashConfigs { diff --git a/internal/manager/manager.go b/internal/manager/manager.go index ca70b1c13..2d47fd907 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -262,6 +262,10 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error { cfg.SetString(config.Cache, input.CacheLocation) } + if input.SFWContentMode { + cfg.SetBool(config.SFWContentMode, true) + } + if input.StoreBlobsInDatabase { cfg.SetInterface(config.BlobsStorage, config.BlobStorageTypeDatabase) } else { diff --git a/internal/manager/models.go b/internal/manager/models.go index 3e96e6182..b7c7232c5 100644 --- a/internal/manager/models.go +++ b/internal/manager/models.go @@ -21,6 +21,7 @@ type SetupInput struct { // Empty to indicate $HOME/.stash/config.yml default ConfigLocation string `json:"configLocation"` Stashes []*config.StashConfigInput `json:"stashes"` + SFWContentMode bool `json:"sfwContentMode"` // Empty to indicate default DatabaseFile string `json:"databaseFile"` // Empty to indicate default diff --git a/internal/static/embed.go b/internal/static/embed.go index 91437a81f..665c5a892 100644 --- a/internal/static/embed.go +++ b/internal/static/embed.go @@ -8,12 +8,13 @@ import ( "io/fs" ) -//go:embed performer performer_male scene image gallery tag studio group +//go:embed performer performer_male performer_sfw scene image gallery tag studio group var data embed.FS const ( - Performer = "performer" - PerformerMale = "performer_male" + Performer = "performer" + PerformerMale = "performer_male" + DefaultSFWPerformerImage = "performer_sfw/performer.svg" Scene = "scene" DefaultSceneImage = "scene/scene.svg" diff --git a/internal/static/performer_sfw/performer.svg b/internal/static/performer_sfw/performer.svg new file mode 100644 index 000000000..24b444171 --- /dev/null +++ b/internal/static/performer_sfw/performer.svg @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index c0bcda821..95d55864f 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -71,6 +71,7 @@ fragment ConfigGeneralData on ConfigGeneralResult { } fragment ConfigInterfaceData on ConfigInterfaceResult { + sfwContentMode menuItems soundOnPreview wallShowTitle diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 005d101aa..a8b92ecc3 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -31,7 +31,10 @@ import * as GQL from "./core/generated-graphql"; import { makeTitleProps } from "./hooks/title"; import { LoadingIndicator } from "./components/Shared/LoadingIndicator"; -import { ConfigurationProvider } from "./hooks/Config"; +import { + ConfigurationProvider, + useConfigurationContextOptional, +} from "./hooks/Config"; import { ManualProvider } from "./components/Help/context"; import { InteractiveProvider } from "./hooks/Interactive/context"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; @@ -50,6 +53,7 @@ import { PatchFunction } from "./patch"; import moment from "moment/min/moment-with-locales"; import { ErrorMessage } from "./components/Shared/ErrorMessage"; +import cx from "classnames"; const Performers = lazyComponent( () => import("./components/Performers/Performers") @@ -104,8 +108,17 @@ const AppContainer: React.FC> = PatchFunction( ) as React.FC; const MainContainer: React.FC = ({ children }) => { + // use optional here because the configuration may have be loading or errored + const { configuration } = useConfigurationContextOptional() || {}; + const { sfwContentMode } = configuration?.interface || {}; + return ( -
+
{children}
); @@ -300,28 +313,36 @@ export const App: React.FC = () => { return null; } - if (config.error) { + function renderSimple(content: React.ReactNode) { return ( - - - } - error={config.error.message} - /> - + {content} ); } + if (config.loading) { + return renderSimple(); + } + + if (config.error) { + return renderSimple( + + } + error={config.error.message} + /> + ); + } + return ( { - + {maybeRenderReleaseNotes()} }> diff --git a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx index 669bc8aa4..5afdb0b8e 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -6,7 +6,7 @@ import { Icon } from "src/components/Shared/Icon"; import { useToast } from "src/hooks/Toast"; import * as GQL from "src/core/generated-graphql"; import { FormattedMessage, useIntl } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { Manual } from "../Help/Manual"; import { withoutTypename } from "src/utils/data"; import { GenerateOptions } from "../Settings/Tasks/GenerateOptions"; @@ -25,7 +25,7 @@ export const GenerateDialog: React.FC = ({ onClose, type, }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); function getDefaultOptions(): GQL.GenerateMetadataInput { return { diff --git a/ui/v2.5/src/components/FrontPage/Control.tsx b/ui/v2.5/src/components/FrontPage/Control.tsx index 7ff31bbac..72f84516f 100644 --- a/ui/v2.5/src/components/FrontPage/Control.tsx +++ b/ui/v2.5/src/components/FrontPage/Control.tsx @@ -1,9 +1,9 @@ -import React, { useContext, useMemo } from "react"; +import React, { useMemo } from "react"; import { useIntl } from "react-intl"; import { FrontPageContent, ICustomFilter } from "src/core/config"; import * as GQL from "src/core/generated-graphql"; import { useFindSavedFilter } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ListFilterModel } from "src/models/list-filter/filter"; import { GalleryRecommendationRow } from "../Galleries/GalleryRecommendationRow"; import { ImageRecommendationRow } from "../Images/ImageRecommendationRow"; @@ -105,7 +105,7 @@ interface ISavedFilterResults { const SavedFilterResults: React.FC = ({ savedFilterID, }) => { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const { loading, data } = useFindSavedFilter(savedFilterID.toString()); const filter = useMemo(() => { @@ -136,7 +136,7 @@ interface ICustomFilterProps { const CustomFilterResults: React.FC = ({ customFilter, }) => { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const intl = useIntl(); const filter = useMemo(() => { diff --git a/ui/v2.5/src/components/FrontPage/FrontPage.tsx b/ui/v2.5/src/components/FrontPage/FrontPage.tsx index 89b4db468..12e56f6ab 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPage.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPage.tsx @@ -6,7 +6,7 @@ import { Button } from "react-bootstrap"; import { FrontPageConfig } from "./FrontPageConfig"; import { useToast } from "src/hooks/Toast"; import { Control } from "./Control"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FrontPageContent, generateDefaultFrontPageContent, @@ -24,7 +24,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => { const [saveUI] = useConfigureUI(); - const { configuration, loading } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); useScrollToTopOnMount(); @@ -51,7 +51,7 @@ const FrontPage: React.FC = PatchComponent("FrontPage", () => { setSaving(false); } - if (loading || saving) { + if (saving) { return ; } diff --git a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx index 2f72d0740..33e6c066a 100644 --- a/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx +++ b/ui/v2.5/src/components/FrontPage/FrontPageConfig.tsx @@ -4,7 +4,7 @@ import { useFindSavedFilters } from "src/core/StashService"; import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { Button, Form, Modal } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ISavedFilterRow, ICustomFilter, @@ -277,11 +277,11 @@ interface IFrontPageConfigProps { export const FrontPageConfig: React.FC = ({ onClose, }) => { - const { configuration, loading } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const ui = configuration?.ui; - const { data: allFilters, loading: loading2 } = useFindSavedFilters(); + const { data: allFilters, loading } = useFindSavedFilters(); const [isAdd, setIsAdd] = useState(false); const [currentContent, setCurrentContent] = useState([]); @@ -338,7 +338,7 @@ export const FrontPageConfig: React.FC = ({ setDragIndex(undefined); } - if (loading || loading2) { + if (loading) { return ; } diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index 2feaa0f1e..0e50c16b8 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ModalComponent } from "../Shared/Modal"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -33,7 +33,7 @@ export const DeleteGalleriesDialog: React.FC = ( { count: props.selected.length, singularEntity, pluralEntity } ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [deleteFile, setDeleteFile] = useState( config?.defaults.deleteFile ?? false diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 5d7cdeb51..9cee2d1e2 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -1,5 +1,5 @@ import { Button, Tab, Nav, Dropdown } from "react-bootstrap"; -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useHistory, Link, @@ -41,7 +41,7 @@ import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import cx from "classnames"; import { useRatingKeybinds } from "src/hooks/keybinds"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { goBackOrReplace } from "src/utils/history"; @@ -59,7 +59,7 @@ export const GalleryPage: React.FC = ({ gallery, add }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const showLightbox = useGalleryLightbox(gallery.id, gallery.chapters); const [collapsed, setCollapsed] = useState(false); diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx index 5fe20b7b0..fbfde9f97 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx @@ -161,32 +161,38 @@ export const GalleryScrapeDialog: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURLs(value)} /> setDate(value)} /> setPhotographer(value)} /> setStudio(value)} @@ -194,6 +200,7 @@ export const GalleryScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setPerformers(value)} @@ -203,6 +210,7 @@ export const GalleryScrapeDialog: React.FC = ({ /> {scrapedTagsRow} setDetails(value)} diff --git a/ui/v2.5/src/components/Galleries/GallerySelect.tsx b/ui/v2.5/src/components/Galleries/GallerySelect.tsx index 4cd8825bb..c76266cf7 100644 --- a/ui/v2.5/src/components/Galleries/GallerySelect.tsx +++ b/ui/v2.5/src/components/Galleries/GallerySelect.tsx @@ -12,7 +12,7 @@ import { queryFindGalleriesForSelect, queryFindGalleriesByIDForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -70,7 +70,7 @@ const gallerySelectSort = PatchFunction( const _GallerySelect: React.FC< IFilterProps & IFilterValueProps & ExtraGalleryProps > = (props) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/Groups/GroupCard.tsx b/ui/v2.5/src/components/Groups/GroupCard.tsx index 87a594446..7412c986a 100644 --- a/ui/v2.5/src/components/Groups/GroupCard.tsx +++ b/ui/v2.5/src/components/Groups/GroupCard.tsx @@ -10,7 +10,7 @@ import { FormattedMessage } from "react-intl"; import { RatingBanner } from "../Shared/RatingBanner"; import { faPlayCircle, faTag } from "@fortawesome/free-solid-svg-icons"; import { RelatedGroupPopoverButton } from "./RelatedGroupPopover"; -import { SweatDrops } from "../Shared/SweatDrops"; +import { OCounterButton } from "../Shared/CountButton"; const Description: React.FC<{ sceneNumber?: number; @@ -111,16 +111,7 @@ export const GroupCard: React.FC = ({ function maybeRenderOCounter() { if (!group.o_counter) return; - return ( -
- -
- ); + return ; } function maybeRenderPopoverButtonGroup() { diff --git a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx index b48f3b98c..b2b3d8176 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx @@ -23,7 +23,7 @@ import { import { GroupEditPanel } from "./GroupEditPanel"; import { faRefresh, faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { DetailImage } from "src/components/Shared/DetailImage"; import { useRatingKeybinds } from "src/hooks/keybinds"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; @@ -146,7 +146,7 @@ const GroupPage: React.FC = ({ group, tabKey }) => { const Toast = useToast(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const enableBackgroundImage = uiConfig?.enableMovieBackgroundImage ?? false; const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx index bdb5d6ad5..d37210c43 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx @@ -149,37 +149,44 @@ export const GroupScrapeDialog: React.FC = ({ return ( <> setName(value)} /> setAliases(value)} /> setDuration(value)} /> setDate(value)} /> setDirector(value)} /> setSynopsis(value)} /> setStudio(value)} @@ -187,18 +194,21 @@ export const GroupScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setURLs(value)} /> {scrapedTagsRow} setFrontImage(value)} /> = PatchComponent("GroupSelect", (props) => { const [createGroup] = useGroupCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx index 36a3ead3c..ec442a5ca 100644 --- a/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx +++ b/ui/v2.5/src/components/Images/DeleteImagesDialog.tsx @@ -4,7 +4,7 @@ import { useImagesDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ModalComponent } from "src/components/Shared/Modal"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; @@ -33,7 +33,7 @@ export const DeleteImagesDialog: React.FC = ( { count: props.selected.length, singularEntity, pluralEntity } ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [deleteFile, setDeleteFile] = useState( config?.defaults.deleteFile ?? false diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index 9a8c86a10..a22e48139 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -5,7 +5,6 @@ import * as GQL from "src/core/generated-graphql"; import { Icon } from "src/components/Shared/Icon"; import { GalleryLink, TagLink } from "src/components/Shared/TagLink"; import { HoverPopover } from "src/components/Shared/HoverPopover"; -import { SweatDrops } from "src/components/Shared/SweatDrops"; import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton"; import { GridCard } from "src/components/Shared/GridCard/GridCard"; import { RatingBanner } from "src/components/Shared/RatingBanner"; @@ -18,6 +17,7 @@ import { import { imageTitle } from "src/core/files"; import { TruncatedText } from "../Shared/TruncatedText"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; +import { OCounterButton } from "../Shared/CountButton"; interface IImageCardProps { image: GQL.SlimImageDataFragment; @@ -74,16 +74,7 @@ export const ImageCard: React.FC = ( function maybeRenderOCounter() { if (props.image.o_counter) { - return ( -
- -
- ); + return ; } } diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index 3b77d2eef..699d3d4e4 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -1,5 +1,5 @@ import { Tab, Nav, Dropdown } from "react-bootstrap"; -import React, { useContext, useEffect, useMemo, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import { useHistory, Link, RouteComponentProps } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -29,7 +29,7 @@ import { imagePath, imageTitle } from "src/core/files"; import { isVideo } from "src/utils/visualFile"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { useRatingKeybinds } from "src/hooks/keybinds"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import TextUtils from "src/utils/text"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import cx from "classnames"; @@ -48,7 +48,7 @@ const ImagePage: React.FC = ({ image }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [incrementO] = useImageIncrementO(image.id); const [decrementO] = useImageDecrementO(image.id); diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx index cc7dffe66..44b112078 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx @@ -163,32 +163,38 @@ export const ImageScrapeDialog: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURLs(value)} /> setDate(value)} /> setPhotographer(value)} /> setStudio(value)} @@ -196,6 +202,7 @@ export const ImageScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setPerformers(value)} @@ -204,6 +211,7 @@ export const ImageScrapeDialog: React.FC = ({ /> {scrapedTagsRow} setDetails(value)} diff --git a/ui/v2.5/src/components/Images/ImageList.tsx b/ui/v2.5/src/components/Images/ImageList.tsx index dc1f9b1e1..df10ff4b5 100644 --- a/ui/v2.5/src/components/Images/ImageList.tsx +++ b/ui/v2.5/src/components/Images/ImageList.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useState, - useMemo, - MouseEvent, - useContext, -} from "react"; +import React, { useCallback, useState, useMemo, MouseEvent } from "react"; import { FormattedNumber, useIntl } from "react-intl"; import cloneDeep from "lodash-es/cloneDeep"; import { useHistory } from "react-router-dom"; @@ -23,7 +17,7 @@ import "flexbin/flexbin.css"; import Gallery, { RenderImageProps } from "react-photo-gallery"; import { ExportDialog } from "../Shared/ExportDialog"; import { objectTitle } from "src/core/files"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ImageGridCard } from "./ImageGridCard"; import { View } from "../List/views"; import { IItemListOperation } from "../List/FilteredListToolbar"; @@ -51,7 +45,7 @@ const ImageWall: React.FC = ({ zoomIndex, handleImageOpen, }) => { - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const containerRef = React.useRef(null); diff --git a/ui/v2.5/src/components/List/EditFilterDialog.tsx b/ui/v2.5/src/components/List/EditFilterDialog.tsx index 5f6d43004..3f0f486b8 100644 --- a/ui/v2.5/src/components/List/EditFilterDialog.tsx +++ b/ui/v2.5/src/components/List/EditFilterDialog.tsx @@ -1,7 +1,6 @@ import cloneDeep from "lodash-es/cloneDeep"; import React, { useCallback, - useContext, useEffect, useMemo, useRef, @@ -14,7 +13,7 @@ import { CriterionOption, } from "src/models/list-filter/criteria/criterion"; import { FormattedMessage, useIntl } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ListFilterModel } from "src/models/list-filter/filter"; import { getFilterOptions } from "src/models/list-filter/factory"; import { FilterTags } from "./FilterTags"; @@ -65,6 +64,9 @@ const CriterionOptionList: React.FC = ({ onTogglePin, externallySelected = false, }) => { + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + const prevCriterion = usePrevious(currentCriterion); const scrolled = useRef(false); @@ -148,7 +150,9 @@ const CriterionOptionList: React.FC = ({ className="collapse-icon fa-fw" icon={type === c.type ? faChevronDown : faChevronRight} /> - + {criteria.some((cc) => c.type === cc) && ( - - - ))} - - - - + + {menuItems.map(({ href, icon, message }) => ( + + + + + + ))} + + diff --git a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx index 71fcbedd9..677ac3aa1 100644 --- a/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx +++ b/ui/v2.5/src/components/Performers/EditPerformersDialog.tsx @@ -27,6 +27,8 @@ import { BulkUpdateTextInput } from "../Shared/BulkUpdateTextInput"; import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; import * as FormUtils from "src/utils/form"; import { CountrySelect } from "../Shared/CountrySelect"; +import { useConfigurationContext } from "src/hooks/Config"; +import cx from "classnames"; interface IListOperationProps { selected: GQL.SlimPerformerDataFragment[]; @@ -61,6 +63,10 @@ export const EditPerformersDialog: React.FC = ( ) => { const intl = useIntl(); const Toast = useToast(); + + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + const [tagIds, setTagIds] = useState({ mode: GQL.BulkUpdateIdMode.Add, }); @@ -204,7 +210,7 @@ export const EditPerformersDialog: React.FC = ( setter: (newValue: string | undefined) => void ) { return ( - + @@ -218,9 +224,13 @@ export const EditPerformersDialog: React.FC = ( } function render() { + // sfw class needs to be set because it is outside body + return ( = ( }} isRunning={isUpdating} > - + {FormUtils.renderLabel({ title: intl.formatMessage({ id: "rating" }), })} @@ -322,7 +332,7 @@ export const EditPerformersDialog: React.FC = ( setPenisLength(v) )} - + diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 02c304547..5f7a26d42 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -6,7 +6,6 @@ import NavUtils from "src/utils/navigation"; import TextUtils from "src/utils/text"; import { GridCard } from "../Shared/GridCard/GridCard"; import { CountryFlag } from "../Shared/CountryFlag"; -import { SweatDrops } from "../Shared/SweatDrops"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; import { TagLink } from "../Shared/TagLink"; @@ -25,7 +24,8 @@ import { ILabeledId } from "src/models/list-filter/types"; import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { PatchComponent } from "src/patch"; import { ExternalLinksButton } from "../Shared/ExternalLinksButton"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; +import { OCounterButton } from "../Shared/CountButton"; export interface IPerformerCardExtraCriteria { scenes?: ModifierCriterion[]; @@ -103,16 +103,7 @@ const PerformerCardPopovers: React.FC = PatchComponent( function maybeRenderOCounter() { if (!performer.o_counter) return; - return ( -
- -
- ); + return ; } function maybeRenderTagPopoverButton() { @@ -179,7 +170,7 @@ const PerformerCardPopovers: React.FC = PatchComponent( const PerformerCardOverlays: React.FC = PatchComponent( "PerformerCard.Overlays", ({ performer }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const [updatePerformer] = usePerformerUpdate(); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 0a1535068..dd72d0025 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -16,7 +16,7 @@ import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { CompressedPerformerDetailsPanel, @@ -42,13 +42,13 @@ import { import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; -import { SweatDrops } from "src/components/Shared/SweatDrops"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { LightboxLink } from "src/hooks/Lightbox/LightboxLink"; import { PatchComponent } from "src/patch"; import { ILightboxImage } from "src/hooks/Lightbox/types"; import { goBackOrReplace } from "src/utils/history"; +import { OCounterButton } from "src/components/Shared/CountButton"; interface IProps { performer: GQL.PerformerDataFragment; @@ -240,7 +240,7 @@ const PerformerPage: React.FC = PatchComponent( const intl = useIntl(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = @@ -432,12 +432,7 @@ const PerformerPage: React.FC = PatchComponent( withoutContext /> {!!performer.o_counter && ( - - - - - {performer.o_counter} - + )}
{!isEditing && ( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index a3f128fee..597cbad1c 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -30,7 +30,7 @@ import { stringCircumMap, stringToCircumcised, } from "src/utils/circumcised"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import PerformerScrapeModal from "./PerformerScrapeModal"; import PerformerStashBoxModal, { IStashBox } from "./PerformerStashBoxModal"; @@ -97,7 +97,7 @@ export const PerformerEditPanel: React.FC = ({ const [scrapedPerformer, setScrapedPerformer] = useState(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const intl = useIntl(); diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx index 0398f1eec..ad7e44d6d 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx @@ -63,6 +63,7 @@ function renderScrapedGenderRow( ) { return ( renderScrapedGender(result)} @@ -113,6 +114,7 @@ function renderScrapedCircumcisedRow( return ( renderScrapedCircumcised(result)} renderNewField={() => @@ -401,16 +403,19 @@ export const PerformerScrapeDialog: React.FC = ( return ( <> setName(value)} /> setDisambiguation(value)} /> setAliases(value)} @@ -421,46 +426,55 @@ export const PerformerScrapeDialog: React.FC = ( (value) => setGender(value) )} setBirthdate(value)} /> setDeathDate(value)} /> setEthnicity(value)} /> setCountry(value)} /> setHairColor(value)} /> setEyeColor(value)} /> setWeight(value)} /> setHeight(value)} /> setPenisLength(value)} @@ -471,42 +485,50 @@ export const PerformerScrapeDialog: React.FC = ( (value) => setCircumcised(value) )} setMeasurements(value)} /> setFakeTits(value)} /> setCareerLength(value)} /> setTattoos(value)} /> setPiercings(value)} /> setURLs(value)} /> setDetails(value)} /> {scrapedTagsRow} = ( onChange={(value) => setImage(value)} /> = ({ placement = "top", target, }) => { - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true; diff --git a/ui/v2.5/src/components/Performers/PerformerSelect.tsx b/ui/v2.5/src/components/Performers/PerformerSelect.tsx index ed7b7b303..f10519897 100644 --- a/ui/v2.5/src/components/Performers/PerformerSelect.tsx +++ b/ui/v2.5/src/components/Performers/PerformerSelect.tsx @@ -13,7 +13,7 @@ import { queryFindPerformersByIDForSelect, queryFindPerformersForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -82,7 +82,7 @@ const _PerformerSelect: React.FC< > = (props) => { const [createPerformer] = usePerformerCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx index 4440f80df..e07c0091d 100644 --- a/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx +++ b/ui/v2.5/src/components/ScenePlayer/ScenePlayer.tsx @@ -1,7 +1,6 @@ import React, { KeyboardEvent, useCallback, - useContext, useEffect, useMemo, useRef, @@ -31,7 +30,7 @@ import { import * as GQL from "src/core/generated-graphql"; import { ScenePlayerScrubber } from "./ScenePlayerScrubber"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ConnectionState, InteractiveContext, @@ -240,7 +239,7 @@ export const ScenePlayer: React.FC = PatchComponent( onNext, onPrevious, }) => { - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const interfaceConfig = configuration?.interface; const uiConfig = configuration?.ui; const videoRef = useRef(null); diff --git a/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx b/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx index 88f133a80..3cf9b7ecf 100644 --- a/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx +++ b/ui/v2.5/src/components/Scenes/DeleteScenesDialog.tsx @@ -4,7 +4,7 @@ import { useScenesDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { ModalComponent } from "src/components/Shared/Modal"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FormattedMessage, useIntl } from "react-intl"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { objectPath } from "src/core/files"; @@ -34,7 +34,7 @@ export const DeleteScenesDialog: React.FC = ( { count: props.selected.length, singularEntity, pluralEntity } ); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [deleteFile, setDeleteFile] = useState( config?.defaults.deleteFile ?? false diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index 99b910f67..2cb4a9af3 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -6,12 +6,11 @@ import * as GQL from "src/core/generated-graphql"; import { Icon } from "../Shared/Icon"; import { GalleryLink, TagLink, SceneMarkerLink } from "../Shared/TagLink"; import { HoverPopover } from "../Shared/HoverPopover"; -import { SweatDrops } from "../Shared/SweatDrops"; import { TruncatedText } from "../Shared/TruncatedText"; import NavUtils from "src/utils/navigation"; import TextUtils from "src/utils/text"; import { SceneQueue } from "src/models/sceneQueue"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { GridCard } from "../Shared/GridCard/GridCard"; import { RatingBanner } from "../Shared/RatingBanner"; @@ -30,6 +29,7 @@ import { PatchComponent } from "src/patch"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; import { GroupTag } from "../Groups/GroupTag"; import { FileSize } from "../Shared/FileSize"; +import { OCounterButton } from "../Shared/CountButton"; interface IScenePreviewProps { isPortrait: boolean; @@ -218,16 +218,7 @@ const SceneCardPopovers = PatchComponent( function maybeRenderOCounter() { if (props.scene.o_counter) { - return ( -
- -
- ); + return ; } } @@ -353,7 +344,7 @@ const SceneCardImage = PatchComponent( "SceneCard.Image", (props: ISceneCardProps) => { const history = useHistory(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const cont = configuration?.interface.continuePlaylistDefault ?? false; const file = useMemo( @@ -437,7 +428,7 @@ const SceneCardImage = PatchComponent( export const SceneCard = PatchComponent( "SceneCard", (props: ISceneCardProps) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const file = useMemo( () => (props.scene.files.length > 0 ? props.scene.files[0] : undefined), diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx index 8fdb7dfd7..d8963df4d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/OCounterButton.tsx @@ -1,10 +1,11 @@ -import { faBan, faMinus } from "@fortawesome/free-solid-svg-icons"; +import { faBan, faMinus, faThumbsUp } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, ButtonGroup, Dropdown, DropdownButton } from "react-bootstrap"; import { useIntl } from "react-intl"; import { Icon } from "src/components/Shared/Icon"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { SweatDrops } from "src/components/Shared/SweatDrops"; +import { useConfigurationContext } from "src/hooks/Config"; export interface IOCounterButtonProps { value: number; @@ -17,6 +18,12 @@ export const OCounterButton: React.FC = ( props: IOCounterButtonProps ) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + + const icon = !sfwContentMode ? : ; + const messageID = !sfwContentMode ? "o_count" : "o_count_sfw"; + const [loading, setLoading] = useState(false); async function increment() { @@ -44,9 +51,9 @@ export const OCounterButton: React.FC = ( className="minimal pr-1" onClick={increment} variant="secondary" - title={intl.formatMessage({ id: "o_counter" })} + title={intl.formatMessage({ id: messageID })} > - + {icon} {props.value} ); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index f7e844392..aee6ab344 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState, useMemo, - useContext, useRef, useLayoutEffect, } from "react"; @@ -32,7 +31,7 @@ import SceneQueue, { QueuedScene } from "src/models/sceneQueue"; import { ListFilterModel } from "src/models/list-filter/filter"; import Mousetrap from "mousetrap"; import { OrganizedButton } from "./OrganizedButton"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { getPlayerPosition } from "src/components/ScenePlayer/util"; import { faEllipsisV, @@ -184,7 +183,7 @@ const ScenePage: React.FC = PatchComponent("ScenePage", (props) => { const intl = useIntl(); const [updateScene] = useSceneUpdate(); const [generateScreenshot] = useSceneGenerateScreenshot(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [showDraftModal, setShowDraftModal] = useState(false); const boxes = configuration?.general?.stashBoxes ?? []; @@ -689,7 +688,7 @@ const SceneLoader: React.FC> = ({ match, }) => { const { id } = match.params; - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const { data, loading, error } = useFindScene(id); const [scene, setScene] = useState(); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx index 69b378787..e56ea265b 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx @@ -19,7 +19,7 @@ import ImageUtils from "src/utils/image"; import { getStashIDs } from "src/utils/stashIds"; import { useFormik } from "formik"; import { Prompt } from "react-router-dom"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable"; import { faSearch } from "@fortawesome/free-solid-svg-icons"; import { objectTitle } from "src/core/files"; @@ -103,7 +103,7 @@ export const SceneEditPanel: React.FC = ({ setStudio(scene.studio ?? null); }, [scene.studio]); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); // Network state const [isLoading, setIsLoading] = useState(false); diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx index 1ac9dd5a2..2ba587a2b 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneHistoryPanel.tsx @@ -21,6 +21,7 @@ import { useSceneResetActivity, } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; +import { useConfigurationContext } from "src/hooks/Config"; import { useToast } from "src/hooks/Toast"; import { TextField } from "src/utils/field"; import TextUtils from "src/utils/text"; @@ -172,6 +173,9 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => { const intl = useIntl(); const Toast = useToast(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + const [dialogs, setDialogs] = React.useState({ playHistory: false, oHistory: false, @@ -299,6 +303,9 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => { } function maybeRenderDialogs() { + const clearHistoryMessageID = sfwContentMode + ? "dialogs.clear_o_history_confirm_sfw" + : "dialogs.clear_play_history_confirm"; return ( <> = ({ scene }) => { /> handleClearODates()} onCancel={() => setDialogPartial({ oHistory: false })} @@ -351,6 +358,11 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => { ) as string[]; const oHistory = (scene.o_history ?? []).filter((h) => h != null) as string[]; + const oHistoryMessageID = sfwContentMode ? "o_history_sfw" : "o_history"; + const noneMessageID = sfwContentMode + ? "odate_recorded_no_sfw" + : "odate_recorded_no"; + return (
{maybeRenderDialogs()} @@ -401,7 +413,7 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => {
- + @@ -427,7 +439,7 @@ export const SceneHistoryPanel: React.FC = ({ scene }) => {
handleDeleteODate(t)} /> diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index 6a89caf85..7be291bd2 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -218,32 +218,38 @@ export const SceneScrapeDialog: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURLs(value)} /> setDate(value)} /> setDirector(value)} /> setStudio(value)} @@ -251,6 +257,7 @@ export const SceneScrapeDialog: React.FC = ({ onCreateNew={createNewStudio} /> setPerformers(value)} @@ -259,6 +266,7 @@ export const SceneScrapeDialog: React.FC = ({ ageFromDate={date.useNewValue ? date.newValue : date.originalValue} /> setGroups(value)} @@ -267,17 +275,20 @@ export const SceneScrapeDialog: React.FC = ({ /> {scrapedTagsRow} setDetails(value)} /> setStashID(value)} /> { }, }); - const { filter, setFilter, loading: filterLoading } = filterState; + const { filter, setFilter } = filterState; const { effectiveFilter, result, cachedResult, items, totalCount } = queryResult; @@ -709,7 +709,7 @@ export const FilteredSceneList = (props: IFilteredScenes) => { ]; // render - if (filterLoading || sidebarStateLoading) return null; + if (sidebarStateLoading) return null; const operations = ( { }; const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const file = useMemo( () => diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx index 9c507730b..0349fae0f 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerWallPanel.tsx @@ -1,17 +1,11 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import Gallery, { GalleryI, PhotoProps, RenderImageProps, } from "react-photo-gallery"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { objectTitle } from "src/core/files"; import { Link, useHistory } from "react-router-dom"; import { TruncatedText } from "../Shared/TruncatedText"; @@ -46,7 +40,7 @@ interface IExtraProps { export const MarkerWallItem: React.FC< RenderImageProps & IExtraProps > = (props: RenderImageProps & IExtraProps) => { - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const playSound = configuration?.interface.soundOnPreview ?? false; const showTitle = configuration?.interface.wallShowTitle ?? false; diff --git a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx index d5a18d4ac..511ca2351 100644 --- a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx @@ -372,27 +372,32 @@ const SceneMergeDetails: React.FC = ({ return ( <> setTitle(value)} /> setCode(value)} /> setURL(value)} /> setDate(value)} /> ( @@ -404,6 +409,7 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setRating(value)} /> ( @@ -425,6 +431,7 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setOCounter(value)} /> ( @@ -446,6 +453,7 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setPlayCount(value)} /> ( @@ -469,6 +477,7 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setPlayDuration(value)} /> ( @@ -492,32 +501,38 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setGalleries(value)} /> setStudio(value)} /> setPerformers(value)} ageFromDate={date.useNewValue ? date.newValue : date.originalValue} /> setGroups(value)} /> setTags(value)} /> setDetails(value)} /> ( @@ -539,6 +554,7 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setOrganized(value)} /> ( @@ -550,6 +566,7 @@ const SceneMergeDetails: React.FC = ({ onChange={(value) => setStashIDs(value)} /> & ExtraSceneProps > = (props) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx b/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx index 6f98cdaab..3f5020793 100644 --- a/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneWallPanel.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { SceneQueue } from "src/models/sceneQueue"; import Gallery, { @@ -12,7 +6,7 @@ import Gallery, { PhotoProps, RenderImageProps, } from "react-photo-gallery"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { objectTitle } from "src/core/files"; import { Link, useHistory } from "react-router-dom"; import { TruncatedText } from "../Shared/TruncatedText"; @@ -35,7 +29,7 @@ export const SceneWallItem: React.FC< > = (props: RenderImageProps & IExtraProps) => { const intl = useIntl(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const playSound = configuration?.interface.soundOnPreview ?? false; const showTitle = configuration?.interface.wallShowTitle ?? false; diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index e0c538cd0..ba93385b5 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -240,6 +240,14 @@ export const SettingsInterfacePanel: React.FC = PatchComponent( + saveInterface({ sfwContentMode: v })} + /> +
diff --git a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx index e093dc60a..c36e076f4 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx @@ -22,7 +22,7 @@ import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting } from "../Inputs"; import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared/Icon"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { faMinus, @@ -44,7 +44,7 @@ const CleanDialog: React.FC = ({ onClose, }) => { const intl = useIntl(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const libraryPaths = configuration?.general.stashes.map((s) => s.path); diff --git a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx index 9fdaf09f4..87a58f292 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx @@ -9,7 +9,7 @@ import { useIntl } from "react-intl"; import { Icon } from "src/components/Shared/Icon"; import { ModalComponent } from "src/components/Shared/Modal"; import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; interface IDirectorySelectionDialogProps { animation?: boolean; @@ -22,7 +22,7 @@ export const DirectorySelectionDialog: React.FC< IDirectorySelectionDialogProps > = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => { const intl = useIntl(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const libraryPaths = configuration?.general.stashes.map((s) => s.path); diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx index cb60891fd..605e37933 100644 --- a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -7,7 +7,7 @@ import { mutateMetadataGenerate, } from "src/core/StashService"; import { withoutTypename } from "src/utils/data"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { IdentifyDialog } from "../../Dialogs/IdentifyDialog/IdentifyDialog"; import * as GQL from "src/core/generated-graphql"; import { DirectorySelectionDialog } from "./DirectorySelectionDialog"; @@ -123,7 +123,7 @@ export const LibraryTasks: React.FC = () => { type DialogOpenState = typeof dialogOpen; - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [configRead, setConfigRead] = useState(false); useEffect(() => { diff --git a/ui/v2.5/src/components/Setup/Setup.tsx b/ui/v2.5/src/components/Setup/Setup.tsx index ab5411fe1..27f9b4e58 100644 --- a/ui/v2.5/src/components/Setup/Setup.tsx +++ b/ui/v2.5/src/components/Setup/Setup.tsx @@ -1,4 +1,4 @@ -import React, { useState, useContext, useCallback } from "react"; +import React, { useState, useCallback } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { Alert, @@ -15,7 +15,7 @@ import { useSystemStatus, } from "src/core/StashService"; import { useHistory } from "react-router-dom"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import StashConfiguration from "../Settings/StashConfiguration"; import { Icon } from "../Shared/Icon"; import { LoadingIndicator } from "../Shared/LoadingIndicator"; @@ -518,6 +518,10 @@ const SetPathsStep: React.FC = ({ goBack, next }) => { const [stashes, setStashes] = useState( setupState.stashes ?? [] ); + const [sfwContentMode, setSfwContentMode] = useState( + setupState.sfwContentMode ?? false + ); + const [databaseFile, setDatabaseFile] = useState( setupState.databaseFile ?? "" ); @@ -555,6 +559,7 @@ const SetPathsStep: React.FC = ({ goBack, next }) => { cacheLocation, blobsLocation: storeBlobsInDatabase ? "" : blobsLocation, storeBlobsInDatabase, + sfwContentMode, }; next(input); } @@ -594,6 +599,22 @@ const SetPathsStep: React.FC = ({ goBack, next }) => { /> + +

+ +

+

+ +

+ + } + onChange={() => setSfwContentMode(!sfwContentMode)} + /> + +
{overrideDatabase ? null : ( = ({ goBack }) => { export const Setup: React.FC = () => { const intl = useIntl(); - const { configuration, loading: configLoading } = - useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [saveUI] = useConfigureUI(); @@ -1024,7 +1044,7 @@ export const Setup: React.FC = () => { } } - if (configLoading || statusLoading) { + if (statusLoading) { return ; } diff --git a/ui/v2.5/src/components/Shared/CountButton.tsx b/ui/v2.5/src/components/Shared/CountButton.tsx index 1519c104b..ad099c2f5 100644 --- a/ui/v2.5/src/components/Shared/CountButton.tsx +++ b/ui/v2.5/src/components/Shared/CountButton.tsx @@ -1,10 +1,11 @@ -import { faEye } from "@fortawesome/free-solid-svg-icons"; +import { faEye, faThumbsUp } from "@fortawesome/free-solid-svg-icons"; import React from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import { Icon } from "src/components/Shared/Icon"; import { SweatDrops } from "./SweatDrops"; import cx from "classnames"; import { useIntl } from "react-intl"; +import { useConfigurationContext } from "src/hooks/Config"; interface ICountButtonProps { value: number; @@ -63,11 +64,17 @@ export const ViewCountButton: React.FC = (props) => { export const OCounterButton: React.FC = (props) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + + const icon = !sfwContentMode ? : ; + const messageID = !sfwContentMode ? "o_count" : "o_count_sfw"; + return ( } - title={intl.formatMessage({ id: "o_count" })} + icon={icon} + title={intl.formatMessage({ id: messageID })} countTitle={intl.formatMessage({ id: "actions.view_history" })} /> ); diff --git a/ui/v2.5/src/components/Shared/DetailItem.tsx b/ui/v2.5/src/components/Shared/DetailItem.tsx index a92f75868..76b595127 100644 --- a/ui/v2.5/src/components/Shared/DetailItem.tsx +++ b/ui/v2.5/src/components/Shared/DetailItem.tsx @@ -3,6 +3,7 @@ import { FormattedMessage } from "react-intl"; interface IDetailItem { id?: string | null; + className?: string; label?: React.ReactNode; value?: React.ReactNode; labelTitle?: string; @@ -13,6 +14,7 @@ interface IDetailItem { export const DetailItem: React.FC = ({ id, + className = "", label, value, labelTitle, @@ -30,7 +32,7 @@ export const DetailItem: React.FC = ({ const sanitisedID = id.replace(/_/g, "-"); return ( -
+
{message} {fullWidth ? ":" : ""} diff --git a/ui/v2.5/src/components/Shared/GridCard/StudioOverlay.tsx b/ui/v2.5/src/components/Shared/GridCard/StudioOverlay.tsx index 875b122d8..9bfd25071 100644 --- a/ui/v2.5/src/components/Shared/GridCard/StudioOverlay.tsx +++ b/ui/v2.5/src/components/Shared/GridCard/StudioOverlay.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; import { Link } from "react-router-dom"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; interface IStudio { id: string; @@ -11,7 +11,7 @@ interface IStudio { export const StudioOverlay: React.FC<{ studio: IStudio | null | undefined; }> = ({ studio }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const configValue = configuration?.interface.showStudioAsText; diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index 79a36bd9d..d652ff6ad 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -11,14 +11,14 @@ import React from "react"; import { Button, OverlayTrigger, Tooltip } from "react-bootstrap"; import { FormattedNumber, useIntl } from "react-intl"; import { Link } from "react-router-dom"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import TextUtils from "src/utils/text"; import { Icon } from "./Icon"; export const Count: React.FC<{ count: number; }> = ({ count }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const abbreviateCounter = configuration?.ui.abbreviateCounters ?? false; if (!abbreviateCounter) { diff --git a/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx b/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx index a0a11c363..11103acf8 100644 --- a/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx +++ b/ui/v2.5/src/components/Shared/Rating/RatingSystem.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { defaultRatingStarPrecision, defaultRatingSystemOptions, @@ -23,7 +22,7 @@ export interface IRatingSystemProps { export const RatingSystem = PatchComponent( "RatingSystem", (props: IRatingSystemProps) => { - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const ratingSystemOptions = config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; diff --git a/ui/v2.5/src/components/Shared/RatingBanner.tsx b/ui/v2.5/src/components/Shared/RatingBanner.tsx index d152b8b52..d94b26433 100644 --- a/ui/v2.5/src/components/Shared/RatingBanner.tsx +++ b/ui/v2.5/src/components/Shared/RatingBanner.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React from "react"; import { FormattedMessage } from "react-intl"; import { convertToRatingFormat, @@ -6,14 +6,14 @@ import { RatingStarPrecision, RatingSystemType, } from "src/utils/rating"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; interface IProps { rating?: number | null; } export const RatingBanner: React.FC = ({ rating }) => { - const { configuration: config } = useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const ratingSystemOptions = config?.ui.ratingSystemOptions ?? defaultRatingSystemOptions; const isLegacy = diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx index 59d5f3985..b67c55f41 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx @@ -24,6 +24,7 @@ import { CountrySelect } from "../CountrySelect"; import { StringListInput } from "../StringListInput"; import { ImageSelector } from "../ImageSelector"; import { ScrapeResult } from "./scrapeResult"; +import { useConfigurationContext } from "src/hooks/Config"; interface IScrapedFieldProps { result: ScrapeResult; @@ -31,6 +32,7 @@ interface IScrapedFieldProps { interface IScrapedRowProps extends IScrapedFieldProps { className?: string; + field: string; title: string; renderOriginalField: (result: ScrapeResult) => JSX.Element | undefined; renderNewField: (result: ScrapeResult) => JSX.Element | undefined; @@ -105,7 +107,10 @@ export const ScrapeDialogRow = (props: IScrapedRowProps) => { } return ( - + {props.title} @@ -175,6 +180,8 @@ function getNameString(value: string) { interface IScrapedInputGroupRowProps { title: string; + field: string; + className?: string; placeholder?: string; result: ScrapeResult; locked?: boolean; @@ -187,6 +194,8 @@ export const ScrapedInputGroupRow: React.FC = ( return ( ( = (props) => { interface IScrapedStringListRowProps { title: string; + field: string; placeholder?: string; result: ScrapeResult; locked?: boolean; @@ -253,6 +263,7 @@ export const ScrapedStringListRow: React.FC = ( ( = ( return ( ( = (props) => { interface IScrapedImageRowProps { title: string; + field: string; className?: string; result: ScrapeResult; onChange: (value: ScrapeResult) => void; @@ -355,6 +368,7 @@ export const ScrapedImageRow: React.FC = (props) => { return ( ( = (props) => { interface IScrapedImagesRowProps { title: string; + field: string; className?: string; result: ScrapeResult; images: string[]; @@ -397,6 +412,7 @@ export const ScrapedImagesRow: React.FC = (props) => { return ( ( = ( props: IScrapeDialogProps ) => { const intl = useIntl(); + const { configuration } = useConfigurationContext(); + const { sfwContentMode } = configuration.interface; + return ( = ( text: intl.formatMessage({ id: "actions.cancel" }), variant: "secondary", }} - modalProps={{ size: "lg", dialogClassName: "scrape-dialog" }} + modalProps={{ + size: "lg", + dialogClassName: `scrape-dialog ${sfwContentMode ? "sfw-mode" : ""}`, + }} >
@@ -479,6 +501,7 @@ export const ScrapeDialog: React.FC = ( interface IScrapedCountryRowProps { title: string; + field: string; result: ScrapeResult; onChange: (value: ScrapeResult) => void; locked?: boolean; @@ -487,6 +510,7 @@ interface IScrapedCountryRowProps { export const ScrapedCountryRow: React.FC = ({ title, + field, result, onChange, locked, @@ -494,6 +518,7 @@ export const ScrapedCountryRow: React.FC = ({ }) => ( ( ; onChange: (value: ObjectScrapeResult) => void; newStudio?: GQL.ScrapedStudio; @@ -25,6 +26,7 @@ function getObjectName(value: T) { export const ScrapedStudioRow: React.FC = ({ title, + field, result, onChange, newStudio, @@ -73,6 +75,7 @@ export const ScrapedStudioRow: React.FC = ({ return ( renderScrapedStudio(result)} renderNewField={() => @@ -92,6 +95,7 @@ export const ScrapedStudioRow: React.FC = ({ interface IScrapedObjectsRow { title: string; + field: string; result: ScrapeResult; onChange: (value: ScrapeResult) => void; newObjects?: T[]; @@ -107,6 +111,7 @@ interface IScrapedObjectsRow { export const ScrapedObjectsRow = (props: IScrapedObjectsRow) => { const { title, + field, result, onChange, newObjects, @@ -118,6 +123,7 @@ export const ScrapedObjectsRow = (props: IScrapedObjectsRow) => { return ( renderObjects(result)} renderNewField={() => @@ -142,7 +148,15 @@ type IScrapedObjectRowImpl = Omit< export const ScrapedPerformersRow: React.FC< IScrapedObjectRowImpl & { ageFromDate?: string | null } -> = ({ title, result, onChange, newObjects, onCreateNew, ageFromDate }) => { +> = ({ + title, + field, + result, + onChange, + newObjects, + onCreateNew, + ageFromDate, +}) => { const performersCopy = useMemo(() => { return ( newObjects?.map((p) => { @@ -191,6 +205,7 @@ export const ScrapedPerformersRow: React.FC< return ( title={title} + field={field} result={result} renderObjects={renderScrapedPerformers} onChange={onChange} @@ -203,7 +218,7 @@ export const ScrapedPerformersRow: React.FC< export const ScrapedGroupsRow: React.FC< IScrapedObjectRowImpl -> = ({ title, result, onChange, newObjects, onCreateNew }) => { +> = ({ title, field, result, onChange, newObjects, onCreateNew }) => { const groupsCopy = useMemo(() => { return ( newObjects?.map((p) => { @@ -251,6 +266,7 @@ export const ScrapedGroupsRow: React.FC< return ( title={title} + field={field} result={result} renderObjects={renderScrapedGroups} onChange={onChange} @@ -263,7 +279,7 @@ export const ScrapedGroupsRow: React.FC< export const ScrapedTagsRow: React.FC< IScrapedObjectRowImpl -> = ({ title, result, onChange, newObjects, onCreateNew }) => { +> = ({ title, field, result, onChange, newObjects, onCreateNew }) => { function renderScrapedTags( scrapeResult: ScrapeResult, isNew?: boolean, @@ -297,6 +313,7 @@ export const ScrapedTagsRow: React.FC< return ( title={title} + field={field} result={result} renderObjects={renderScrapedTags} onChange={onChange} diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx index ca3658391..f298a6eeb 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/scrapedTags.tsx @@ -39,6 +39,7 @@ export function useScrapedTags( const scrapedTagsRow = ( setTags(value)} diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 4ae547cfe..4eea52a38 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -15,7 +15,7 @@ import CreatableSelect from "react-select/creatable"; import * as GQL from "src/core/generated-graphql"; import { useMarkerStrings } from "src/core/StashService"; import { SelectComponents } from "react-select/dist/declarations/src/components"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { objectTitle } from "src/core/files"; import { defaultMaxOptionsShown } from "src/core/config"; import { useDebounce } from "src/hooks/debounce"; @@ -108,7 +108,7 @@ const getSelectedItems = (selectedItems: OnChangeValue) => { const LimitedSelectMenu = ( props: MenuListProps> ) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; @@ -496,6 +496,7 @@ export const ListSelect = (props: IListSelect) => { type DisableOption = Option & { isDisabled?: boolean; + className?: string; }; interface ICheckBoxSelectProps { @@ -510,7 +511,17 @@ export const CheckBoxSelect: React.FC = ({ onChange, }) => { const Option = (props: OptionProps) => ( - + , + HTMLDivElement + > + } + > ; linkType: LinkType; }> = ({ stashID, linkType }) => { - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const { endpoint, stash_id } = stashID; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index fc416320f..c26ed0c73 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -18,7 +18,7 @@ import { ModalComponent } from "src/components/Shared/Modal"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel"; @@ -264,7 +264,7 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { const intl = useIntl(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; diff --git a/ui/v2.5/src/components/Studios/StudioSelect.tsx b/ui/v2.5/src/components/Studios/StudioSelect.tsx index c62d25675..7305aa60d 100644 --- a/ui/v2.5/src/components/Studios/StudioSelect.tsx +++ b/ui/v2.5/src/components/Studios/StudioSelect.tsx @@ -13,7 +13,7 @@ import { queryFindStudiosByIDForSelect, queryFindStudiosForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown, IUIConfig } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -65,7 +65,7 @@ const _StudioSelect: React.FC< > = (props) => { const [createStudio] = useStudioCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = (configuration?.ui as IUIConfig).maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/Tagger/config.ts b/ui/v2.5/src/components/Tagger/config.ts index 78515f550..c30db7da2 100644 --- a/ui/v2.5/src/components/Tagger/config.ts +++ b/ui/v2.5/src/components/Tagger/config.ts @@ -1,10 +1,10 @@ -import { useCallback, useContext } from "react"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useCallback } from "react"; +import { useConfigurationContext } from "src/hooks/Config"; import { initialConfig, ITaggerConfig } from "./constants"; import { useConfigureUISetting } from "src/core/StashService"; export function useTaggerConfig() { - const { configuration: stashConfig } = useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [saveUISetting] = useConfigureUISetting(); const config = stashConfig?.ui.taggerConfig ?? initialConfig; diff --git a/ui/v2.5/src/components/Tagger/context.tsx b/ui/v2.5/src/components/Tagger/context.tsx index 0db1fba1e..028e83ed0 100644 --- a/ui/v2.5/src/components/Tagger/context.tsx +++ b/ui/v2.5/src/components/Tagger/context.tsx @@ -17,7 +17,7 @@ import { useTagCreate, } from "src/core/StashService"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ITaggerSource, SCRAPER_PREFIX, STASH_BOX_PREFIX } from "./constants"; import { errorToString } from "src/utils"; import { mergeStudioStashIDs } from "./utils"; @@ -117,7 +117,7 @@ export const TaggerContext: React.FC = ({ children }) => { const stopping = useRef(false); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const { config, setConfig } = useTaggerConfig(); const Scrapers = useListSceneScrapers(); diff --git a/ui/v2.5/src/components/Tagger/performers/Config.tsx b/ui/v2.5/src/components/Tagger/performers/Config.tsx index a839e1ae6..0d5316735 100644 --- a/ui/v2.5/src/components/Tagger/performers/Config.tsx +++ b/ui/v2.5/src/components/Tagger/performers/Config.tsx @@ -1,7 +1,7 @@ import React, { Dispatch, useState } from "react"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ITaggerConfig } from "../constants"; import PerformerFieldSelector from "../PerformerFieldSelector"; @@ -13,7 +13,7 @@ interface IConfigProps { } const Config: React.FC = ({ show, config, setConfig }) => { - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [showExclusionModal, setShowExclusionModal] = useState(false); const excludedFields = config.excludedPerformerFields ?? []; diff --git a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx index f0c87ff57..a6e2bcd1c 100755 --- a/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx +++ b/ui/v2.5/src/components/Tagger/performers/PerformerTagger.tsx @@ -16,7 +16,7 @@ import { performerMutationImpactedQueries, } from "src/core/StashService"; import { Manual } from "src/components/Help/Manual"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import StashSearchResult from "./StashSearchResult"; import PerformerConfig from "./Config"; @@ -620,7 +620,7 @@ interface ITaggerProps { export const PerformerTagger: React.FC = ({ performers }) => { const jobsSubscribe = useJobsSubscribe(); const intl = useIntl(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const { config, setConfig } = useTaggerConfig(); const [showConfig, setShowConfig] = useState(false); const [showManual, setShowManual] = useState(false); diff --git a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx index 922ecc473..695ed2817 100755 --- a/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/SceneTagger.tsx @@ -12,7 +12,7 @@ import Config from "./Config"; import { TaggerScene } from "./TaggerScene"; import { SceneTaggerModals } from "./sceneTaggerModals"; import { SceneSearchResults } from "./StashSearchResult"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { faCog } from "@fortawesome/free-solid-svg-icons"; import { useLightbox } from "src/hooks/Lightbox/hooks"; @@ -26,7 +26,7 @@ const Scene: React.FC<{ const intl = useIntl(); const { currentSource, doSceneQuery, doSceneFragmentScrape, loading } = useContext(TaggerStateContext); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const cont = configuration?.interface.continuePlaylistDefault ?? false; diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index db36bf404..4825ebcfd 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -19,7 +19,7 @@ import { faImage, } from "@fortawesome/free-solid-svg-icons"; import { objectPath, objectTitle } from "src/core/files"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { SceneQueue } from "src/models/sceneQueue"; interface ITaggerSceneDetails { @@ -154,7 +154,7 @@ export const TaggerScene: React.FC> = ({ const history = useHistory(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const cont = configuration?.interface.continuePlaylistDefault ?? false; async function query() { diff --git a/ui/v2.5/src/components/Tagger/studios/Config.tsx b/ui/v2.5/src/components/Tagger/studios/Config.tsx index 9dd9f6856..ddfd17b1e 100644 --- a/ui/v2.5/src/components/Tagger/studios/Config.tsx +++ b/ui/v2.5/src/components/Tagger/studios/Config.tsx @@ -1,7 +1,7 @@ import React, { Dispatch, useState } from "react"; import { Badge, Button, Card, Collapse, Form } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { ITaggerConfig } from "../constants"; import StudioFieldSelector from "./StudioFieldSelector"; @@ -13,7 +13,7 @@ interface IConfigProps { } const Config: React.FC = ({ show, config, setConfig }) => { - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [showExclusionModal, setShowExclusionModal] = useState(false); const excludedFields = config.excludedStudioFields ?? []; diff --git a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx index b8fbefdb5..78553e518 100644 --- a/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx +++ b/ui/v2.5/src/components/Tagger/studios/StudioTagger.tsx @@ -17,7 +17,7 @@ import { evictQueries, } from "src/core/StashService"; import { Manual } from "src/components/Help/Manual"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import StashSearchResult from "./StashSearchResult"; import StudioConfig from "./Config"; @@ -669,7 +669,7 @@ interface ITaggerProps { export const StudioTagger: React.FC = ({ studios }) => { const jobsSubscribe = useJobsSubscribe(); const intl = useIntl(); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const { config, setConfig } = useTaggerConfig(); const [showConfig, setShowConfig] = useState(false); const [showManual, setShowManual] = useState(false); diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 6d6a4a660..e0bc11e37 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -19,7 +19,7 @@ import { ModalComponent } from "src/components/Shared/Modal"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { Icon } from "src/components/Shared/Icon"; import { useToast } from "src/hooks/Toast"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { tagRelationHook } from "src/core/tags"; import { TagScenesPanel } from "./TagScenesPanel"; import { TagMarkersPanel } from "./TagMarkersPanel"; @@ -293,7 +293,7 @@ const TagPage: React.FC = ({ tag, tabKey }) => { const intl = useIntl(); // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const uiConfig = configuration?.ui; const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false; diff --git a/ui/v2.5/src/components/Tags/TagPopover.tsx b/ui/v2.5/src/components/Tags/TagPopover.tsx index 9e3f0d80b..ef3aa950a 100644 --- a/ui/v2.5/src/components/Tags/TagPopover.tsx +++ b/ui/v2.5/src/components/Tags/TagPopover.tsx @@ -4,7 +4,7 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { HoverPopover } from "../Shared/HoverPopover"; import { useFindTag } from "../../core/StashService"; import { TagCard } from "./TagCard"; -import { ConfigurationContext } from "../../hooks/Config"; +import { useConfigurationContext } from "../../hooks/Config"; import { Placement } from "react-bootstrap/esm/Overlay"; interface ITagPopoverCardProps { @@ -47,7 +47,7 @@ export const TagPopover: React.FC = ({ placement = "top", target, }) => { - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const showTagCardOnHover = config?.ui.showTagCardOnHover ?? true; diff --git a/ui/v2.5/src/components/Tags/TagSelect.tsx b/ui/v2.5/src/components/Tags/TagSelect.tsx index 9fdc57eaf..5b8da7a6d 100644 --- a/ui/v2.5/src/components/Tags/TagSelect.tsx +++ b/ui/v2.5/src/components/Tags/TagSelect.tsx @@ -13,7 +13,7 @@ import { queryFindTagsByIDForSelect, queryFindTagsForSelect, } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useIntl } from "react-intl"; import { defaultMaxOptionsShown } from "src/core/config"; import { ListFilterModel } from "src/models/list-filter/filter"; @@ -67,7 +67,7 @@ export type TagSelectProps = IFilterProps & const _TagSelect: React.FC = (props) => { const [createTag] = useTagCreate(); - const { configuration } = React.useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const intl = useIntl(); const maxOptionsShown = configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown; diff --git a/ui/v2.5/src/components/Wall/WallItem.tsx b/ui/v2.5/src/components/Wall/WallItem.tsx index 5811b7543..959ac1617 100644 --- a/ui/v2.5/src/components/Wall/WallItem.tsx +++ b/ui/v2.5/src/components/Wall/WallItem.tsx @@ -12,7 +12,7 @@ import TextUtils from "src/utils/text"; import NavUtils from "src/utils/navigation"; import cx from "classnames"; import { SceneQueue } from "src/models/sceneQueue"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { markerTitle } from "src/core/markers"; import { objectTitle } from "src/core/files"; @@ -128,7 +128,7 @@ export const WallItem = ({ }: IWallItemProps) => { const [active, setActive] = useState(false); const itemEl = useRef(null); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const showTextContainer = config?.interface.wallShowTitle ?? true; diff --git a/ui/v2.5/src/docs/en/Manual/Interface.md b/ui/v2.5/src/docs/en/Manual/Interface.md index 31c7e25d4..cf5911405 100644 --- a/ui/v2.5/src/docs/en/Manual/Interface.md +++ b/ui/v2.5/src/docs/en/Manual/Interface.md @@ -4,6 +4,15 @@ Setting the language affects the formatting of numbers and dates. +## SFW Content Mode + +SFW Content Mode is used to indicate that the content being managed is _not_ adult content. + +When SFW Content Mode is enabled, the following changes are made to the UI: +- default performer images are changed to less adult-oriented images +- certain adult-specific metadata fields are hidden (e.g. performer genital fields) +- `O`-Counter is replaced with `Like`-counter + ## Scene/Marker Wall Preview Type The Scene Wall and Marker pages display scene preview videos (mp4) by default. This can be changed to animated image (webp) or static image. diff --git a/ui/v2.5/src/hooks/Config.tsx b/ui/v2.5/src/hooks/Config.tsx index 0b00d0dc5..65ad7122a 100644 --- a/ui/v2.5/src/hooks/Config.tsx +++ b/ui/v2.5/src/hooks/Config.tsx @@ -2,14 +2,28 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; export interface IContext { - configuration?: GQL.ConfigDataFragment; - loading?: boolean; + configuration: GQL.ConfigDataFragment; } -export const ConfigurationContext = React.createContext({}); +export const ConfigurationContext = React.createContext(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 = ({ - loading, configuration, children, }) => { @@ -17,7 +31,6 @@ export const ConfigurationProvider: React.FC = ({ {children} diff --git a/ui/v2.5/src/hooks/Interactive/context.tsx b/ui/v2.5/src/hooks/Interactive/context.tsx index 9e7194d6a..ccdc948b4 100644 --- a/ui/v2.5/src/hooks/Interactive/context.tsx +++ b/ui/v2.5/src/hooks/Interactive/context.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useContext, useEffect, useState } from "react"; -import { ConfigurationContext } from "../Config"; +import { useConfigurationContext } from "../Config"; import { useLocalForage } from "../LocalForage"; import { Interactive as InteractiveAPI } from "./interactive"; import InteractiveUtils, { @@ -86,7 +86,7 @@ export const InteractiveProvider: React.FC = ({ children }) => { { serverOffset: 0, lastSyncTime: 0 } ); - const { configuration: stashConfig } = React.useContext(ConfigurationContext); + const { configuration: stashConfig } = useConfigurationContext(); const [state, setState] = useState(ConnectionState.Missing); const [handyKey, setHandyKey] = useState(undefined); diff --git a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx index 5619275ff..f0f057d86 100644 --- a/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx +++ b/ui/v2.5/src/hooks/Lightbox/Lightbox.tsx @@ -19,7 +19,7 @@ import usePageVisibility from "../PageVisibility"; import { useToast } from "../Toast"; import { FormattedMessage, useIntl } from "react-intl"; import { LightboxImage } from "./LightboxImage"; -import { ConfigurationContext } from "../Config"; +import { useConfigurationContext } from "../Config"; import { Link } from "react-router-dom"; import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton"; import { @@ -154,7 +154,7 @@ export const LightboxComponent: React.FC = ({ const Toast = useToast(); const intl = useIntl(); - const { configuration: config } = React.useContext(ConfigurationContext); + const { configuration: config } = useConfigurationContext(); const [interfaceLocalForage, setInterfaceLocalForage] = useInterfaceLocalForage(); diff --git a/ui/v2.5/src/hooks/useTableColumns.ts b/ui/v2.5/src/hooks/useTableColumns.ts index 09d5357d2..ed6380bdb 100644 --- a/ui/v2.5/src/hooks/useTableColumns.ts +++ b/ui/v2.5/src/hooks/useTableColumns.ts @@ -1,6 +1,5 @@ -import { useContext } from "react"; import { useConfigureUI } from "src/core/StashService"; -import { ConfigurationContext } from "src/hooks/Config"; +import { useConfigurationContext } from "src/hooks/Config"; import { useToast } from "./Toast"; export const useTableColumns = ( @@ -9,7 +8,7 @@ export const useTableColumns = ( ) => { const Toast = useToast(); - const { configuration } = useContext(ConfigurationContext); + const { configuration } = useConfigurationContext(); const [saveUI] = useConfigureUI(); const ui = configuration?.ui; diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index eedc84c01..74599eb34 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -10,6 +10,7 @@ $sidebar-width: 250px; @import "styles/theme"; @import "styles/range"; @import "styles/scrollbars"; +@import "sfw-mode.scss"; @import "src/components/Changelog/styles.scss"; @import "src/components/Galleries/styles.scss"; @import "src/components/Help/styles.scss"; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 08297727a..1adcd7671 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -784,6 +784,10 @@ "description": "Number of times to attempt to scroll before moving to the next/previous item. Only applies for Pan Y scroll mode.", "heading": "Scroll attempts before transition" }, + "sfw_mode": { + "description": "Enable if using stash to store SFW content. Hides or changes some adult-content-related aspects of the UI.", + "heading": "SFW Content Mode" + }, "show_tag_card_on_hover": { "description": "Show tag card when hovering tag badges", "heading": "Tag card tooltips" @@ -897,6 +901,7 @@ "developmentVersion": "Development Version", "dialogs": { "clear_o_history_confirm": "Are you sure you want to clear the O history?", + "clear_o_history_confirm_sfw": "Are you sure you want to clear the like history?", "clear_play_history_confirm": "Are you sure you want to clear the play history?", "create_new_entity": "Create new {entity}", "delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:", @@ -1158,6 +1163,7 @@ "interactive_speed": "Interactive Speed", "isMissing": "Is Missing", "last_o_at": "Last O At", + "last_o_at_sfw": "Last Like At", "last_played_at": "Last Played At", "library": "Library", "loading": { @@ -1198,9 +1204,11 @@ "new": "New", "none": "None", "o_count": "O Count", - "o_counter": "O-Counter", + "o_count_sfw": "Likes", "o_history": "O History", + "o_history_sfw": "Like History", "odate_recorded_no": "No O Date Recorded", + "odate_recorded_no_sfw": "No Like Date Recorded", "operations": "Operations", "organized": "Organised", "orientation": "Orientation", @@ -1377,25 +1385,28 @@ }, "paths": { "database_filename_empty_for_default": "database filename (empty for default)", - "description": "Next up, we need to determine where to find your porn collection, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.", + "description": "Next up, we need to determine where to find your content, and where to store the Stash database, generated files and cache files. These settings can be changed later if needed.", "path_to_blobs_directory_empty_for_default": "path to blobs directory (empty for default)", "path_to_cache_directory_empty_for_default": "path to cache directory (empty for default)", "path_to_generated_directory_empty_for_default": "path to generated directory (empty for default)", "set_up_your_paths": "Set up your paths", + "sfw_content_settings": "Using stash for SFW content?", + "sfw_content_settings_description": "stash can be used to manage SFW content such as photography, art, comics, and more. Enabling this option will adjust some UI behaviour to be more appropriate for SFW content.", "stash_alert": "No library paths have been selected. No media will be able to be scanned into Stash. Are you sure?", "store_blobs_in_database": "Store blobs in database", + "use_sfw_content_mode": "Use SFW content mode", "where_can_stash_store_blobs": "Where can Stash store database binary data?", "where_can_stash_store_blobs_description": "Stash can store binary data such as scene covers, performer, studio and tag images either in the database or in the filesystem. By default, it will store this data in the filesystem in the subdirectory blobs 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. Note: This will increase the size of your database file, and will increase database migration times.", "where_can_stash_store_cache_files": "Where can Stash store cache files?", "where_can_stash_store_cache_files_description": "In order for some functionality like HLS/DASH live transcoding to function, Stash requires a cache directory for temporary files. By default, Stash will create a cache directory within the directory containing your config file. If you want to change this, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", "where_can_stash_store_its_database": "Where can Stash store its database?", - "where_can_stash_store_its_database_description": "Stash uses an SQLite database to store your porn metadata. By default, this will be created as stash-go.sqlite 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 stash-go.sqlite 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 unsupported! SQLite is not intended for use across a network, and attempting to do so can very easily cause your entire database to become corrupted.", "where_can_stash_store_its_generated_content": "Where can Stash store its generated content?", "where_can_stash_store_its_generated_content_description": "In order to provide thumbnails, previews and sprites, Stash generates images and videos. This also includes transcodes for unsupported file formats. By default, Stash will create a generated directory within the directory containing your config file. If you want to change where this generated media will be stored, please enter an absolute or relative (to the current working directory) path. Stash will create this directory if it does not already exist.", - "where_is_your_porn_located": "Where is your porn located?", - "where_is_your_porn_located_description": "Add directories containing your porn videos and images. Stash will use these directories to find videos and images during scanning." + "where_is_your_porn_located": "Where is your content located?", + "where_is_your_porn_located_description": "Add directories containing your videos and images. Stash will use these directories to find videos and images during scanning." }, "stash_setup_wizard": "Stash Setup Wizard", "success": { diff --git a/ui/v2.5/src/models/list-filter/criteria/criterion.ts b/ui/v2.5/src/models/list-filter/criteria/criterion.ts index a4d3a145c..8f30e5d17 100644 --- a/ui/v2.5/src/models/list-filter/criteria/criterion.ts +++ b/ui/v2.5/src/models/list-filter/criteria/criterion.ts @@ -78,7 +78,7 @@ export abstract class Criterion { protected cloneValues() {} - public abstract getLabel(intl: IntlShape): string; + public abstract getLabel(intl: IntlShape, sfwContentMode?: boolean): string; public getId(): string { return `${this.criterionOption.type}`; @@ -148,7 +148,7 @@ export abstract class ModifierCriterion< : ""; } - public getLabel(intl: IntlShape): string { + public getLabel(intl: IntlShape, sfwContentMode: boolean = false): string { const modifierString = ModifierCriterion.getModifierLabel( intl, this.modifier @@ -162,10 +162,14 @@ export abstract class ModifierCriterion< valueString = this.getLabelValue(intl); } + const messageID = !sfwContentMode + ? this.criterionOption.messageID + : this.criterionOption.sfwMessageID ?? this.criterionOption.messageID; + return intl.formatMessage( { id: "criterion_modifier.format_string" }, { - criterion: intl.formatMessage({ id: this.criterionOption.messageID }), + criterion: intl.formatMessage({ id: messageID }), modifierString, valueString, } @@ -257,12 +261,14 @@ interface ICriterionOptionParams { type: CriterionType; makeCriterion: MakeCriterionFn; hidden?: boolean; + sfwMessageID?: string; } export class CriterionOption { public readonly type: CriterionType; public readonly messageID: string; public readonly makeCriterionFn: MakeCriterionFn; + public readonly sfwMessageID?: string; // used for legacy criteria that are not shown in the UI public readonly hidden: boolean = false; @@ -272,6 +278,7 @@ export class CriterionOption { this.messageID = options.messageID; this.makeCriterionFn = options.makeCriterion; this.hidden = options.hidden ?? false; + this.sfwMessageID = options.sfwMessageID; } public makeCriterion(config?: ConfigDataFragment) { @@ -478,7 +485,7 @@ export class IHierarchicalLabeledIdCriterion extends ModifierCriterion ModifierCriterion + makeCriterion?: () => ModifierCriterion, + options?: { sfwMessageID?: string } ) { super({ messageID, @@ -773,15 +790,22 @@ export class MandatoryNumberCriterionOption extends ModifierCriterionOption { makeCriterion: makeCriterion ? makeCriterion : () => new NumberCriterion(this), + ...options, }); } } export function createMandatoryNumberCriterionOption( value: CriterionType, - messageID?: string + messageID?: string, + options?: { sfwMessageID?: string } ) { - return new MandatoryNumberCriterionOption(messageID ?? value, value); + return new MandatoryNumberCriterionOption( + messageID ?? value, + value, + undefined, + options + ); } export function encodeRangeValue( diff --git a/ui/v2.5/src/models/list-filter/filter-options.ts b/ui/v2.5/src/models/list-filter/filter-options.ts index 32b86e786..a63394f35 100644 --- a/ui/v2.5/src/models/list-filter/filter-options.ts +++ b/ui/v2.5/src/models/list-filter/filter-options.ts @@ -4,6 +4,7 @@ import { DisplayMode } from "./types"; export interface ISortByOption { messageID: string; value: string; + sfwMessageID?: string; } export const MediaSortByOptions = [ @@ -22,7 +23,7 @@ export class ListFilterOptions { public readonly displayModeOptions: DisplayMode[] = []; public readonly criterionOptions: CriterionOption[] = []; - public static createSortBy(value: string) { + public static createSortBy(value: string): ISortByOption { return { messageID: value, value, diff --git a/ui/v2.5/src/models/list-filter/groups.ts b/ui/v2.5/src/models/list-filter/groups.ts index 6aed48fdc..5a263b272 100644 --- a/ui/v2.5/src/models/list-filter/groups.ts +++ b/ui/v2.5/src/models/list-filter/groups.ts @@ -38,6 +38,7 @@ const sortByOptions = [ { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", }, ]); const displayModeOptions = [DisplayMode.Grid]; @@ -53,7 +54,9 @@ const criterionOptions = [ RatingCriterionOption, PerformersCriterionOption, createDateCriterionOption("date"), - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), ContainingGroupsCriterionOption, SubGroupsCriterionOption, createMandatoryNumberCriterionOption("containing_group_count"), diff --git a/ui/v2.5/src/models/list-filter/images.ts b/ui/v2.5/src/models/list-filter/images.ts index d8619112d..4d5630b1c 100644 --- a/ui/v2.5/src/models/list-filter/images.ts +++ b/ui/v2.5/src/models/list-filter/images.ts @@ -31,6 +31,7 @@ const sortByOptions = ["filesize", "file_count", "date", ...MediaSortByOptions] { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", }, ]); const displayModeOptions = [DisplayMode.Grid, DisplayMode.Wall]; @@ -43,7 +44,9 @@ const criterionOptions = [ PathCriterionOption, GalleriesCriterionOption, OrganizedCriterionOption, - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), ResolutionCriterionOption, OrientationCriterionOption, ImageIsMissingCriterionOption, diff --git a/ui/v2.5/src/models/list-filter/performers.ts b/ui/v2.5/src/models/list-filter/performers.ts index 2cb3ef216..fcc152d01 100644 --- a/ui/v2.5/src/models/list-filter/performers.ts +++ b/ui/v2.5/src/models/list-filter/performers.ts @@ -31,7 +31,6 @@ const sortByOptions = [ "penis_length", "play_count", "last_played_at", - "last_o_at", "career_length", "weight", "measurements", @@ -54,6 +53,12 @@ const sortByOptions = [ { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", + }, + { + messageID: "last_o_at", + value: "last_o_at", + sfwMessageID: "last_o_at_sfw", }, ]); @@ -102,7 +107,9 @@ const criterionOptions = [ createMandatoryNumberCriterionOption("image_count"), createMandatoryNumberCriterionOption("gallery_count"), createMandatoryNumberCriterionOption("play_count"), - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), createBooleanCriterionOption("ignore_auto_tag"), CountryCriterionOption, createNumberCriterionOption("height_cm", "height"), diff --git a/ui/v2.5/src/models/list-filter/scenes.ts b/ui/v2.5/src/models/list-filter/scenes.ts index b8dd6515a..cf2791567 100644 --- a/ui/v2.5/src/models/list-filter/scenes.ts +++ b/ui/v2.5/src/models/list-filter/scenes.ts @@ -46,7 +46,6 @@ const sortByOptions = [ "framerate", "bitrate", "last_played_at", - "last_o_at", "resume_time", "play_duration", "play_count", @@ -62,6 +61,12 @@ const sortByOptions = [ { messageID: "o_count", value: "o_counter", + sfwMessageID: "o_count_sfw", + }, + { + messageID: "last_o_at", + value: "last_o_at", + sfwMessageID: "last_o_at_sfw", }, { messageID: "group_scene_number", @@ -97,7 +102,9 @@ const criterionOptions = [ DuplicatedCriterionOption, OrganizedCriterionOption, RatingCriterionOption, - createMandatoryNumberCriterionOption("o_counter", "o_count"), + createMandatoryNumberCriterionOption("o_counter", "o_count", { + sfwMessageID: "o_count_sfw", + }), ResolutionCriterionOption, OrientationCriterionOption, createMandatoryNumberCriterionOption("framerate"), diff --git a/ui/v2.5/src/sfw-mode.scss b/ui/v2.5/src/sfw-mode.scss new file mode 100644 index 000000000..9883afb4b --- /dev/null +++ b/ui/v2.5/src/sfw-mode.scss @@ -0,0 +1,91 @@ +// hide nsfw elements when in sfw-content mode +// stylelint-disable selector-class-pattern +.sfw-content-mode { + // hide adult-oriented performer fields in sort by select + .sort-by-select, + .performer-table { + [data-value="ethnicity"], + [data-value="hair_color"], + [data-value="eye_color"], + [data-value="measurements"], + [data-value="weight"], + [data-value="weight_kg"], + [data-value="penis_length"], + [data-value="penis_length_cm"], + [data-value="circumcised"], + [data-value="fake_tits"] { + display: none; + } + } + + .performer-table { + td, + th { + &.ethnicity, + &.hair_color, + &.eye_color, + &.height, + &.measurements, + &.weight_kg, + &.penis_length_cm, + &.circumcised, + &.fake_tits { + &-head, + &-data { + display: none; + } + } + } + } + + #performer-edit, + &.scrape-dialog { + [data-field="ethnicity"], + [data-field="hair_color"], + [data-field="eye_color"], + [data-field="measurements"], + [data-field="weight"], + [data-field="penis_length"], + [data-field="circumcised"], + [data-field="fake_tits"], + [data-field="tattoos"], + [data-field="piercings"] { + display: none; + } + } + + &.edit-filter-dialog { + [data-type="ethnicity"], + [data-type="hair_color"], + [data-type="eye_color"], + [data-type="measurements"], + [data-type="weight"], + [data-type="penis_length"], + [data-type="circumcised"], + [data-type="fake_tits"], + [data-type="tattoos"], + [data-type="piercings"] { + display: none; + } + } + + #performer-page { + .detail-item.ethnicity, + .detail-item.hair_color, + .detail-item.eye_color, + .detail-item.measurements, + .detail-item.weight, + .detail-item.penis_length, + .detail-item.circumcised, + .detail-item.fake_tits, + .detail-item.tattoos, + .detail-item.piercings { + display: none; + } + } + + // hide performer age on performer cards + .performer-card__age { + display: none; + } +} diff --git a/ui/v2.5/src/utils/form.tsx b/ui/v2.5/src/utils/form.tsx index f518d5700..e11d885f8 100644 --- a/ui/v2.5/src/utils/form.tsx +++ b/ui/v2.5/src/utils/form.tsx @@ -103,10 +103,14 @@ export function formikUtils( }, }: IProps = {} ) { - type Field = keyof V & string; + type FieldName = keyof V & string; type ErrorMessage = string | undefined; - function renderFormControl(field: Field, type: string, placeholder: string) { + function renderFormControl( + field: FieldName, + type: string, + placeholder: string + ) { const formikProps = formik.getFieldProps({ name: field, type: type }); const error = formik.errors[field] as ErrorMessage; @@ -168,38 +172,90 @@ export function formikUtils( ); } - function renderField( - field: Field, - title: string, - control: React.ReactNode, - props?: IProps - ) { + const FieldGroup: React.FC<{ + field: FieldName; + title: string; + control: React.ReactNode; + props?: IProps; + className?: string; + }> = ({ field, title, control, props, className }) => { return ( - + {title} {control} ); + }; + + function renderField( + field: FieldName, + title: string, + control: React.ReactNode, + props?: IProps, + className?: string + ) { + return ( + + ); } - function renderInputField( - field: Field, - type: string = "text", - messageID: string = field, - props?: IProps - ) { + const InputFieldGroup: React.FC<{ + field: FieldName; + type?: string; + messageID?: string; + props?: IProps; + className?: string; + }> = ({ field, type = "text", messageID = field, props, className }) => { const title = intl.formatMessage({ id: messageID }); const control = renderFormControl(field, type, title); - return renderField(field, title, control, props); + return ( + + ); + }; + + function renderInputField( + field: FieldName, + type: string = "text", + messageID: string = field, + props?: IProps, + className?: string + ) { + return ( + + ); } - function renderSelectField( - field: Field, - entries: Map, - messageID: string = field, - props?: IProps - ) { + const SelectFieldGroup: React.FC<{ + field: FieldName; + className?: string; + entries: Map; + messageID?: string; + props?: IProps; + }> = ({ field, className, entries, messageID = field, props }) => { const formikProps = formik.getFieldProps(field); let { value } = formikProps; @@ -224,11 +280,35 @@ export function formikUtils( ); - return renderField(field, title, control, props); + return ( + + ); + }; + + function renderSelectField( + field: FieldName, + entries: Map, + messageID: string = field, + props?: IProps + ) { + return ( + + ); } function renderDateField( - field: Field, + field: FieldName, messageID: string = field, props?: IProps ) { @@ -248,7 +328,7 @@ export function formikUtils( } function renderDurationField( - field: Field, + field: FieldName, messageID: string = field, props?: IProps ) { @@ -268,7 +348,7 @@ export function formikUtils( } function renderRatingField( - field: Field, + field: FieldName, messageID: string = field, props?: IProps ) { @@ -309,7 +389,7 @@ export function formikUtils( } function renderStringListField( - field: Field, + field: FieldName, messageID: string = field, props?: IProps ) { @@ -332,7 +412,7 @@ export function formikUtils( } function renderURLListField( - field: Field, + field: FieldName, onScrapeClick?: (url: string) => void, urlScrapable?: (url: string) => boolean, messageID: string = field, @@ -359,7 +439,7 @@ export function formikUtils( } function renderStashIDsField( - field: Field, + field: FieldName, linkType: LinkType, messageID: string = field, props?: IProps @@ -405,8 +485,11 @@ export function formikUtils( return { renderFormControl, renderField, + FieldGroup, renderInputField, + InputFieldGroup, renderSelectField, + SelectFieldGroup, renderDateField, renderDurationField, renderRatingField,