mirror of
https://github.com/stashapp/stash.git
synced 2025-12-16 20:07:05 +03:00
Plugin api improvements (#5703)
* Add ReactSelect to PluginApi.libraries * Make Performer tabs patchable * Make PerformerCard patchable * Use registration pattern for HoverPopover, TagLink and LoadingIndicator Initialising the components map to include these was causing an initialisation error. * Add showZero property to PopoverCountButton * Make TagCard patchable * Make ScenePage and ScenePlayer patchable * Pass properties to container components * Add example for scene tabs * Make FrontPage patchable * Add FrontPage example
This commit is contained in:
@@ -14,7 +14,11 @@ interface IPluginApi {
|
||||
Button: React.FC<any>;
|
||||
Nav: React.FC<any> & {
|
||||
Link: React.FC<any>;
|
||||
Item: React.FC<any>;
|
||||
};
|
||||
Tab: React.FC<any> & {
|
||||
Pane: React.FC<any>;
|
||||
}
|
||||
},
|
||||
FontAwesomeSolid: {
|
||||
faEthernet: any;
|
||||
@@ -45,7 +49,7 @@ interface IPluginApi {
|
||||
const React = PluginApi.React;
|
||||
const GQL = PluginApi.GQL;
|
||||
|
||||
const { Button } = PluginApi.libraries.Bootstrap;
|
||||
const { Button, Nav, Tab } = PluginApi.libraries.Bootstrap;
|
||||
const { faEthernet } = PluginApi.libraries.FontAwesomeSolid;
|
||||
const {
|
||||
Link,
|
||||
@@ -144,6 +148,10 @@ interface IPluginApi {
|
||||
return <><Overlays />{original({...props})}</>;
|
||||
});
|
||||
|
||||
PluginApi.patch.instead("FrontPage", function (props: any, _: any, original: (props: any) => any) {
|
||||
return <><p>Hello from Test React!</p>{original({...props})}</>;
|
||||
});
|
||||
|
||||
const TestPage: React.FC = () => {
|
||||
const componentsToLoad = [
|
||||
PluginApi.loadableComponents.SceneCard,
|
||||
@@ -237,5 +245,37 @@ interface IPluginApi {
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
});
|
||||
|
||||
PluginApi.patch.before("ScenePage.Tabs", function (props: any) {
|
||||
return [
|
||||
{
|
||||
children: (
|
||||
<>
|
||||
{props.children}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="test-react-tab">
|
||||
Test React tab
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
PluginApi.patch.before("ScenePage.TabContent", function (props: any) {
|
||||
return [
|
||||
{
|
||||
children: (
|
||||
<>
|
||||
{props.children}
|
||||
<Tab.Pane eventKey="test-react-tab">
|
||||
Test React tab content {props.scene.id}
|
||||
</Tab.Pane>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
});
|
||||
})();
|
||||
@@ -13,8 +13,9 @@ import {
|
||||
getFrontPageContent,
|
||||
} from "src/core/config";
|
||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
const FrontPage: React.FC = () => {
|
||||
const FrontPage: React.FC = PatchComponent("FrontPage", () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
@@ -81,6 +82,6 @@ const FrontPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export default FrontPage;
|
||||
|
||||
@@ -23,6 +23,7 @@ import { usePerformerUpdate } from "src/core/StashService";
|
||||
import { ILabeledId } from "src/models/list-filter/types";
|
||||
import ScreenUtils from "src/utils/screen";
|
||||
import { FavoriteIcon } from "../Shared/FavoriteIcon";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
export interface IPerformerCardExtraCriteria {
|
||||
scenes?: ModifierCriterion<CriterionValue>[];
|
||||
@@ -43,176 +44,109 @@ interface IPerformerCardProps {
|
||||
extraCriteria?: IPerformerCardExtraCriteria;
|
||||
}
|
||||
|
||||
export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
performer,
|
||||
containerWidth,
|
||||
ageFromDate,
|
||||
selecting,
|
||||
selected,
|
||||
zoomIndex,
|
||||
onSelectedChanged,
|
||||
extraCriteria,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const age = TextUtils.age(
|
||||
performer.birthdate,
|
||||
ageFromDate ?? performer.death_date
|
||||
);
|
||||
const ageL10nId = ageFromDate
|
||||
? "media_info.performer_card.age_context"
|
||||
: "media_info.performer_card.age";
|
||||
const ageL10String = intl.formatMessage({
|
||||
id: "years_old",
|
||||
defaultMessage: "years old",
|
||||
});
|
||||
const ageString = intl.formatMessage(
|
||||
{ id: ageL10nId },
|
||||
{ age, years_old: ageL10String }
|
||||
);
|
||||
const PerformerCardPopovers: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard.Popovers",
|
||||
({ performer, extraCriteria }) => {
|
||||
function maybeRenderScenesPopoverButton() {
|
||||
if (!performer.scene_count) return;
|
||||
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [cardWidth, setCardWidth] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerWidth || zoomIndex === undefined || ScreenUtils.isMobile())
|
||||
return;
|
||||
|
||||
let zoomValue = zoomIndex;
|
||||
let preferredCardWidth: number;
|
||||
switch (zoomValue) {
|
||||
case 0:
|
||||
preferredCardWidth = 240;
|
||||
break;
|
||||
case 1:
|
||||
preferredCardWidth = 300;
|
||||
break;
|
||||
case 2:
|
||||
preferredCardWidth = 375;
|
||||
break;
|
||||
case 3:
|
||||
preferredCardWidth = 470;
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="scene-count"
|
||||
type="scene"
|
||||
count={performer.scene_count}
|
||||
url={NavUtils.makePerformerScenesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.scenes
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
let fittedCardWidth = calculateCardWidth(
|
||||
containerWidth,
|
||||
preferredCardWidth!
|
||||
);
|
||||
setCardWidth(fittedCardWidth);
|
||||
}, [containerWidth, zoomIndex]);
|
||||
|
||||
function onToggleFavorite(v: boolean) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
function maybeRenderImagesPopoverButton() {
|
||||
if (!performer.image_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="image-count"
|
||||
type="image"
|
||||
count={performer.image_count}
|
||||
url={NavUtils.makePerformerImagesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.images
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderScenesPopoverButton() {
|
||||
if (!performer.scene_count) return;
|
||||
function maybeRenderGalleriesPopoverButton() {
|
||||
if (!performer.gallery_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="scene-count"
|
||||
type="scene"
|
||||
count={performer.scene_count}
|
||||
url={NavUtils.makePerformerScenesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.scenes
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="gallery-count"
|
||||
type="gallery"
|
||||
count={performer.gallery_count}
|
||||
url={NavUtils.makePerformerGalleriesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.galleries
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderImagesPopoverButton() {
|
||||
if (!performer.image_count) return;
|
||||
function maybeRenderOCounter() {
|
||||
if (!performer.o_counter) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="image-count"
|
||||
type="image"
|
||||
count={performer.image_count}
|
||||
url={NavUtils.makePerformerImagesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.images
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="o-counter">
|
||||
<Button className="minimal">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{performer.o_counter}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderGalleriesPopoverButton() {
|
||||
if (!performer.gallery_count) return;
|
||||
function maybeRenderTagPopoverButton() {
|
||||
if (performer.tags.length <= 0) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="gallery-count"
|
||||
type="gallery"
|
||||
count={performer.gallery_count}
|
||||
url={NavUtils.makePerformerGalleriesUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.galleries
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const popoverContent = performer.tags.map((tag) => (
|
||||
<TagLink key={tag.id} linkType="performer" tag={tag} />
|
||||
));
|
||||
|
||||
function maybeRenderOCounter() {
|
||||
if (!performer.o_counter) return;
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal tag-count">
|
||||
<Icon icon={faTag} />
|
||||
<span>{performer.tags.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="o-counter">
|
||||
<Button className="minimal">
|
||||
<span className="fa-icon">
|
||||
<SweatDrops />
|
||||
</span>
|
||||
<span>{performer.o_counter}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function maybeRenderGroupsPopoverButton() {
|
||||
if (!performer.group_count) return;
|
||||
|
||||
function maybeRenderTagPopoverButton() {
|
||||
if (performer.tags.length <= 0) return;
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="group-count"
|
||||
type="group"
|
||||
count={performer.group_count}
|
||||
url={NavUtils.makePerformerGroupsUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.groups
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const popoverContent = performer.tags.map((tag) => (
|
||||
<TagLink key={tag.id} linkType="performer" tag={tag} />
|
||||
));
|
||||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button className="minimal tag-count">
|
||||
<Icon icon={faTag} />
|
||||
<span>{performer.tags.length}</span>
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderGroupsPopoverButton() {
|
||||
if (!performer.group_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="group-count"
|
||||
type="group"
|
||||
count={performer.group_count}
|
||||
url={NavUtils.makePerformerGroupsUrl(
|
||||
performer,
|
||||
extraCriteria?.performer,
|
||||
extraCriteria?.groups
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
if (
|
||||
performer.scene_count ||
|
||||
performer.image_count ||
|
||||
@@ -235,85 +169,189 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderRatingBanner() {
|
||||
if (!performer.rating100) {
|
||||
return;
|
||||
}
|
||||
return <RatingBanner rating={performer.rating100} />;
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
function maybeRenderFlag() {
|
||||
if (performer.country) {
|
||||
return (
|
||||
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
|
||||
<CountryFlag
|
||||
className="performer-card__country-flag"
|
||||
country={performer.country}
|
||||
includeOverlay
|
||||
/>
|
||||
<span className="performer-card__country-string">
|
||||
{performer.country}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
const PerformerCardOverlays: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard.Overlays",
|
||||
({ performer }) => {
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
|
||||
return (
|
||||
<GridCard
|
||||
className={`performer-card zoom-${zoomIndex}`}
|
||||
url={`/performers/${performer.id}`}
|
||||
width={cardWidth}
|
||||
pretitleIcon={
|
||||
<GenderIcon className="gender-icon" gender={performer.gender} />
|
||||
function onToggleFavorite(v: boolean) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
title={
|
||||
<div>
|
||||
<span className="performer-name">{performer.name}</span>
|
||||
{performer.disambiguation && (
|
||||
<span className="performer-disambiguation">
|
||||
{` (${performer.disambiguation})`}
|
||||
}
|
||||
|
||||
function maybeRenderRatingBanner() {
|
||||
if (!performer.rating100) {
|
||||
return;
|
||||
}
|
||||
return <RatingBanner rating={performer.rating100} />;
|
||||
}
|
||||
|
||||
function maybeRenderFlag() {
|
||||
if (performer.country) {
|
||||
return (
|
||||
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
|
||||
<CountryFlag
|
||||
className="performer-card__country-flag"
|
||||
country={performer.country}
|
||||
includeOverlay
|
||||
/>
|
||||
<span className="performer-card__country-string">
|
||||
{performer.country}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
image={
|
||||
<>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="performer-card-image"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FavoriteIcon
|
||||
favorite={performer.favorite}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
size="2x"
|
||||
className="hide-not-favorite"
|
||||
/>
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderFlag()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const PerformerCardDetails: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard.Details",
|
||||
({ performer, ageFromDate }) => {
|
||||
const intl = useIntl();
|
||||
const age = TextUtils.age(
|
||||
performer.birthdate,
|
||||
ageFromDate ?? performer.death_date
|
||||
);
|
||||
const ageL10nId = ageFromDate
|
||||
? "media_info.performer_card.age_context"
|
||||
: "media_info.performer_card.age";
|
||||
const ageL10String = intl.formatMessage({
|
||||
id: "years_old",
|
||||
defaultMessage: "years old",
|
||||
});
|
||||
const ageString = intl.formatMessage(
|
||||
{ id: ageL10nId },
|
||||
{ age, years_old: ageL10String }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{age !== 0 ? (
|
||||
<div className="performer-card__age">{ageString}</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const PerformerCardImage: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard.Image",
|
||||
({ performer }) => {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="performer-card-image"
|
||||
alt={performer.name ?? ""}
|
||||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const PerformerCardTitle: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard.Title",
|
||||
({ performer }) => {
|
||||
return (
|
||||
<div>
|
||||
<span className="performer-name">{performer.name}</span>
|
||||
{performer.disambiguation && (
|
||||
<span className="performer-disambiguation">
|
||||
{` (${performer.disambiguation})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export const PerformerCard: React.FC<IPerformerCardProps> = PatchComponent(
|
||||
"PerformerCard",
|
||||
(props) => {
|
||||
const {
|
||||
performer,
|
||||
containerWidth,
|
||||
selecting,
|
||||
selected,
|
||||
onSelectedChanged,
|
||||
zoomIndex,
|
||||
} = props;
|
||||
|
||||
const [cardWidth, setCardWidth] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerWidth || zoomIndex === undefined || ScreenUtils.isMobile())
|
||||
return;
|
||||
|
||||
let zoomValue = zoomIndex;
|
||||
let preferredCardWidth: number;
|
||||
switch (zoomValue) {
|
||||
case 0:
|
||||
preferredCardWidth = 240;
|
||||
break;
|
||||
case 1:
|
||||
preferredCardWidth = 300;
|
||||
break;
|
||||
case 2:
|
||||
preferredCardWidth = 375;
|
||||
break;
|
||||
case 3:
|
||||
preferredCardWidth = 470;
|
||||
}
|
||||
overlays={
|
||||
<>
|
||||
<FavoriteIcon
|
||||
favorite={performer.favorite}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
size="2x"
|
||||
className="hide-not-favorite"
|
||||
/>
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderFlag()}
|
||||
</>
|
||||
}
|
||||
details={
|
||||
<>
|
||||
{age !== 0 ? (
|
||||
<div className="performer-card__age">{ageString}</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</>
|
||||
}
|
||||
popovers={maybeRenderPopoverButtonGroup()}
|
||||
selected={selected}
|
||||
selecting={selecting}
|
||||
onSelectedChanged={onSelectedChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
let fittedCardWidth = calculateCardWidth(
|
||||
containerWidth,
|
||||
preferredCardWidth!
|
||||
);
|
||||
setCardWidth(fittedCardWidth);
|
||||
}, [containerWidth, zoomIndex]);
|
||||
|
||||
return (
|
||||
<GridCard
|
||||
className={`performer-card zoom-${zoomIndex}`}
|
||||
url={`/performers/${performer.id}`}
|
||||
width={cardWidth}
|
||||
pretitleIcon={
|
||||
<GenderIcon className="gender-icon" gender={performer.gender} />
|
||||
}
|
||||
title={<PerformerCardTitle {...props} />}
|
||||
image={<PerformerCardImage {...props} />}
|
||||
overlays={<PerformerCardOverlays {...props} />}
|
||||
details={<PerformerCardDetails {...props} />}
|
||||
popovers={<PerformerCardPopovers {...props} />}
|
||||
selected={selected}
|
||||
selecting={selecting}
|
||||
onSelectedChanged={onSelectedChanged}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -45,6 +45,7 @@ import { FavoriteIcon } from "src/components/Shared/FavoriteIcon";
|
||||
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";
|
||||
|
||||
interface IProps {
|
||||
performer: GQL.PerformerDataFragment;
|
||||
@@ -200,272 +201,277 @@ const PerformerTabs: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
const PerformerPage: React.FC<IProps> = PatchComponent(
|
||||
"PerformerPage",
|
||||
({ performer, tabKey }) => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
const intl = useIntl();
|
||||
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const uiConfig = configuration?.ui;
|
||||
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||
const enableBackgroundImage =
|
||||
uiConfig?.enablePerformerBackgroundImage ?? false;
|
||||
const showAllDetails = uiConfig?.showAllDetails ?? true;
|
||||
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const uiConfig = configuration?.ui;
|
||||
const abbreviateCounter = uiConfig?.abbreviateCounters ?? false;
|
||||
const enableBackgroundImage =
|
||||
uiConfig?.enablePerformerBackgroundImage ?? false;
|
||||
const showAllDetails = uiConfig?.showAllDetails ?? true;
|
||||
const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false;
|
||||
|
||||
const [collapsed, setCollapsed] = useState<boolean>(!showAllDetails);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
const loadStickyHeader = useLoadStickyHeader();
|
||||
const [collapsed, setCollapsed] = useState<boolean>(!showAllDetails);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||
const loadStickyHeader = useLoadStickyHeader();
|
||||
|
||||
const activeImage = useMemo(() => {
|
||||
const performerImage = performer.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null && performerImage) {
|
||||
const performerImageURL = new URL(performerImage);
|
||||
performerImageURL.searchParams.set("default", "true");
|
||||
return performerImageURL.toString();
|
||||
} else if (image) {
|
||||
return image;
|
||||
const activeImage = useMemo(() => {
|
||||
const performerImage = performer.image_path;
|
||||
if (isEditing) {
|
||||
if (image === null && performerImage) {
|
||||
const performerImageURL = new URL(performerImage);
|
||||
performerImageURL.searchParams.set("default", "true");
|
||||
return performerImageURL.toString();
|
||||
} else if (image) {
|
||||
return image;
|
||||
}
|
||||
}
|
||||
return performerImage;
|
||||
}, [image, isEditing, performer.image_path]);
|
||||
|
||||
const lightboxImages = useMemo(
|
||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||
[activeImage]
|
||||
);
|
||||
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [deletePerformer, { loading: isDestroying }] = usePerformerDestroy();
|
||||
|
||||
async function onAutoTag() {
|
||||
try {
|
||||
await mutateMetadataAutoTag({ performers: [performer.id] });
|
||||
Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" }));
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
return performerImage;
|
||||
}, [image, isEditing, performer.image_path]);
|
||||
|
||||
const lightboxImages = useMemo(
|
||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||
[activeImage]
|
||||
);
|
||||
useRatingKeybinds(
|
||||
true,
|
||||
configuration?.ui.ratingSystemOptions?.type,
|
||||
setRating
|
||||
);
|
||||
|
||||
const [updatePerformer] = usePerformerUpdate();
|
||||
const [deletePerformer, { loading: isDestroying }] = usePerformerDestroy();
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
Mousetrap.bind("f", () => setFavorite(!performer.favorite));
|
||||
Mousetrap.bind(",", () => setCollapsed(!collapsed));
|
||||
|
||||
async function onAutoTag() {
|
||||
try {
|
||||
await mutateMetadataAutoTag({ performers: [performer.id] });
|
||||
Toast.success(intl.formatMessage({ id: "toast.started_auto_tagging" }));
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
useRatingKeybinds(
|
||||
true,
|
||||
configuration?.ui.ratingSystemOptions?.type,
|
||||
setRating
|
||||
);
|
||||
|
||||
// set up hotkeys
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("e", () => toggleEditing());
|
||||
Mousetrap.bind("f", () => setFavorite(!performer.favorite));
|
||||
Mousetrap.bind(",", () => setCollapsed(!collapsed));
|
||||
|
||||
return () => {
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("f");
|
||||
Mousetrap.unbind(",");
|
||||
};
|
||||
});
|
||||
|
||||
async function onSave(input: GQL.PerformerCreateInput) {
|
||||
await updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
return () => {
|
||||
Mousetrap.unbind("e");
|
||||
Mousetrap.unbind("f");
|
||||
Mousetrap.unbind(",");
|
||||
};
|
||||
});
|
||||
toggleEditing(false);
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{ entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase() }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
try {
|
||||
await deletePerformer({ variables: { id: performer.id } });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
|
||||
// redirect to performers page
|
||||
history.push("/performers");
|
||||
}
|
||||
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
} else {
|
||||
setIsEditing((e) => !e);
|
||||
}
|
||||
setImage(undefined);
|
||||
}
|
||||
|
||||
function setFavorite(v: boolean) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
async function onSave(input: GQL.PerformerCreateInput) {
|
||||
await updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
favorite: v,
|
||||
...input,
|
||||
},
|
||||
},
|
||||
});
|
||||
toggleEditing(false);
|
||||
Toast.success(
|
||||
intl.formatMessage(
|
||||
{ id: "toast.updated_entity" },
|
||||
{
|
||||
entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase(),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setRating(v: number | null) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
rating100: v,
|
||||
async function onDelete() {
|
||||
try {
|
||||
await deletePerformer({ variables: { id: performer.id } });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
|
||||
// redirect to performers page
|
||||
history.push("/performers");
|
||||
}
|
||||
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
} else {
|
||||
setIsEditing((e) => !e);
|
||||
}
|
||||
setImage(undefined);
|
||||
}
|
||||
|
||||
function setFavorite(v: boolean) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isDestroying)
|
||||
return (
|
||||
<LoadingIndicator
|
||||
message={`Deleting performer ${performer.id}: ${performer.name}`}
|
||||
/>
|
||||
);
|
||||
function setRating(v: number | null) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
variables: {
|
||||
input: {
|
||||
id: performer.id,
|
||||
rating100: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const headerClassName = cx("detail-header", {
|
||||
edit: isEditing,
|
||||
collapsed,
|
||||
"full-width": !collapsed && !compactExpandedDetails,
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="performer-page" className="row">
|
||||
<Helmet>
|
||||
<title>{performer.name}</title>
|
||||
</Helmet>
|
||||
|
||||
<div className={headerClassName}>
|
||||
<BackgroundImage
|
||||
imagePath={activeImage ?? undefined}
|
||||
show={enableBackgroundImage && !isEditing}
|
||||
if (isDestroying)
|
||||
return (
|
||||
<LoadingIndicator
|
||||
message={`Deleting performer ${performer.id}: ${performer.name}`}
|
||||
/>
|
||||
<div className="detail-container">
|
||||
<HeaderImage encodingImage={encodingImage}>
|
||||
{!!activeImage && (
|
||||
<LightboxLink images={lightboxImages}>
|
||||
<DetailImage
|
||||
className="performer"
|
||||
src={activeImage}
|
||||
alt={performer.name}
|
||||
/>
|
||||
</LightboxLink>
|
||||
)}
|
||||
</HeaderImage>
|
||||
);
|
||||
|
||||
<div className="row">
|
||||
<div className="performer-head col">
|
||||
<DetailTitle
|
||||
name={performer.name}
|
||||
disambiguation={performer.disambiguation ?? undefined}
|
||||
classNamePrefix="performer"
|
||||
>
|
||||
const headerClassName = cx("detail-header", {
|
||||
edit: isEditing,
|
||||
collapsed,
|
||||
"full-width": !collapsed && !compactExpandedDetails,
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="performer-page" className="row">
|
||||
<Helmet>
|
||||
<title>{performer.name}</title>
|
||||
</Helmet>
|
||||
|
||||
<div className={headerClassName}>
|
||||
<BackgroundImage
|
||||
imagePath={activeImage ?? undefined}
|
||||
show={enableBackgroundImage && !isEditing}
|
||||
/>
|
||||
<div className="detail-container">
|
||||
<HeaderImage encodingImage={encodingImage}>
|
||||
{!!activeImage && (
|
||||
<LightboxLink images={lightboxImages}>
|
||||
<DetailImage
|
||||
className="performer"
|
||||
src={activeImage}
|
||||
alt={performer.name}
|
||||
/>
|
||||
</LightboxLink>
|
||||
)}
|
||||
</HeaderImage>
|
||||
|
||||
<div className="row">
|
||||
<div className="performer-head col">
|
||||
<DetailTitle
|
||||
name={performer.name}
|
||||
disambiguation={performer.disambiguation ?? undefined}
|
||||
classNamePrefix="performer"
|
||||
>
|
||||
{!isEditing && (
|
||||
<ExpandCollapseButton
|
||||
collapsed={collapsed}
|
||||
setCollapsed={(v) => setCollapsed(v)}
|
||||
/>
|
||||
)}
|
||||
<span className="name-icons">
|
||||
<FavoriteIcon
|
||||
favorite={performer.favorite}
|
||||
onToggleFavorite={(v) => setFavorite(v)}
|
||||
/>
|
||||
<ExternalLinkButtons urls={performer.urls ?? undefined} />
|
||||
</span>
|
||||
</DetailTitle>
|
||||
<AliasList aliases={performer.alias_list} />
|
||||
<RatingSystem
|
||||
value={performer.rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
clickToRate
|
||||
withoutContext
|
||||
/>
|
||||
{!isEditing && (
|
||||
<ExpandCollapseButton
|
||||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
collapsed={collapsed}
|
||||
setCollapsed={(v) => setCollapsed(v)}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
)}
|
||||
<span className="name-icons">
|
||||
<FavoriteIcon
|
||||
favorite={performer.favorite}
|
||||
onToggleFavorite={(v) => setFavorite(v)}
|
||||
{isEditing ? (
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
<ExternalLinkButtons urls={performer.urls ?? undefined} />
|
||||
</span>
|
||||
</DetailTitle>
|
||||
<AliasList aliases={performer.alias_list} />
|
||||
<RatingSystem
|
||||
value={performer.rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
clickToRate
|
||||
withoutContext
|
||||
/>
|
||||
) : (
|
||||
<Col>
|
||||
<Row xs={8}>
|
||||
<DetailsEditNavbar
|
||||
objectName={
|
||||
performer?.name ??
|
||||
intl.formatMessage({ id: "performer" })
|
||||
}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={performer.ignore_auto_tag}
|
||||
isNew={false}
|
||||
isEditing={false}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
classNames="mb-2"
|
||||
customButtons={
|
||||
<div>
|
||||
<PerformerSubmitButton performer={performer} />
|
||||
</div>
|
||||
}
|
||||
></DetailsEditNavbar>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isEditing && loadStickyHeader && (
|
||||
<CompressedPerformerDetailsPanel performer={performer} />
|
||||
)}
|
||||
|
||||
<div className="detail-body">
|
||||
<div className="performer-body">
|
||||
<div className="performer-tabs">
|
||||
{!isEditing && (
|
||||
<PerformerDetailsPanel
|
||||
<PerformerTabs
|
||||
tabKey={tabKey}
|
||||
performer={performer}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
) : (
|
||||
<Col>
|
||||
<Row xs={8}>
|
||||
<DetailsEditNavbar
|
||||
objectName={
|
||||
performer?.name ??
|
||||
intl.formatMessage({ id: "performer" })
|
||||
}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={performer.ignore_auto_tag}
|
||||
isNew={false}
|
||||
isEditing={false}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
classNames="mb-2"
|
||||
customButtons={
|
||||
<div>
|
||||
<PerformerSubmitButton performer={performer} />
|
||||
</div>
|
||||
}
|
||||
></DetailsEditNavbar>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!isEditing && loadStickyHeader && (
|
||||
<CompressedPerformerDetailsPanel performer={performer} />
|
||||
)}
|
||||
|
||||
<div className="detail-body">
|
||||
<div className="performer-body">
|
||||
<div className="performer-tabs">
|
||||
{!isEditing && (
|
||||
<PerformerTabs
|
||||
tabKey={tabKey}
|
||||
performer={performer}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const PerformerLoader: React.FC<RouteComponentProps<IPerformerParams>> = ({
|
||||
location,
|
||||
|
||||
@@ -3,22 +3,21 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { GalleryList } from "src/components/Galleries/GalleryList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
active: boolean;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerGalleriesPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
active,
|
||||
performer,
|
||||
}) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<GalleryList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerGalleries}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const PerformerGalleriesPanel: React.FC<IPerformerDetailsProps> =
|
||||
PatchComponent("PerformerGalleriesPanel", ({ active, performer }) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<GalleryList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerGalleries}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,22 +3,21 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { GroupList } from "src/components/Groups/GroupList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
active: boolean;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerGroupsPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
active,
|
||||
performer,
|
||||
}) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<GroupList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerGroups}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const PerformerGroupsPanel: React.FC<IPerformerDetailsProps> =
|
||||
PatchComponent("PerformerGroupsPanel", ({ active, performer }) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<GroupList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerGroups}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,22 +3,21 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { ImageList } from "src/components/Images/ImageList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPerformerImagesPanel {
|
||||
active: boolean;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> = ({
|
||||
active,
|
||||
performer,
|
||||
}) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<ImageList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerImages}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const PerformerImagesPanel: React.FC<IPerformerImagesPanel> =
|
||||
PatchComponent("PerformerImagesPanel", ({ active, performer }) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<ImageList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerImages}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,22 +3,21 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { SceneList } from "src/components/Scenes/SceneList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
active: boolean;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
active,
|
||||
performer,
|
||||
}) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<SceneList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerScenes}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> =
|
||||
PatchComponent("PerformerScenesPanel", ({ active, performer }) => {
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
return (
|
||||
<SceneList
|
||||
filterHook={filterHook}
|
||||
alterQuery={active}
|
||||
view={View.PerformerScenes}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,33 +3,32 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { PerformerList } from "src/components/Performers/PerformerList";
|
||||
import { usePerformerFilterHook } from "src/core/performers";
|
||||
import { View } from "src/components/List/views";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPerformerDetailsProps {
|
||||
active: boolean;
|
||||
performer: GQL.PerformerDataFragment;
|
||||
}
|
||||
|
||||
export const PerformerAppearsWithPanel: React.FC<IPerformerDetailsProps> = ({
|
||||
active,
|
||||
performer,
|
||||
}) => {
|
||||
const performerValue = {
|
||||
id: performer.id,
|
||||
label: performer.name ?? `Performer ${performer.id}`,
|
||||
};
|
||||
export const PerformerAppearsWithPanel: React.FC<IPerformerDetailsProps> =
|
||||
PatchComponent("PerformerAppearsWithPanel", ({ active, performer }) => {
|
||||
const performerValue = {
|
||||
id: performer.id,
|
||||
label: performer.name ?? `Performer ${performer.id}`,
|
||||
};
|
||||
|
||||
const extraCriteria = {
|
||||
performer: performerValue,
|
||||
};
|
||||
const extraCriteria = {
|
||||
performer: performerValue,
|
||||
};
|
||||
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
const filterHook = usePerformerFilterHook(performer);
|
||||
|
||||
return (
|
||||
<PerformerList
|
||||
filterHook={filterHook}
|
||||
extraCriteria={extraCriteria}
|
||||
alterQuery={active}
|
||||
view={View.PerformerAppearsWith}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<PerformerList
|
||||
filterHook={filterHook}
|
||||
extraCriteria={extraCriteria}
|
||||
alterQuery={active}
|
||||
view={View.PerformerAppearsWith}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { lazyComponent } from "src/utils/lazyComponent";
|
||||
import cx from "classnames";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { PatchComponent, PatchContainerComponent } from "src/patch";
|
||||
|
||||
const SubmitStashBoxDraft = lazyComponent(
|
||||
() => import("src/components/Dialogs/SubmitDraft")
|
||||
@@ -153,24 +154,31 @@ interface ISceneParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const ScenePage: React.FC<IProps> = ({
|
||||
scene,
|
||||
setTimestamp,
|
||||
queueScenes,
|
||||
onQueueNext,
|
||||
onQueuePrevious,
|
||||
onQueueRandom,
|
||||
onQueueSceneClicked,
|
||||
onDelete,
|
||||
continuePlaylist,
|
||||
queueHasMoreScenes,
|
||||
onQueueMoreScenes,
|
||||
onQueueLessScenes,
|
||||
queueStart,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
setContinuePlaylist,
|
||||
}) => {
|
||||
const ScenePageTabs = PatchContainerComponent<IProps>("ScenePage.Tabs");
|
||||
const ScenePageTabContent = PatchContainerComponent<IProps>(
|
||||
"ScenePage.TabContent"
|
||||
);
|
||||
|
||||
const ScenePage: React.FC<IProps> = PatchComponent("ScenePage", (props) => {
|
||||
const {
|
||||
scene,
|
||||
setTimestamp,
|
||||
queueScenes,
|
||||
onQueueNext,
|
||||
onQueuePrevious,
|
||||
onQueueRandom,
|
||||
onQueueSceneClicked,
|
||||
onDelete,
|
||||
continuePlaylist,
|
||||
queueHasMoreScenes,
|
||||
onQueueMoreScenes,
|
||||
onQueueLessScenes,
|
||||
queueStart,
|
||||
collapsed,
|
||||
setCollapsed,
|
||||
setContinuePlaylist,
|
||||
} = props;
|
||||
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
const [updateScene] = useSceneUpdate();
|
||||
@@ -423,126 +431,133 @@ const ScenePage: React.FC<IProps> = ({
|
||||
>
|
||||
<div>
|
||||
<Nav variant="tabs" className="mr-auto">
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-details-panel">
|
||||
<FormattedMessage id="details" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{queueScenes.length > 0 ? (
|
||||
<ScenePageTabs {...props}>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-queue-panel">
|
||||
<FormattedMessage id="queue" />
|
||||
<Nav.Link eventKey="scene-details-panel">
|
||||
<FormattedMessage id="details" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-markers-panel">
|
||||
<FormattedMessage id="markers" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
{scene.groups.length > 0 ? (
|
||||
{queueScenes.length > 0 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-queue-panel">
|
||||
<FormattedMessage id="queue" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-group-panel">
|
||||
<FormattedMessage
|
||||
id="countables.groups"
|
||||
values={{ count: scene.groups.length }}
|
||||
/>
|
||||
<Nav.Link eventKey="scene-markers-panel">
|
||||
<FormattedMessage id="markers" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{scene.galleries.length >= 1 ? (
|
||||
{scene.groups.length > 0 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-group-panel">
|
||||
<FormattedMessage
|
||||
id="countables.groups"
|
||||
values={{ count: scene.groups.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{scene.galleries.length >= 1 ? (
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-galleries-panel">
|
||||
<FormattedMessage
|
||||
id="countables.galleries"
|
||||
values={{ count: scene.galleries.length }}
|
||||
/>
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-galleries-panel">
|
||||
<FormattedMessage
|
||||
id="countables.galleries"
|
||||
values={{ count: scene.galleries.length }}
|
||||
/>
|
||||
<Nav.Link eventKey="scene-video-filter-panel">
|
||||
<FormattedMessage id="effect_filters.name" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
) : undefined}
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-video-filter-panel">
|
||||
<FormattedMessage id="effect_filters.name" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-file-info-panel">
|
||||
<FormattedMessage id="file_info" />
|
||||
<Counter count={scene.files.length} hideZero hideOne />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-history-panel">
|
||||
<FormattedMessage id="history" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-edit-panel">
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-file-info-panel">
|
||||
<FormattedMessage id="file_info" />
|
||||
<Counter count={scene.files.length} hideZero hideOne />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-history-panel">
|
||||
<FormattedMessage id="history" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
<Nav.Item>
|
||||
<Nav.Link eventKey="scene-edit-panel">
|
||||
<FormattedMessage id="actions.edit" />
|
||||
</Nav.Link>
|
||||
</Nav.Item>
|
||||
</ScenePageTabs>
|
||||
</Nav>
|
||||
</div>
|
||||
|
||||
<Tab.Content>
|
||||
<Tab.Pane eventKey="scene-details-panel">
|
||||
<SceneDetailPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-queue-panel">
|
||||
<QueueViewer
|
||||
scenes={queueScenes}
|
||||
currentID={scene.id}
|
||||
continue={continuePlaylist}
|
||||
setContinue={setContinuePlaylist}
|
||||
onSceneClicked={onQueueSceneClicked}
|
||||
onNext={onQueueNext}
|
||||
onPrevious={onQueuePrevious}
|
||||
onRandom={onQueueRandom}
|
||||
start={queueStart}
|
||||
hasMoreScenes={queueHasMoreScenes}
|
||||
onLessScenes={onQueueLessScenes}
|
||||
onMoreScenes={onQueueMoreScenes}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-markers-panel">
|
||||
<SceneMarkersPanel
|
||||
sceneId={scene.id}
|
||||
onClickMarker={onClickMarker}
|
||||
isVisible={activeTabKey === "scene-markers-panel"}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-group-panel">
|
||||
<SceneGroupPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
{scene.galleries.length >= 1 && (
|
||||
<Tab.Pane eventKey="scene-galleries-panel">
|
||||
<SceneGalleriesPanel galleries={scene.galleries} />
|
||||
{scene.galleries.length === 1 && (
|
||||
<GalleryViewer galleryId={scene.galleries[0].id} />
|
||||
)}
|
||||
<ScenePageTabContent {...props}>
|
||||
<Tab.Pane eventKey="scene-details-panel">
|
||||
<SceneDetailPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
)}
|
||||
<Tab.Pane eventKey="scene-video-filter-panel">
|
||||
<SceneVideoFilterPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane className="file-info-panel" eventKey="scene-file-info-panel">
|
||||
<SceneFileInfoPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-edit-panel" mountOnEnter>
|
||||
<SceneEditPanel
|
||||
isVisible={activeTabKey === "scene-edit-panel"}
|
||||
scene={scene}
|
||||
onSubmit={onSave}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-history-panel">
|
||||
<SceneHistoryPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-queue-panel">
|
||||
<QueueViewer
|
||||
scenes={queueScenes}
|
||||
currentID={scene.id}
|
||||
continue={continuePlaylist}
|
||||
setContinue={setContinuePlaylist}
|
||||
onSceneClicked={onQueueSceneClicked}
|
||||
onNext={onQueueNext}
|
||||
onPrevious={onQueuePrevious}
|
||||
onRandom={onQueueRandom}
|
||||
start={queueStart}
|
||||
hasMoreScenes={queueHasMoreScenes}
|
||||
onLessScenes={onQueueLessScenes}
|
||||
onMoreScenes={onQueueMoreScenes}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-markers-panel">
|
||||
<SceneMarkersPanel
|
||||
sceneId={scene.id}
|
||||
onClickMarker={onClickMarker}
|
||||
isVisible={activeTabKey === "scene-markers-panel"}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-group-panel">
|
||||
<SceneGroupPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
{scene.galleries.length >= 1 && (
|
||||
<Tab.Pane eventKey="scene-galleries-panel">
|
||||
<SceneGalleriesPanel galleries={scene.galleries} />
|
||||
{scene.galleries.length === 1 && (
|
||||
<GalleryViewer galleryId={scene.galleries[0].id} />
|
||||
)}
|
||||
</Tab.Pane>
|
||||
)}
|
||||
<Tab.Pane eventKey="scene-video-filter-panel">
|
||||
<SceneVideoFilterPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane
|
||||
className="file-info-panel"
|
||||
eventKey="scene-file-info-panel"
|
||||
>
|
||||
<SceneFileInfoPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-edit-panel" mountOnEnter>
|
||||
<SceneEditPanel
|
||||
isVisible={activeTabKey === "scene-edit-panel"}
|
||||
scene={scene}
|
||||
onSubmit={onSave}
|
||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||
/>
|
||||
</Tab.Pane>
|
||||
<Tab.Pane eventKey="scene-history-panel">
|
||||
<SceneHistoryPanel scene={scene} />
|
||||
</Tab.Pane>
|
||||
</ScenePageTabContent>
|
||||
</Tab.Content>
|
||||
</Tab.Container>
|
||||
);
|
||||
@@ -657,7 +672,7 @@ const ScenePage: React.FC<IProps> = ({
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const SceneLoader: React.FC<RouteComponentProps<ISceneParams>> = ({
|
||||
location,
|
||||
|
||||
@@ -2,19 +2,23 @@ import { FormattedMessage } from "react-intl";
|
||||
import { Counter } from "../Counter";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
export const TabTitleCounter: React.FC<{
|
||||
messageID: string;
|
||||
count: number;
|
||||
abbreviateCounter: boolean;
|
||||
}> = ({ messageID, count, abbreviateCounter }) => {
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage id={messageID} />
|
||||
<Counter count={count} abbreviateCounter={abbreviateCounter} hideZero />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}> = PatchComponent(
|
||||
"TabTitleCounter",
|
||||
({ messageID, count, abbreviateCounter }) => {
|
||||
return (
|
||||
<>
|
||||
<FormattedMessage id={messageID} />
|
||||
<Counter count={count} abbreviateCounter={abbreviateCounter} hideZero />
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export function useTabKey(props: {
|
||||
tabKey: string | undefined;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { Overlay, Popover, OverlayProps } from "react-bootstrap";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IHoverPopover {
|
||||
enterDelay?: number;
|
||||
@@ -12,72 +13,75 @@ interface IHoverPopover {
|
||||
target?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const HoverPopover: React.FC<IHoverPopover> = ({
|
||||
enterDelay = 200,
|
||||
leaveDelay = 200,
|
||||
content,
|
||||
children,
|
||||
className,
|
||||
placement = "top",
|
||||
onOpen,
|
||||
onClose,
|
||||
target,
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const enterTimer = useRef<number>();
|
||||
const leaveTimer = useRef<number>();
|
||||
export const HoverPopover: React.FC<IHoverPopover> = PatchComponent(
|
||||
"HoverPopover",
|
||||
({
|
||||
enterDelay = 200,
|
||||
leaveDelay = 200,
|
||||
content,
|
||||
children,
|
||||
className,
|
||||
placement = "top",
|
||||
onOpen,
|
||||
onClose,
|
||||
target,
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const triggerRef = useRef<HTMLDivElement>(null);
|
||||
const enterTimer = useRef<number>();
|
||||
const leaveTimer = useRef<number>();
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
window.clearTimeout(leaveTimer.current);
|
||||
enterTimer.current = window.setTimeout(() => {
|
||||
setShow(true);
|
||||
onOpen?.();
|
||||
}, enterDelay);
|
||||
}, [enterDelay, onOpen]);
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
window.clearTimeout(enterTimer.current);
|
||||
leaveTimer.current = window.setTimeout(() => {
|
||||
setShow(false);
|
||||
onClose?.();
|
||||
}, leaveDelay);
|
||||
}, [leaveDelay, onClose]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(enterTimer.current);
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
window.clearTimeout(leaveTimer.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
enterTimer.current = window.setTimeout(() => {
|
||||
setShow(true);
|
||||
onOpen?.();
|
||||
}, enterDelay);
|
||||
}, [enterDelay, onOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={triggerRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{triggerRef.current && (
|
||||
<Overlay
|
||||
show={show}
|
||||
placement={placement}
|
||||
target={target?.current ?? triggerRef.current}
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
window.clearTimeout(enterTimer.current);
|
||||
leaveTimer.current = window.setTimeout(() => {
|
||||
setShow(false);
|
||||
onClose?.();
|
||||
}, leaveDelay);
|
||||
}, [leaveDelay, onClose]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
window.clearTimeout(enterTimer.current);
|
||||
window.clearTimeout(leaveTimer.current);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={className}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
ref={triggerRef}
|
||||
>
|
||||
<Popover
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
id="popover"
|
||||
className="hover-popover-content"
|
||||
{children}
|
||||
</div>
|
||||
{triggerRef.current && (
|
||||
<Overlay
|
||||
show={show}
|
||||
placement={placement}
|
||||
target={target?.current ?? triggerRef.current}
|
||||
>
|
||||
{content}
|
||||
</Popover>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
<Popover
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
id="popover"
|
||||
className="hover-popover-content"
|
||||
>
|
||||
{content}
|
||||
</Popover>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from "react";
|
||||
import { Spinner } from "react-bootstrap";
|
||||
import cx from "classnames";
|
||||
import { useIntl } from "react-intl";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface ILoadingProps {
|
||||
message?: JSX.Element | string;
|
||||
@@ -13,24 +14,26 @@ interface ILoadingProps {
|
||||
const CLASSNAME = "LoadingIndicator";
|
||||
const CLASSNAME_MESSAGE = `${CLASSNAME}-message`;
|
||||
|
||||
export const LoadingIndicator: React.FC<ILoadingProps> = ({
|
||||
message,
|
||||
inline = false,
|
||||
small = false,
|
||||
card = false,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
export const LoadingIndicator: React.FC<ILoadingProps> = PatchComponent(
|
||||
"LoadingIndicator",
|
||||
({ message, inline = false, small = false, card = false }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const text = intl.formatMessage({ id: "loading.generic" });
|
||||
const text = intl.formatMessage({ id: "loading.generic" });
|
||||
|
||||
return (
|
||||
<div className={cx(CLASSNAME, { inline, small, "card-based": card })}>
|
||||
<Spinner animation="border" role="status" size={small ? "sm" : undefined}>
|
||||
<span className="sr-only">{text}</span>
|
||||
</Spinner>
|
||||
{message !== "" && (
|
||||
<h4 className={CLASSNAME_MESSAGE}>{message ?? text}</h4>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div className={cx(CLASSNAME, { inline, small, "card-based": card })}>
|
||||
<Spinner
|
||||
animation="border"
|
||||
role="status"
|
||||
size={small ? "sm" : undefined}
|
||||
>
|
||||
<span className="sr-only">{text}</span>
|
||||
</Spinner>
|
||||
{message !== "" && (
|
||||
<h4 className={CLASSNAME_MESSAGE}>{message ?? text}</h4>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ interface IProps {
|
||||
url: string;
|
||||
type: PopoverLinkType;
|
||||
count: number;
|
||||
showZero?: boolean;
|
||||
}
|
||||
|
||||
export const PopoverCountButton: React.FC<IProps> = ({
|
||||
@@ -60,9 +61,14 @@ export const PopoverCountButton: React.FC<IProps> = ({
|
||||
url,
|
||||
type,
|
||||
count,
|
||||
showZero = true,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
if (!showZero && count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO - refactor - create SceneIcon, ImageIcon etc components
|
||||
function getIcon() {
|
||||
switch (type) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
import { faFolderTree } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
type SceneMarkerFragment = Pick<GQL.SceneMarker, "id" | "title" | "seconds"> & {
|
||||
scene: Pick<GQL.Scene, "id">;
|
||||
@@ -243,66 +244,69 @@ interface ITagLinkProps {
|
||||
hierarchyTooltipID?: string;
|
||||
}
|
||||
|
||||
export const TagLink: React.FC<ITagLinkProps> = ({
|
||||
tag,
|
||||
linkType = "scene",
|
||||
className,
|
||||
hoverPlacement,
|
||||
showHierarchyIcon = false,
|
||||
hierarchyTooltipID,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "scene":
|
||||
return NavUtils.makeTagScenesUrl(tag);
|
||||
case "performer":
|
||||
return NavUtils.makeTagPerformersUrl(tag);
|
||||
case "studio":
|
||||
return NavUtils.makeTagStudiosUrl(tag);
|
||||
case "gallery":
|
||||
return NavUtils.makeTagGalleriesUrl(tag);
|
||||
case "image":
|
||||
return NavUtils.makeTagImagesUrl(tag);
|
||||
case "group":
|
||||
return NavUtils.makeTagGroupsUrl(tag);
|
||||
case "scene_marker":
|
||||
return NavUtils.makeTagSceneMarkersUrl(tag);
|
||||
case "details":
|
||||
return NavUtils.makeTagUrl(tag.id ?? "");
|
||||
}
|
||||
}, [tag, linkType]);
|
||||
export const TagLink: React.FC<ITagLinkProps> = PatchComponent(
|
||||
"TagLink",
|
||||
({
|
||||
tag,
|
||||
linkType = "scene",
|
||||
className,
|
||||
hoverPlacement,
|
||||
showHierarchyIcon = false,
|
||||
hierarchyTooltipID,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "scene":
|
||||
return NavUtils.makeTagScenesUrl(tag);
|
||||
case "performer":
|
||||
return NavUtils.makeTagPerformersUrl(tag);
|
||||
case "studio":
|
||||
return NavUtils.makeTagStudiosUrl(tag);
|
||||
case "gallery":
|
||||
return NavUtils.makeTagGalleriesUrl(tag);
|
||||
case "image":
|
||||
return NavUtils.makeTagImagesUrl(tag);
|
||||
case "group":
|
||||
return NavUtils.makeTagGroupsUrl(tag);
|
||||
case "scene_marker":
|
||||
return NavUtils.makeTagSceneMarkersUrl(tag);
|
||||
case "details":
|
||||
return NavUtils.makeTagUrl(tag.id ?? "");
|
||||
}
|
||||
}, [tag, linkType]);
|
||||
|
||||
const title = tag.name || "";
|
||||
const title = tag.name || "";
|
||||
|
||||
const tooltip = useMemo(() => {
|
||||
if (!hierarchyTooltipID) {
|
||||
return <></>;
|
||||
}
|
||||
const tooltip = useMemo(() => {
|
||||
if (!hierarchyTooltipID) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip id="tag-hierarchy-tooltip">
|
||||
<FormattedMessage id={hierarchyTooltipID} />
|
||||
</Tooltip>
|
||||
);
|
||||
}, [hierarchyTooltipID]);
|
||||
|
||||
return (
|
||||
<Tooltip id="tag-hierarchy-tooltip">
|
||||
<FormattedMessage id={hierarchyTooltipID} />
|
||||
</Tooltip>
|
||||
<SortNameLinkComponent
|
||||
sortName={tag.sort_name || title}
|
||||
link={link}
|
||||
className={className}
|
||||
>
|
||||
<TagPopover id={tag.id ?? ""} placement={hoverPlacement}>
|
||||
{title}
|
||||
{showHierarchyIcon && (
|
||||
<OverlayTrigger placement="top" overlay={tooltip}>
|
||||
<span className="icon-wrapper">
|
||||
<span className="vertical-line">|</span>
|
||||
<Icon icon={faFolderTree} className="tag-icon" />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</TagPopover>
|
||||
</SortNameLinkComponent>
|
||||
);
|
||||
}, [hierarchyTooltipID]);
|
||||
|
||||
return (
|
||||
<SortNameLinkComponent
|
||||
sortName={tag.sort_name || title}
|
||||
link={link}
|
||||
className={className}
|
||||
>
|
||||
<TagPopover id={tag.id ?? ""} placement={hoverPlacement}>
|
||||
{title}
|
||||
{showHierarchyIcon && (
|
||||
<OverlayTrigger placement="top" overlay={tooltip}>
|
||||
<span className="icon-wrapper">
|
||||
<span className="vertical-line">|</span>
|
||||
<Icon icon={faFolderTree} className="tag-icon" />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</TagPopover>
|
||||
</SortNameLinkComponent>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { Button, ButtonGroup } from "react-bootstrap";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -12,6 +13,7 @@ import { Icon } from "../Shared/Icon";
|
||||
import { faHeart } from "@fortawesome/free-solid-svg-icons";
|
||||
import cx from "classnames";
|
||||
import { useTagUpdate } from "src/core/StashService";
|
||||
|
||||
interface IProps {
|
||||
tag: GQL.TagDataFragment;
|
||||
containerWidth?: number;
|
||||
@@ -21,16 +23,226 @@ interface IProps {
|
||||
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
export const TagCard: React.FC<IProps> = ({
|
||||
tag,
|
||||
containerWidth,
|
||||
zoomIndex,
|
||||
selecting,
|
||||
selected,
|
||||
onSelectedChanged,
|
||||
}) => {
|
||||
const TagCardPopovers: React.FC<IProps> = PatchComponent(
|
||||
"TagCard.Popovers",
|
||||
({ tag }) => {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<ButtonGroup className="card-popovers">
|
||||
<PopoverCountButton
|
||||
className="scene-count"
|
||||
type="scene"
|
||||
count={tag.scene_count}
|
||||
url={NavUtils.makeTagScenesUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
<PopoverCountButton
|
||||
className="image-count"
|
||||
type="image"
|
||||
count={tag.image_count}
|
||||
url={NavUtils.makeTagImagesUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
<PopoverCountButton
|
||||
className="gallery-count"
|
||||
type="gallery"
|
||||
count={tag.gallery_count}
|
||||
url={NavUtils.makeTagGalleriesUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
<PopoverCountButton
|
||||
className="group-count"
|
||||
type="group"
|
||||
count={tag.group_count}
|
||||
url={NavUtils.makeTagGroupsUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
<PopoverCountButton
|
||||
className="marker-count"
|
||||
type="marker"
|
||||
count={tag.scene_marker_count}
|
||||
url={NavUtils.makeTagSceneMarkersUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
<PopoverCountButton
|
||||
className="performer-count"
|
||||
type="performer"
|
||||
count={tag.performer_count}
|
||||
url={NavUtils.makeTagPerformersUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
<PopoverCountButton
|
||||
className="studio-count"
|
||||
type="studio"
|
||||
count={tag.studio_count}
|
||||
url={NavUtils.makeTagStudiosUrl(tag)}
|
||||
showZero={false}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const TagCardOverlays: React.FC<IProps> = PatchComponent(
|
||||
"TagCard.Overlays",
|
||||
({ tag }) => {
|
||||
const [updateTag] = useTagUpdate();
|
||||
|
||||
function renderFavoriteIcon() {
|
||||
return (
|
||||
<Link to="" onClick={(e) => e.preventDefault()}>
|
||||
<Button
|
||||
className={cx(
|
||||
"minimal",
|
||||
"mousetrap",
|
||||
"favorite-button",
|
||||
tag.favorite ? "favorite" : "not-favorite"
|
||||
)}
|
||||
onClick={() => onToggleFavorite!(!tag.favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} size="2x" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function onToggleFavorite(v: boolean) {
|
||||
if (tag.id) {
|
||||
updateTag({
|
||||
variables: {
|
||||
input: {
|
||||
id: tag.id,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return <>{renderFavoriteIcon()}</>;
|
||||
}
|
||||
);
|
||||
|
||||
const TagCardDetails: React.FC<IProps> = PatchComponent(
|
||||
"TagCard.Details",
|
||||
({ tag }) => {
|
||||
function maybeRenderDescription() {
|
||||
if (tag.description) {
|
||||
return (
|
||||
<TruncatedText
|
||||
className="tag-description"
|
||||
text={tag.description}
|
||||
lineCount={3}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderParents() {
|
||||
if (tag.parents.length === 1) {
|
||||
const parent = tag.parents[0];
|
||||
return (
|
||||
<div className="tag-parent-tags">
|
||||
<FormattedMessage
|
||||
id="sub_tag_of"
|
||||
values={{
|
||||
parent: <Link to={`/tags/${parent.id}`}>{parent.name}</Link>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tag.parents.length > 1) {
|
||||
return (
|
||||
<div className="tag-parent-tags">
|
||||
<FormattedMessage
|
||||
id="sub_tag_of"
|
||||
values={{
|
||||
parent: (
|
||||
<Link to={NavUtils.makeParentTagsUrl(tag)}>
|
||||
{tag.parents.length}
|
||||
<FormattedMessage
|
||||
id="countables.tags"
|
||||
values={{ count: tag.parents.length }}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderChildren() {
|
||||
if (tag.children.length > 0) {
|
||||
return (
|
||||
<div className="tag-sub-tags">
|
||||
<FormattedMessage
|
||||
id="parent_of"
|
||||
values={{
|
||||
children: (
|
||||
<Link to={NavUtils.makeChildTagsUrl(tag)}>
|
||||
{tag.children.length}
|
||||
<FormattedMessage
|
||||
id="countables.tags"
|
||||
values={{ count: tag.children.length }}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{maybeRenderDescription()}
|
||||
{maybeRenderParents()}
|
||||
{maybeRenderChildren()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const TagCardImage: React.FC<IProps> = PatchComponent(
|
||||
"TagCard.Image",
|
||||
({ tag }) => {
|
||||
return (
|
||||
<>
|
||||
<img
|
||||
loading="lazy"
|
||||
className="tag-card-image"
|
||||
alt={tag.name}
|
||||
src={tag.image_path ?? ""}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const TagCardTitle: React.FC<IProps> = PatchComponent(
|
||||
"TagCard.Title",
|
||||
({ tag }) => {
|
||||
return <>{tag.name ?? ""}</>;
|
||||
}
|
||||
);
|
||||
|
||||
export const TagCard: React.FC<IProps> = PatchComponent("TagCard", (props) => {
|
||||
const {
|
||||
tag,
|
||||
containerWidth,
|
||||
zoomIndex,
|
||||
selecting,
|
||||
selected,
|
||||
onSelectedChanged,
|
||||
} = props;
|
||||
const [cardWidth, setCardWidth] = useState<number>();
|
||||
const [updateTag] = useTagUpdate();
|
||||
useEffect(() => {
|
||||
if (!containerWidth || zoomIndex === undefined || ScreenUtils.isMobile())
|
||||
return;
|
||||
@@ -57,244 +269,20 @@ export const TagCard: React.FC<IProps> = ({
|
||||
setCardWidth(fittedCardWidth);
|
||||
}, [containerWidth, zoomIndex]);
|
||||
|
||||
function maybeRenderDescription() {
|
||||
if (tag.description) {
|
||||
return (
|
||||
<TruncatedText
|
||||
className="tag-description"
|
||||
text={tag.description}
|
||||
lineCount={3}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
function renderFavoriteIcon() {
|
||||
return (
|
||||
<Link to="" onClick={(e) => e.preventDefault()}>
|
||||
<Button
|
||||
className={cx(
|
||||
"minimal",
|
||||
"mousetrap",
|
||||
"favorite-button",
|
||||
tag.favorite ? "favorite" : "not-favorite"
|
||||
)}
|
||||
onClick={() => onToggleFavorite!(!tag.favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} size="2x" />
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function onToggleFavorite(v: boolean) {
|
||||
if (tag.id) {
|
||||
updateTag({
|
||||
variables: {
|
||||
input: {
|
||||
id: tag.id,
|
||||
favorite: v,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
function maybeRenderParents() {
|
||||
if (tag.parents.length === 1) {
|
||||
const parent = tag.parents[0];
|
||||
return (
|
||||
<div className="tag-parent-tags">
|
||||
<FormattedMessage
|
||||
id="sub_tag_of"
|
||||
values={{
|
||||
parent: <Link to={`/tags/${parent.id}`}>{parent.name}</Link>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tag.parents.length > 1) {
|
||||
return (
|
||||
<div className="tag-parent-tags">
|
||||
<FormattedMessage
|
||||
id="sub_tag_of"
|
||||
values={{
|
||||
parent: (
|
||||
<Link to={NavUtils.makeParentTagsUrl(tag)}>
|
||||
{tag.parents.length}
|
||||
<FormattedMessage
|
||||
id="countables.tags"
|
||||
values={{ count: tag.parents.length }}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderChildren() {
|
||||
if (tag.children.length > 0) {
|
||||
return (
|
||||
<div className="tag-sub-tags">
|
||||
<FormattedMessage
|
||||
id="parent_of"
|
||||
values={{
|
||||
children: (
|
||||
<Link to={NavUtils.makeChildTagsUrl(tag)}>
|
||||
{tag.children.length}
|
||||
<FormattedMessage
|
||||
id="countables.tags"
|
||||
values={{ count: tag.children.length }}
|
||||
/>
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderScenesPopoverButton() {
|
||||
if (!tag.scene_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="scene-count"
|
||||
type="scene"
|
||||
count={tag.scene_count}
|
||||
url={NavUtils.makeTagScenesUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderSceneMarkersPopoverButton() {
|
||||
if (!tag.scene_marker_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="marker-count"
|
||||
type="marker"
|
||||
count={tag.scene_marker_count}
|
||||
url={NavUtils.makeTagSceneMarkersUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderImagesPopoverButton() {
|
||||
if (!tag.image_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="image-count"
|
||||
type="image"
|
||||
count={tag.image_count}
|
||||
url={NavUtils.makeTagImagesUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderGalleriesPopoverButton() {
|
||||
if (!tag.gallery_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="gallery-count"
|
||||
type="gallery"
|
||||
count={tag.gallery_count}
|
||||
url={NavUtils.makeTagGalleriesUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderPerformersPopoverButton() {
|
||||
if (!tag.performer_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="performer-count"
|
||||
type="performer"
|
||||
count={tag.performer_count}
|
||||
url={NavUtils.makeTagPerformersUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderStudiosPopoverButton() {
|
||||
if (!tag.studio_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="studio-count"
|
||||
type="studio"
|
||||
count={tag.studio_count}
|
||||
url={NavUtils.makeTagStudiosUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderGroupsPopoverButton() {
|
||||
if (!tag.group_count) return;
|
||||
|
||||
return (
|
||||
<PopoverCountButton
|
||||
className="group-count"
|
||||
type="group"
|
||||
count={tag.group_count}
|
||||
url={NavUtils.makeTagGroupsUrl(tag)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderPopoverButtonGroup() {
|
||||
if (tag) {
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<ButtonGroup className="card-popovers">
|
||||
{maybeRenderScenesPopoverButton()}
|
||||
{maybeRenderImagesPopoverButton()}
|
||||
{maybeRenderGalleriesPopoverButton()}
|
||||
{maybeRenderGroupsPopoverButton()}
|
||||
{maybeRenderSceneMarkersPopoverButton()}
|
||||
{maybeRenderPerformersPopoverButton()}
|
||||
{maybeRenderStudiosPopoverButton()}
|
||||
</ButtonGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<GridCard
|
||||
className={`tag-card zoom-${zoomIndex}`}
|
||||
url={`/tags/${tag.id}`}
|
||||
width={cardWidth}
|
||||
title={tag.name ?? ""}
|
||||
title={<TagCardTitle {...props} />}
|
||||
linkClassName="tag-card-header"
|
||||
image={
|
||||
<img
|
||||
loading="lazy"
|
||||
className="tag-card-image"
|
||||
alt={tag.name}
|
||||
src={tag.image_path ?? ""}
|
||||
/>
|
||||
}
|
||||
details={
|
||||
<>
|
||||
{maybeRenderDescription()}
|
||||
{maybeRenderParents()}
|
||||
{maybeRenderChildren()}
|
||||
</>
|
||||
}
|
||||
overlays={<>{renderFavoriteIcon()}</>}
|
||||
popovers={maybeRenderPopoverButtonGroup()}
|
||||
image={<TagCardImage {...props} />}
|
||||
details={<TagCardDetails {...props} />}
|
||||
overlays={<TagCardOverlays {...props} />}
|
||||
popovers={<TagCardPopovers {...props} />}
|
||||
selected={selected}
|
||||
selecting={selecting}
|
||||
onSelectedChanged={onSelectedChanged}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ This namespace contains the generated graphql client interface. This is a low-le
|
||||
- `FontAwesomeSolid`
|
||||
- `Mousetrap`
|
||||
- `MousetrapPause`
|
||||
- `ReactSelect`
|
||||
|
||||
### `register`
|
||||
|
||||
@@ -147,21 +148,36 @@ Returns `void`.
|
||||
- `CountrySelect`
|
||||
- `DateInput`
|
||||
- `FolderSelect`
|
||||
- `FrontPage`
|
||||
- `GalleryIDSelect`
|
||||
- `GallerySelect`
|
||||
- `GallerySelect.sort`
|
||||
- `HoverPopover`
|
||||
- `Icon`
|
||||
- `ImageDetailPanel`
|
||||
- `LoadingIndicator`
|
||||
- `ModalSetting`
|
||||
- `GroupIDSelect`
|
||||
- `GroupSelect`
|
||||
- `GroupSelect.sort`
|
||||
- `NumberSetting`
|
||||
- `PerformerAppearsWithPanel`
|
||||
- `PerformerCard`
|
||||
- `PerformerCard.Details`
|
||||
- `PerformerCard.Image`
|
||||
- `PerformerCard.Overlays`
|
||||
- `PerformerCard.Popovers`
|
||||
- `PerformerCard.Title`
|
||||
- `PerformerDetailsPanel`
|
||||
- `PerformerDetailsPanel.DetailGroup`
|
||||
- `PerformerIDSelect`
|
||||
- `PerformerPage`
|
||||
- `PerformerSelect`
|
||||
- `PerformerSelect.sort`
|
||||
- `PerformerGalleriesPanel`
|
||||
- `PerformerGroupsPanel`
|
||||
- `PerformerImagesPanel`
|
||||
- `PerformerScenesPanel`
|
||||
- `PluginRoutes`
|
||||
- `SceneCard`
|
||||
- `SceneCard.Details`
|
||||
@@ -169,6 +185,10 @@ Returns `void`.
|
||||
- `SceneCard.Overlays`
|
||||
- `SceneCard.Popovers`
|
||||
- `SceneIDSelect`
|
||||
- `ScenePage`
|
||||
- `ScenePage.Tabs`
|
||||
- `ScenePage.TabContent`
|
||||
- `ScenePlayer`
|
||||
- `SceneSelect`
|
||||
- `SceneSelect.sort`
|
||||
- `SelectSetting`
|
||||
@@ -179,6 +199,15 @@ Returns `void`.
|
||||
- `StudioIDSelect`
|
||||
- `StudioSelect`
|
||||
- `StudioSelect.sort`
|
||||
- `TabTitleCounter`
|
||||
- `TagCard`
|
||||
- `TagCard.Details`
|
||||
- `TagCard.Image`
|
||||
- `TagCard.Overlays`
|
||||
- `TagCard.Popovers`
|
||||
- `TagCard.Title`
|
||||
- `TagLink`
|
||||
- `TabTitleCounter`
|
||||
- `TagIDSelect`
|
||||
- `TagSelect`
|
||||
- `TagSelect.sort`
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
import React from "react";
|
||||
import { HoverPopover } from "./components/Shared/HoverPopover";
|
||||
import { TagLink } from "./components/Shared/TagLink";
|
||||
import { LoadingIndicator } from "./components/Shared/LoadingIndicator";
|
||||
|
||||
export const components: Record<string, Function> = {
|
||||
HoverPopover,
|
||||
TagLink,
|
||||
LoadingIndicator,
|
||||
};
|
||||
export let components: Record<string, Function> = {};
|
||||
|
||||
const beforeFns: Record<string, Function[]> = {};
|
||||
const insteadFns: Record<string, Function[]> = {};
|
||||
@@ -118,10 +111,12 @@ export function PatchComponent<T>(
|
||||
}
|
||||
|
||||
// patches a component and registers it in the pluginapi components object
|
||||
export function PatchContainerComponent(
|
||||
export function PatchContainerComponent<T = {}>(
|
||||
component: string
|
||||
): React.FC<React.PropsWithChildren<{}>> {
|
||||
const fn = (props: React.PropsWithChildren<{}>) => {
|
||||
): React.FC<React.PropsWithChildren<T>> {
|
||||
const fn: React.FC<React.PropsWithChildren<T>> = (
|
||||
props: React.PropsWithChildren<T>
|
||||
) => {
|
||||
return <>{props.children}</>;
|
||||
};
|
||||
|
||||
|
||||
24
ui/v2.5/src/pluginApi.d.ts
vendored
24
ui/v2.5/src/pluginApi.d.ts
vendored
@@ -616,6 +616,7 @@ declare namespace PluginApi {
|
||||
const FontAwesomeSolid: typeof import("@fortawesome/free-solid-svg-icons");
|
||||
const Intl: typeof import("react-intl");
|
||||
const Mousetrap: typeof import("mousetrap");
|
||||
const ReactSelect: typeof import("react-select");
|
||||
|
||||
// @ts-expect-error
|
||||
import { MousetrapStatic } from "mousetrap";
|
||||
@@ -688,6 +689,29 @@ declare namespace PluginApi {
|
||||
StringListSetting: React.FC<any>;
|
||||
ConstantSetting: React.FC<any>;
|
||||
SceneFileInfoPanel: React.FC<any>;
|
||||
PerformerPage: React.FC<any>;
|
||||
PerformerAppearsWithPanel: React.FC<any>;
|
||||
PerformerGalleriesPanel: React.FC<any>;
|
||||
PerformerGroupsPanel: React.FC<any>;
|
||||
PerformerScenesPanel: React.FC<any>;
|
||||
PerformerImagesPanel: React.FC<any>;
|
||||
TabTitleCounter: React.FC<any>;
|
||||
PerformerCard: React.FC<any>;
|
||||
"PerformerCard.Popovers": React.FC<any>;
|
||||
"PerformerCard.Details": React.FC<any>;
|
||||
"PerformerCard.Overlays": React.FC<any>;
|
||||
"PerformerCard.Image": React.FC<any>;
|
||||
"PerformerCard.Title": React.FC<any>;
|
||||
"TagCard.Popovers": React.FC<any>;
|
||||
"TagCard.Details": React.FC<any>;
|
||||
"TagCard.Overlays": React.FC<any>;
|
||||
"TagCard.Image": React.FC<any>;
|
||||
"TagCard.Title": React.FC<any>;
|
||||
ScenePage: React.FC<any>;
|
||||
"ScenePage.Tabs": React.FC<any>;
|
||||
"ScenePage.TabContent": React.FC<any>;
|
||||
ScenePlayer: React.FC<any>;
|
||||
FrontPage: React.FC<any>;
|
||||
};
|
||||
type PatchableComponentNames = keyof typeof components | string;
|
||||
namespace utils {
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as Bootstrap from "react-bootstrap";
|
||||
import * as Intl from "react-intl";
|
||||
import * as FontAwesomeSolid from "@fortawesome/free-solid-svg-icons";
|
||||
import * as FontAwesomeRegular from "@fortawesome/free-regular-svg-icons";
|
||||
import * as ReactSelect from "react-select";
|
||||
import { useSpriteInfo } from "./hooks/sprite";
|
||||
import { useToast } from "./hooks/Toast";
|
||||
import Event from "./hooks/event";
|
||||
@@ -73,6 +74,7 @@ export const PluginApi = {
|
||||
FontAwesomeSolid,
|
||||
Mousetrap,
|
||||
MousetrapPause,
|
||||
ReactSelect,
|
||||
},
|
||||
register: {
|
||||
// register a route to be added to the main router
|
||||
|
||||
Reference in New Issue
Block a user