Plugin API - recommendation row components (#6492)

* Patched RecommendationRow component
* Patched @ant-design/react-slick library to ReactSlick
* Patched GalleryRecommendationRow component
* Patched GroupRecommendationRow component
* Patched ImageRecommendationRow component
* Patched PerformerRecommendationRow component
* Patched SceneRecommendationRow component
* Patched SceneMarkerRecommendationRow component
* Patched StudioRecommendationRow component
* Patched TagRecommendationRow component
This commit is contained in:
Valkyr-JS 2026-01-13 22:29:57 +00:00 committed by GitHub
parent 6049b21d22
commit b4969add27
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 352 additions and 297 deletions

View file

@ -1,4 +1,5 @@
import React, { PropsWithChildren } from "react";
import { PatchComponent } from "src/patch";
interface IProps {
className?: string;
@ -6,19 +7,18 @@ interface IProps {
link: JSX.Element;
}
export const RecommendationRow: React.FC<PropsWithChildren<IProps>> = ({
className,
header,
link,
children,
}) => (
<div className={`recommendation-row ${className}`}>
<div className="recommendation-row-head">
<div>
<h2>{header}</h2>
export const RecommendationRow: React.FC<PropsWithChildren<IProps>> =
PatchComponent(
"RecommendationRow",
({ className, header, link, children }) => (
<div className={`recommendation-row ${className}`}>
<div className="recommendation-row-head">
<div>
<h2>{header}</h2>
</div>
{link}
</div>
{children}
</div>
{link}
</div>
{children}
</div>
);
)
);

View file

@ -7,6 +7,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,41 +15,44 @@ interface IProps {
header: string;
}
export const GalleryRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindGalleries(props.filter);
const cardCount = result.data?.findGalleries.count;
export const GalleryRecommendationRow: React.FC<IProps> = PatchComponent(
"GalleryRecommendationRow",
(props) => {
const result = useFindGalleries(props.filter);
const cardCount = result.data?.findGalleries.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="gallery-recommendations"
header={props.header}
link={
<Link to={`/galleries?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="gallery-recommendations"
header={props.header}
link={
<Link to={`/galleries?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="gallery-skeleton skeleton-card"
></div>
))
: result.data?.findGalleries.galleries.map((g) => (
<GalleryCard key={g.id} gallery={g} zoomIndex={1} />
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="gallery-skeleton skeleton-card"
></div>
))
: result.data?.findGalleries.galleries.map((g) => (
<GalleryCard key={g.id} gallery={g} zoomIndex={1} />
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -7,6 +7,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,38 +15,44 @@ interface IProps {
header: string;
}
export const GroupRecommendationRow: React.FC<IProps> = (props: IProps) => {
const result = useFindGroups(props.filter);
const cardCount = result.data?.findGroups.count;
export const GroupRecommendationRow: React.FC<IProps> = PatchComponent(
"GroupRecommendationRow",
(props: IProps) => {
const result = useFindGroups(props.filter);
const cardCount = result.data?.findGroups.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="group-recommendations"
header={props.header}
link={
<Link to={`/groups?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="group-recommendations"
header={props.header}
link={
<Link to={`/groups?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="group-skeleton skeleton-card"></div>
))
: result.data?.findGroups.groups.map((g) => (
<GroupCard key={g.id} group={g} />
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="group-skeleton skeleton-card"
></div>
))
: result.data?.findGroups.groups.map((g) => (
<GroupCard key={g.id} group={g} />
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -7,6 +7,7 @@ import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { ImageCard } from "./ImageCard";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,38 +15,44 @@ interface IProps {
header: string;
}
export const ImageRecommendationRow: React.FC<IProps> = (props: IProps) => {
const result = useFindImages(props.filter);
const cardCount = result.data?.findImages.count;
export const ImageRecommendationRow: React.FC<IProps> = PatchComponent(
"ImageRecommendationRow",
(props: IProps) => {
const result = useFindImages(props.filter);
const cardCount = result.data?.findImages.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="images-recommendations"
header={props.header}
link={
<Link to={`/images?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="images-recommendations"
header={props.header}
link={
<Link to={`/images?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="image-skeleton skeleton-card"></div>
))
: result.data?.findImages.images.map((i) => (
<ImageCard key={i.id} image={i} zoomIndex={1} />
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="image-skeleton skeleton-card"
></div>
))
: result.data?.findImages.images.map((i) => (
<ImageCard key={i.id} image={i} zoomIndex={1} />
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -7,6 +7,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,41 +15,44 @@ interface IProps {
header: string;
}
export const PerformerRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindPerformers(props.filter);
const cardCount = result.data?.findPerformers.count;
export const PerformerRecommendationRow: React.FC<IProps> = PatchComponent(
"PerformerRecommendationRow",
(props) => {
const result = useFindPerformers(props.filter);
const cardCount = result.data?.findPerformers.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="performer-recommendations"
header={props.header}
link={
<Link to={`/performers?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="performer-recommendations"
header={props.header}
link={
<Link to={`/performers?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="performer-skeleton skeleton-card"
></div>
))
: result.data?.findPerformers.performers.map((p) => (
<PerformerCard key={p.id} performer={p} />
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="performer-skeleton skeleton-card"
></div>
))
: result.data?.findPerformers.performers.map((p) => (
<PerformerCard key={p.id} performer={p} />
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -7,6 +7,7 @@ import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { SceneMarkerCard } from "./SceneMarkerCard";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,46 +15,51 @@ interface IProps {
header: string;
}
export const SceneMarkerRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindSceneMarkers(props.filter);
const cardCount = result.data?.findSceneMarkers.count;
export const SceneMarkerRecommendationRow: React.FC<IProps> = PatchComponent(
"SceneMarkerRecommendationRow",
(props) => {
const result = useFindSceneMarkers(props.filter);
const cardCount = result.data?.findSceneMarkers.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="scene-marker-recommendations"
header={props.header}
link={
<Link to={`/scenes/markers?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="scene-marker-recommendations"
header={props.header}
link={
<Link to={`/scenes/markers?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="scene-marker-skeleton skeleton-card"
></div>
))
: result.data?.findSceneMarkers.scene_markers.map((marker, index) => (
<SceneMarkerCard
key={marker.id}
marker={marker}
index={index}
zoomIndex={1}
/>
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="scene-marker-skeleton skeleton-card"
></div>
))
: result.data?.findSceneMarkers.scene_markers.map(
(marker, index) => (
<SceneMarkerCard
key={marker.id}
marker={marker}
index={index}
zoomIndex={1}
/>
)
)}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -8,6 +8,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -15,48 +16,54 @@ interface IProps {
header: string;
}
export const SceneRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindScenes(props.filter);
const cardCount = result.data?.findScenes.count;
export const SceneRecommendationRow: React.FC<IProps> = PatchComponent(
"SceneRecommendationRow",
(props) => {
const result = useFindScenes(props.filter);
const cardCount = result.data?.findScenes.count;
const queue = useMemo(() => {
return SceneQueue.fromListFilterModel(props.filter);
}, [props.filter]);
const queue = useMemo(() => {
return SceneQueue.fromListFilterModel(props.filter);
}, [props.filter]);
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="scene-recommendations"
header={props.header}
link={
<Link to={`/scenes?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="scene-recommendations"
header={props.header}
link={
<Link to={`/scenes?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="scene-skeleton skeleton-card"></div>
))
: result.data?.findScenes.scenes.map((scene, index) => (
<SceneCard
key={scene.id}
scene={scene}
queue={queue}
index={index}
zoomIndex={1}
/>
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="scene-skeleton skeleton-card"
></div>
))
: result.data?.findScenes.scenes.map((scene, index) => (
<SceneCard
key={scene.id}
scene={scene}
queue={queue}
index={index}
zoomIndex={1}
/>
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -7,6 +7,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,41 +15,44 @@ interface IProps {
header: string;
}
export const StudioRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindStudios(props.filter);
const cardCount = result.data?.findStudios.count;
export const StudioRecommendationRow: React.FC<IProps> = PatchComponent(
"StudioRecommendationRow",
(props) => {
const result = useFindStudios(props.filter);
const cardCount = result.data?.findStudios.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="studio-recommendations"
header={props.header}
link={
<Link to={`/studios?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="studio-recommendations"
header={props.header}
link={
<Link to={`/studios?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="studio-skeleton skeleton-card"
></div>
))
: result.data?.findStudios.studios.map((s) => (
<StudioCard key={s.id} studio={s} hideParent={true} />
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div
key={`_${i}`}
className="studio-skeleton skeleton-card"
></div>
))
: result.data?.findStudios.studios.map((s) => (
<StudioCard key={s.id} studio={s} hideParent={true} />
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -7,6 +7,7 @@ import { ListFilterModel } from "src/models/list-filter/filter";
import { getSlickSliderSettings } from "src/core/recommendations";
import { RecommendationRow } from "../FrontPage/RecommendationRow";
import { FormattedMessage } from "react-intl";
import { PatchComponent } from "src/patch";
interface IProps {
isTouch: boolean;
@ -14,38 +15,41 @@ interface IProps {
header: string;
}
export const TagRecommendationRow: React.FC<IProps> = (props) => {
const result = useFindTags(props.filter);
const cardCount = result.data?.findTags.count;
export const TagRecommendationRow: React.FC<IProps> = PatchComponent(
"TagRecommendationRow",
(props) => {
const result = useFindTags(props.filter);
const cardCount = result.data?.findTags.count;
if (!result.loading && !cardCount) {
return null;
}
if (!result.loading && !cardCount) {
return null;
}
return (
<RecommendationRow
className="tag-recommendations"
header={props.header}
link={
<Link to={`/tags?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
return (
<RecommendationRow
className="tag-recommendations"
header={props.header}
link={
<Link to={`/tags?${props.filter.makeQueryParameters()}`}>
<FormattedMessage id="view_all" />
</Link>
}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="tag-skeleton skeleton-card"></div>
))
: result.data?.findTags.tags.map((p) => (
<TagCard key={p.id} tag={p} zoomIndex={0} />
))}
</Slider>
</RecommendationRow>
);
};
<Slider
{...getSlickSliderSettings(
cardCount ? cardCount : props.filter.itemsPerPage,
props.isTouch
)}
>
{result.loading
? [...Array(props.filter.itemsPerPage)].map((i) => (
<div key={`_${i}`} className="tag-skeleton skeleton-card"></div>
))
: result.data?.findTags.tags.map((p) => (
<TagCard key={p.id} tag={p} zoomIndex={0} />
))}
</Slider>
</RecommendationRow>
);
}
);

View file

@ -619,6 +619,7 @@ declare namespace PluginApi {
const Mousetrap: typeof import("mousetrap");
const ReactFontAwesome: typeof import("@fortawesome/react-fontawesome");
const ReactSelect: typeof import("react-select");
const ReactSlick: typeof import("@ant-design/react-slick");
// @ts-expect-error
import { MousetrapStatic } from "mousetrap";
@ -677,12 +678,14 @@ declare namespace PluginApi {
GalleryIDSelect: React.FC<any>;
GalleryImagesPanel: React.FC<any>;
GalleryList: React.FC<any>;
GalleryRecommendationRow: React.FC<any>;
GallerySelect: React.FC<any>;
GridCard: React.FC<any>;
GroupCard: React.FC<any>;
GroupCardGrid: React.FC<any>;
GroupIDSelect: React.FC<any>;
GroupList: React.FC<any>;
GroupRecommendationRow: React.FC<any>;
GroupSelect: React.FC<any>;
GroupSubGroupsPanel: React.FC<any>;
HeaderImage: React.FC<any>;
@ -692,6 +695,7 @@ declare namespace PluginApi {
ImageCardGrid: React.FC<any>;
ImageInput: React.FC<any>;
ImageList: React.FC<any>;
ImageRecommendationRow: React.FC<any>;
LightboxLink: React.FC<any>;
LoadingIndicator: React.FC<any>;
"MainNavBar.MenuItems": React.FC<any>;
@ -715,12 +719,14 @@ declare namespace PluginApi {
PerformerImagesPanel: React.FC<any>;
PerformerList: React.FC<any>;
PerformerPage: React.FC<any>;
PerformerRecommendationRow: React.FC<any>;
PerformerScenesPanel: React.FC<any>;
PerformerSelect: React.FC<any>;
PluginSettings: React.FC<any>;
RatingNumber: React.FC<any>;
RatingStars: React.FC<any>;
RatingSystem: React.FC<any>;
RecommendationRow: React.FC<any>;
SceneFileInfoPanel: React.FC<any>;
SceneIDSelect: React.FC<any>;
ScenePage: React.FC<any>;
@ -741,6 +747,8 @@ declare namespace PluginApi {
"SceneMarkerCard.Popovers": React.FC<any>;
SceneMarkerCardGrid: React.FC<any>;
SceneMarkerList: React.FC<any>;
SceneMarkerRecommendationRow: React.FC<any>;
SceneRecommendationRow: React.FC<any>;
SelectSetting: React.FC<any>;
Setting: React.FC<any>;
SettingGroup: React.FC<any>;
@ -752,6 +760,7 @@ declare namespace PluginApi {
StudioDetailsPanel: React.FC<any>;
StudioIDSelect: React.FC<any>;
StudioList: React.FC<any>;
StudioRecommendationRow: React.FC<any>;
StudioSelect: React.FC<any>;
SweatDrops: React.FC<any>;
TabTitleCounter: React.FC<any>;
@ -764,6 +773,7 @@ declare namespace PluginApi {
TagCardGrid: React.FC<any>;
TagLink: React.FC<any>;
TagList: React.FC<any>;
TagRecommendationRow: React.FC<any>;
TagSelect: React.FC<any>;
TruncatedText: React.FC<any>;
};

View file

@ -14,6 +14,7 @@ import * as FontAwesomeRegular from "@fortawesome/free-regular-svg-icons";
import * as FontAwesomeBrands from "@fortawesome/free-brands-svg-icons";
import * as ReactFontAwesome from "@fortawesome/react-fontawesome";
import * as ReactSelect from "react-select";
import * as ReactSlick from "@ant-design/react-slick";
import { useSpriteInfo } from "./hooks/sprite";
import { useToast } from "./hooks/Toast";
import Event from "./hooks/event";
@ -81,6 +82,7 @@ export const PluginApi = {
MousetrapPause,
ReactFontAwesome,
ReactSelect,
ReactSlick,
},
register: {
// register a route to be added to the main router