UI Plugin API (#4256)

* Add page registration
* Add example plugin
* First version of proper react plugins
* Make reference react plugin
* Add patching functions
* Add tools link poc
* NavItem poc
* Add loading hook for lazily loaded components
* Add documentation
This commit is contained in:
WithoutPants
2023-11-28 13:06:44 +11:00
committed by GitHub
parent 11be56cc42
commit b915428f06
17 changed files with 2418 additions and 384 deletions

View File

@@ -12,6 +12,7 @@ import ScraperDevelopment from "src/docs/en/Manual/ScraperDevelopment.md";
import Plugins from "src/docs/en/Manual/Plugins.md";
import ExternalPlugins from "src/docs/en/Manual/ExternalPlugins.md";
import EmbeddedPlugins from "src/docs/en/Manual/EmbeddedPlugins.md";
import UIPluginApi from "src/docs/en/Manual/UIPluginApi.md";
import Tagger from "src/docs/en/Manual/Tagger.md";
import Contributing from "src/docs/en/Manual/Contributing.md";
import SceneFilenameParser from "src/docs/en/Manual/SceneFilenameParser.md";
@@ -120,6 +121,12 @@ export const Manual: React.FC<IManualProps> = ({
content: EmbeddedPlugins,
className: "indent-1",
},
{
key: "UIPluginApi.md",
title: "UI Plugin API",
content: UIPluginApi,
className: "indent-1",
},
{
key: "Tagger.md",
title: "Scene Tagger",

View File

@@ -33,6 +33,7 @@ import {
faVideo,
} from "@fortawesome/free-solid-svg-icons";
import { baseURL } from "src/core/createClient";
import { PatchComponent } from "src/pluginApi";
interface IMenuItem {
name: string;
@@ -158,6 +159,20 @@ const newPathsList = allMenuItems
.filter((item) => item.userCreatable)
.map((item) => item.href);
const MainNavbarMenuItems = PatchComponent(
"MainNavBar.MenuItems",
(props: React.PropsWithChildren<{}>) => {
return <Nav>{props.children}</Nav>;
}
);
const MainNavbarUtilityItems = PatchComponent(
"MainNavBar.UtilityItems",
(props: React.PropsWithChildren<{}>) => {
return <Nav>{props.children}</Nav>;
}
);
export const MainNavbar: React.FC = () => {
const history = useHistory();
const location = useLocation();
@@ -335,7 +350,7 @@ export const MainNavbar: React.FC = () => {
<Navbar.Collapse className="bg-dark order-sm-1">
<Fade in={!loading}>
<>
<Nav>
<MainNavbarMenuItems>
{menuItems.map(({ href, icon, message }) => (
<Nav.Link
eventKey={href}
@@ -354,8 +369,10 @@ export const MainNavbar: React.FC = () => {
</LinkContainer>
</Nav.Link>
))}
</Nav>
<Nav>{renderUtilityButtons()}</Nav>
</MainNavbarMenuItems>
<MainNavbarUtilityItems>
{renderUtilityButtons()}
</MainNavbarUtilityItems>
</>
</Fade>
</Navbar.Collapse>
@@ -376,7 +393,9 @@ export const MainNavbar: React.FC = () => {
</Link>
</div>
)}
{renderUtilityButtons()}
<MainNavbarUtilityItems>
{renderUtilityButtons()}
</MainNavbarUtilityItems>
<Navbar.Toggle className="nav-menu-toggle ml-sm-2">
<Icon icon={expanded ? faTimes : faBars} />
</Navbar.Toggle>

View File

@@ -31,6 +31,7 @@ import {
} from "@fortawesome/free-solid-svg-icons";
import { objectPath, objectTitle } from "src/core/files";
import { PreviewScrubber } from "./PreviewScrubber";
import { PatchComponent } from "src/pluginApi";
interface IScenePreviewProps {
isPortrait: boolean;
@@ -103,372 +104,416 @@ interface ISceneCardProps {
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
}
export const SceneCard: React.FC<ISceneCardProps> = (
props: ISceneCardProps
) => {
const history = useHistory();
const { configuration } = React.useContext(ConfigurationContext);
const file = useMemo(
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),
[props.scene]
);
function maybeRenderSceneSpecsOverlay() {
let sizeObj = null;
if (file?.size) {
sizeObj = TextUtils.fileSize(file.size);
}
return (
<div className="scene-specs-overlay">
{sizeObj != null ? (
<span className="overlay-filesize extra-scene-info">
<FormattedNumber
value={sizeObj.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
sizeObj.unit
)}
/>
{TextUtils.formatFileSizeUnit(sizeObj.unit)}
</span>
) : (
""
)}
{file?.width && file?.height ? (
<span className="overlay-resolution">
{" "}
{TextUtils.resolution(file?.width, file?.height)}
</span>
) : (
""
)}
{(file?.duration ?? 0) >= 1
? TextUtils.secondsToTimestamp(file?.duration ?? 0)
: ""}
</div>
const SceneCardPopovers = PatchComponent(
"SceneCard.Popovers",
(props: ISceneCardProps) => {
const file = useMemo(
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),
[props.scene]
);
}
function maybeRenderInteractiveSpeedOverlay() {
return (
<div className="scene-interactive-speed-overlay">
{props.scene.interactive_speed ?? ""}
</div>
);
}
function maybeRenderTagPopoverButton() {
if (props.scene.tags.length <= 0) return;
function renderStudioThumbnail() {
const studioImage = props.scene.studio?.image_path;
const studioName = props.scene.studio?.name;
const popoverContent = props.scene.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} />
));
if (configuration?.interface.showStudioAsText || !studioImage) {
return studioName;
}
const studioImageURL = new URL(studioImage);
if (studioImageURL.searchParams.get("default") === "true") {
return studioName;
}
return (
<img
className="image-thumbnail"
loading="lazy"
alt={studioName}
src={studioImage}
/>
);
}
function maybeRenderSceneStudioOverlay() {
if (!props.scene.studio) return;
return (
<div className="scene-studio-overlay">
<Link to={`/studios/${props.scene.studio.id}`}>
{renderStudioThumbnail()}
</Link>
</div>
);
}
function maybeRenderTagPopoverButton() {
if (props.scene.tags.length <= 0) return;
const popoverContent = props.scene.tags.map((tag) => (
<TagLink key={tag.id} tag={tag} />
));
return (
<HoverPopover
className="tag-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<Icon icon={faTag} />
<span>{props.scene.tags.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderPerformerPopoverButton() {
if (props.scene.performers.length <= 0) return;
return <PerformerPopoverButton performers={props.scene.performers} />;
}
function maybeRenderMoviePopoverButton() {
if (props.scene.movies.length <= 0) return;
const popoverContent = props.scene.movies.map((sceneMovie) => (
<div className="movie-tag-container row" key="movie">
<Link
to={`/movies/${sceneMovie.movie.id}`}
className="movie-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
alt={sceneMovie.movie.name ?? ""}
src={sceneMovie.movie.front_image_path ?? ""}
/>
</Link>
<MovieLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
className="d-block"
/>
</div>
));
return (
<HoverPopover
placement="bottom"
content={popoverContent}
className="movie-count tag-tooltip"
>
<Button className="minimal">
<Icon icon={faFilm} />
<span>{props.scene.movies.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderSceneMarkerPopoverButton() {
if (props.scene.scene_markers.length <= 0) return;
const popoverContent = props.scene.scene_markers.map((marker) => {
const markerWithScene = { ...marker, scene: { id: props.scene.id } };
return <SceneMarkerLink key={marker.id} marker={markerWithScene} />;
});
return (
<HoverPopover
className="marker-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<Icon icon={faMapMarkerAlt} />
<span>{props.scene.scene_markers.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderOCounter() {
if (props.scene.o_counter) {
return (
<div className="o-count">
<HoverPopover
className="tag-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<span className="fa-icon">
<SweatDrops />
</span>
<span>{props.scene.o_counter}</span>
<Icon icon={faTag} />
<span>{props.scene.tags.length}</span>
</Button>
</div>
</HoverPopover>
);
}
}
function maybeRenderGallery() {
if (props.scene.galleries.length <= 0) return;
function maybeRenderPerformerPopoverButton() {
if (props.scene.performers.length <= 0) return;
const popoverContent = props.scene.galleries.map((gallery) => (
<GalleryLink key={gallery.id} gallery={gallery} />
));
return <PerformerPopoverButton performers={props.scene.performers} />;
}
return (
<HoverPopover
className="gallery-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<Icon icon={faImages} />
<span>{props.scene.galleries.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderMoviePopoverButton() {
if (props.scene.movies.length <= 0) return;
const popoverContent = props.scene.movies.map((sceneMovie) => (
<div className="movie-tag-container row" key="movie">
<Link
to={`/movies/${sceneMovie.movie.id}`}
className="movie-tag col m-auto zoom-2"
>
<img
className="image-thumbnail"
alt={sceneMovie.movie.name ?? ""}
src={sceneMovie.movie.front_image_path ?? ""}
/>
</Link>
<MovieLink
key={sceneMovie.movie.id}
movie={sceneMovie.movie}
className="d-block"
/>
</div>
));
function maybeRenderOrganized() {
if (props.scene.organized) {
return (
<OverlayTrigger
overlay={<Tooltip id="organised-tooltip">{"Organized"}</Tooltip>}
<HoverPopover
placement="bottom"
content={popoverContent}
className="movie-count tag-tooltip"
>
<div className="organized">
<Button className="minimal">
<Icon icon={faFilm} />
<span>{props.scene.movies.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderSceneMarkerPopoverButton() {
if (props.scene.scene_markers.length <= 0) return;
const popoverContent = props.scene.scene_markers.map((marker) => {
const markerWithScene = { ...marker, scene: { id: props.scene.id } };
return <SceneMarkerLink key={marker.id} marker={markerWithScene} />;
});
return (
<HoverPopover
className="marker-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<Icon icon={faMapMarkerAlt} />
<span>{props.scene.scene_markers.length}</span>
</Button>
</HoverPopover>
);
}
function maybeRenderOCounter() {
if (props.scene.o_counter) {
return (
<div className="o-count">
<Button className="minimal">
<Icon icon={faBox} />
<span className="fa-icon">
<SweatDrops />
</span>
<span>{props.scene.o_counter}</span>
</Button>
</div>
</OverlayTrigger>
);
}
}
function maybeRenderGallery() {
if (props.scene.galleries.length <= 0) return;
const popoverContent = props.scene.galleries.map((gallery) => (
<GalleryLink key={gallery.id} gallery={gallery} />
));
return (
<HoverPopover
className="gallery-count"
placement="bottom"
content={popoverContent}
>
<Button className="minimal">
<Icon icon={faImages} />
<span>{props.scene.galleries.length}</span>
</Button>
</HoverPopover>
);
}
}
function maybeRenderDupeCopies() {
const phash = file
? file.fingerprints.find((fp) => fp.type === "phash")
: undefined;
if (phash) {
return (
<div className="other-copies extra-scene-info">
<Button
href={NavUtils.makeScenesPHashMatchUrl(phash.value)}
className="minimal"
function maybeRenderOrganized() {
if (props.scene.organized) {
return (
<OverlayTrigger
overlay={<Tooltip id="organised-tooltip">{"Organized"}</Tooltip>}
placement="bottom"
>
<Icon icon={faCopy} />
</Button>
<div className="organized">
<Button className="minimal">
<Icon icon={faBox} />
</Button>
</div>
</OverlayTrigger>
);
}
}
function maybeRenderDupeCopies() {
const phash = file
? file.fingerprints.find((fp) => fp.type === "phash")
: undefined;
if (phash) {
return (
<div className="other-copies extra-scene-info">
<Button
href={NavUtils.makeScenesPHashMatchUrl(phash.value)}
className="minimal"
>
<Icon icon={faCopy} />
</Button>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
!props.compact &&
(props.scene.tags.length > 0 ||
props.scene.performers.length > 0 ||
props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 ||
props.scene?.o_counter ||
props.scene.galleries.length > 0 ||
props.scene.organized)
) {
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderMoviePopoverButton()}
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
{maybeRenderDupeCopies()}
</ButtonGroup>
</>
);
}
}
return <>{maybeRenderPopoverButtonGroup()}</>;
}
);
const SceneCardDetails = PatchComponent(
"SceneCard.Details",
(props: ISceneCardProps) => {
return (
<div className="scene-card__details">
<span className="scene-card__date">{props.scene.date}</span>
<span className="file-path extra-scene-info">
{objectPath(props.scene)}
</span>
<TruncatedText
className="scene-card__description"
text={props.scene.details}
lineCount={3}
/>
</div>
);
}
);
const SceneCardOverlays = PatchComponent(
"SceneCard.Overlays",
(props: ISceneCardProps) => {
const { configuration } = React.useContext(ConfigurationContext);
function renderStudioThumbnail() {
const studioImage = props.scene.studio?.image_path;
const studioName = props.scene.studio?.name;
if (configuration?.interface.showStudioAsText || !studioImage) {
return studioName;
}
const studioImageURL = new URL(studioImage);
if (studioImageURL.searchParams.get("default") === "true") {
return studioName;
}
return (
<img
className="image-thumbnail"
loading="lazy"
alt={studioName}
src={studioImage}
/>
);
}
function maybeRenderSceneStudioOverlay() {
if (!props.scene.studio) return;
return (
<div className="scene-studio-overlay">
<Link to={`/studios/${props.scene.studio.id}`}>
{renderStudioThumbnail()}
</Link>
</div>
);
}
}
function maybeRenderPopoverButtonGroup() {
if (
!props.compact &&
(props.scene.tags.length > 0 ||
props.scene.performers.length > 0 ||
props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 ||
props.scene?.o_counter ||
props.scene.galleries.length > 0 ||
props.scene.organized)
) {
return <>{maybeRenderSceneStudioOverlay()}</>;
}
);
const SceneCardImage = PatchComponent(
"SceneCard.Image",
(props: ISceneCardProps) => {
const history = useHistory();
const { configuration } = React.useContext(ConfigurationContext);
const cont = configuration?.interface.continuePlaylistDefault ?? false;
const file = useMemo(
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),
[props.scene]
);
function maybeRenderSceneSpecsOverlay() {
let sizeObj = null;
if (file?.size) {
sizeObj = TextUtils.fileSize(file.size);
}
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderMoviePopoverButton()}
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
{maybeRenderDupeCopies()}
</ButtonGroup>
</>
<div className="scene-specs-overlay">
{sizeObj != null ? (
<span className="overlay-filesize extra-scene-info">
<FormattedNumber
value={sizeObj.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
sizeObj.unit
)}
/>
{TextUtils.formatFileSizeUnit(sizeObj.unit)}
</span>
) : (
""
)}
{file?.width && file?.height ? (
<span className="overlay-resolution">
{" "}
{TextUtils.resolution(file?.width, file?.height)}
</span>
) : (
""
)}
{(file?.duration ?? 0) >= 1
? TextUtils.secondsToTimestamp(file?.duration ?? 0)
: ""}
</div>
);
}
}
function isPortrait() {
const width = file?.width ? file.width : 0;
const height = file?.height ? file.height : 0;
return height > width;
}
function zoomIndex() {
if (!props.compact && props.zoomIndex !== undefined) {
return `zoom-${props.zoomIndex}`;
function maybeRenderInteractiveSpeedOverlay() {
return (
<div className="scene-interactive-speed-overlay">
{props.scene.interactive_speed ?? ""}
</div>
);
}
return "";
}
function onScrubberClick(timestamp: number) {
const link = props.queue
? props.queue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
start: timestamp,
})
: `/scenes/${props.scene.id}?t=${timestamp}`;
function filelessClass() {
if (!props.scene.files.length) {
return "fileless";
history.push(link);
}
return "";
function isPortrait() {
const width = file?.width ? file.width : 0;
const height = file?.height ? file.height : 0;
return height > width;
}
return (
<>
<ScenePreview
image={props.scene.paths.screenshot ?? undefined}
video={props.scene.paths.preview ?? undefined}
isPortrait={isPortrait()}
soundActive={configuration?.interface?.soundOnPreview ?? false}
vttPath={props.scene.paths.vtt ?? undefined}
onScrubberClick={onScrubberClick}
/>
<RatingBanner rating={props.scene.rating100} />
{maybeRenderSceneSpecsOverlay()}
{maybeRenderInteractiveSpeedOverlay()}
</>
);
}
);
const cont = configuration?.interface.continuePlaylistDefault ?? false;
export const SceneCard = PatchComponent(
"SceneCard",
(props: ISceneCardProps) => {
const { configuration } = React.useContext(ConfigurationContext);
const sceneLink = props.queue
? props.queue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
})
: `/scenes/${props.scene.id}`;
const file = useMemo(
() => (props.scene.files.length > 0 ? props.scene.files[0] : undefined),
[props.scene]
);
function onScrubberClick(timestamp: number) {
const link = props.queue
function zoomIndex() {
if (!props.compact && props.zoomIndex !== undefined) {
return `zoom-${props.zoomIndex}`;
}
return "";
}
function filelessClass() {
if (!props.scene.files.length) {
return "fileless";
}
return "";
}
const cont = configuration?.interface.continuePlaylistDefault ?? false;
const sceneLink = props.queue
? props.queue.makeLink(props.scene.id, {
sceneIndex: props.index,
continue: cont,
start: timestamp,
})
: `/scenes/${props.scene.id}?t=${timestamp}`;
: `/scenes/${props.scene.id}`;
history.push(link);
return (
<GridCard
className={`scene-card ${zoomIndex()} ${filelessClass()}`}
url={sceneLink}
title={objectTitle(props.scene)}
linkClassName="scene-card-link"
thumbnailSectionClassName="video-section"
resumeTime={props.scene.resume_time ?? undefined}
duration={file?.duration ?? undefined}
interactiveHeatmap={
props.scene.interactive_speed
? props.scene.paths.interactive_heatmap ?? undefined
: undefined
}
image={<SceneCardImage {...props} />}
overlays={<SceneCardOverlays {...props} />}
details={<SceneCardDetails {...props} />}
popovers={<SceneCardPopovers {...props} />}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
/>
);
}
return (
<GridCard
className={`scene-card ${zoomIndex()} ${filelessClass()}`}
url={sceneLink}
title={objectTitle(props.scene)}
linkClassName="scene-card-link"
thumbnailSectionClassName="video-section"
resumeTime={props.scene.resume_time ?? undefined}
duration={file?.duration ?? undefined}
interactiveHeatmap={
props.scene.interactive_speed
? props.scene.paths.interactive_heatmap ?? undefined
: undefined
}
image={
<>
<ScenePreview
image={props.scene.paths.screenshot ?? undefined}
video={props.scene.paths.preview ?? undefined}
isPortrait={isPortrait()}
soundActive={configuration?.interface?.soundOnPreview ?? false}
vttPath={props.scene.paths.vtt ?? undefined}
onScrubberClick={onScrubberClick}
/>
<RatingBanner rating={props.scene.rating100} />
{maybeRenderSceneSpecsOverlay()}
{maybeRenderInteractiveSpeedOverlay()}
</>
}
overlays={maybeRenderSceneStudioOverlay()}
details={
<div className="scene-card__details">
<span className="scene-card__date">{props.scene.date}</span>
<span className="file-path extra-scene-info">
{objectPath(props.scene)}
</span>
<TruncatedText
className="scene-card__description"
text={props.scene.details}
lineCount={3}
/>
</div>
}
popovers={maybeRenderPopoverButtonGroup()}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
/>
);
};
);

View File

@@ -4,6 +4,7 @@ import { Button, Collapse, Form, Modal, ModalProps } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl";
import { Icon } from "../Shared/Icon";
import { StringListInput } from "../Shared/StringListInput";
import { PatchComponent } from "src/pluginApi";
interface ISetting {
id?: string;
@@ -17,57 +18,64 @@ interface ISetting {
disabled?: boolean;
}
export const Setting: React.FC<PropsWithChildren<ISetting>> = ({
id,
className,
heading,
headingID,
subHeadingID,
subHeading,
children,
tooltipID,
onClick,
disabled,
}) => {
const intl = useIntl();
export const Setting: React.FC<PropsWithChildren<ISetting>> = PatchComponent(
"Setting",
(props: PropsWithChildren<ISetting>) => {
const {
id,
className,
heading,
headingID,
subHeadingID,
subHeading,
children,
tooltipID,
onClick,
disabled,
} = props;
function renderHeading() {
if (headingID) {
return intl.formatMessage({ id: headingID });
const intl = useIntl();
function renderHeading() {
if (headingID) {
return intl.formatMessage({ id: headingID });
}
return heading;
}
return heading;
}
function renderSubHeading() {
if (subHeadingID) {
return (
<div className="sub-heading">
{intl.formatMessage({ id: subHeadingID })}
function renderSubHeading() {
if (subHeadingID) {
return (
<div className="sub-heading">
{intl.formatMessage({ id: subHeadingID })}
</div>
);
}
if (subHeading) {
return <div className="sub-heading">{subHeading}</div>;
}
}
const tooltip = tooltipID
? intl.formatMessage({ id: tooltipID })
: undefined;
const disabledClassName = disabled ? "disabled" : "";
return (
<div
className={`setting ${className ?? ""} ${disabledClassName}`}
id={id}
onClick={onClick}
>
<div>
<h3 title={tooltip}>{renderHeading()}</h3>
{renderSubHeading()}
</div>
);
}
if (subHeading) {
return <div className="sub-heading">{subHeading}</div>;
}
}
const tooltip = tooltipID ? intl.formatMessage({ id: tooltipID }) : undefined;
const disabledClassName = disabled ? "disabled" : "";
return (
<div
className={`setting ${className ?? ""} ${disabledClassName}`}
id={id}
onClick={onClick}
>
<div>
<h3 title={tooltip}>{renderHeading()}</h3>
{renderSubHeading()}
<div>{children}</div>
</div>
<div>{children}</div>
</div>
);
};
);
}
) as React.FC<PropsWithChildren<ISetting>>;
interface ISettingGroup {
settingProps?: ISetting;

View File

@@ -4,11 +4,14 @@ import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { Setting } from "./Inputs";
import { SettingSection } from "./SettingSection";
import { PatchContainerComponent } from "src/pluginApi";
const SettingsToolsSection = PatchContainerComponent("SettingsToolsSection");
export const SettingsToolsPanel: React.FC = () => {
return (
<>
<SettingSection headingID="config.tools.scene_tools">
<SettingSection headingID="config.tools.scene_tools">
<SettingsToolsSection>
<Setting
heading={
<Link to="/sceneFilenameParser">
@@ -28,7 +31,7 @@ export const SettingsToolsPanel: React.FC = () => {
</Link>
}
/>
</SettingSection>
</>
</SettingsToolsSection>
</SettingSection>
);
};

View File

@@ -1,6 +1,7 @@
import React from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconDefinition, SizeProp } from "@fortawesome/fontawesome-svg-core";
import { PatchComponent } from "src/pluginApi";
interface IIcon {
icon: IconDefinition;
@@ -9,11 +10,14 @@ interface IIcon {
size?: SizeProp;
}
export const Icon: React.FC<IIcon> = ({ icon, className, color, size }) => (
<FontAwesomeIcon
icon={icon}
className={`fa-icon ${className ?? ""}`}
color={color}
size={size}
/>
export const Icon: React.FC<IIcon> = PatchComponent(
"Icon",
({ icon, className, color, size }) => (
<FontAwesomeIcon
icon={icon}
className={`fa-icon ${className ?? ""}`}
color={color}
size={size}
/>
)
);