From 610366cf46c8d428cd0befee98de39344d59cfd3 Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 14:57:03 +0000 Subject: [PATCH 01/11] StudioDetailsPanel --- .../StudioDetails/StudioDetailsPanel.tsx | 139 +++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 71 insertions(+), 69 deletions(-) diff --git a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx index 4d5af043fb..5ad92100f4 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/StudioDetailsPanel.tsx @@ -3,6 +3,7 @@ import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; import { DetailItem } from "src/components/Shared/DetailItem"; import { StashIDPill } from "src/components/Shared/StashID"; +import { PatchComponent } from "src/patch"; import { Link } from "react-router-dom"; interface IStudioDetailsPanel { @@ -11,85 +12,85 @@ interface IStudioDetailsPanel { fullWidth?: boolean; } -export const StudioDetailsPanel: React.FC = ({ - studio, - fullWidth, -}) => { - function renderTagsField() { - if (!studio.tags.length) { - return; +export const StudioDetailsPanel: React.FC = PatchComponent( + "StudioDetailsPanel", + ({ studio, fullWidth }) => { + function renderTagsField() { + if (!studio.tags.length) { + return; + } + return ( +
    + {(studio.tags ?? []).map((tag) => ( + + ))} +
+ ); } - return ( -
    - {(studio.tags ?? []).map((tag) => ( - - ))} -
- ); - } - function renderStashIDs() { - if (!studio.stash_ids?.length) { - return; + function renderStashIDs() { + if (!studio.stash_ids?.length) { + return; + } + + return ( +
    + {studio.stash_ids.map((stashID) => { + return ( +
  • + +
  • + ); + })} +
+ ); } - return ( -
    - {studio.stash_ids.map((stashID) => { - return ( -
  • - -
  • - ); - })} -
- ); - } + function renderURLs() { + if (!studio.urls?.length) { + return; + } - function renderURLs() { - if (!studio.urls?.length) { - return; + return ( +
    + {studio.urls.map((url) => ( +
  • + + {url} + +
  • + ))} +
+ ); } return ( -
    - {studio.urls.map((url) => ( -
  • - - {url} - -
  • - ))} -
+
+ + + + {studio.parent_studio.name} + + ) : ( + "" + ) + } + fullWidth={fullWidth} + /> + + +
); } - - return ( -
- - - - {studio.parent_studio.name} - - ) : ( - "" - ) - } - fullWidth={fullWidth} - /> - - -
- ); -}; +); export const CompressedStudioDetailsPanel: React.FC = ({ studio, diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 8f4a896f80..4ef63e0b68 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -733,6 +733,7 @@ declare namespace PluginApi { SettingModal: React.FC; StringListSetting: React.FC; StringSetting: React.FC; + StudioDetailsPanel: React.FC; StudioIDSelect: React.FC; StudioList: React.FC; StudioSelect: React.FC; From 2e2af5014faf1fa8d831b7145d16696d981621e8 Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:07:32 +0000 Subject: [PATCH 02/11] StudioCard --- ui/v2.5/src/components/Studios/StudioCard.tsx | 316 +++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 161 insertions(+), 156 deletions(-) diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index 01b2b5c5a1..87c9b9528d 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -3,6 +3,7 @@ import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import NavUtils from "src/utils/navigation"; import { GridCard } from "src/components/Shared/GridCard/GridCard"; +import { PatchComponent } from "src/patch"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; import { TagLink } from "../Shared/TagLink"; @@ -70,179 +71,182 @@ function maybeRenderChildren(studio: GQL.StudioDataFragment) { } } -export const StudioCard: React.FC = ({ - studio, - cardWidth, - hideParent, - selecting, - selected, - zoomIndex, - onSelectedChanged, -}) => { - const [updateStudio] = useStudioUpdate(); - - function onToggleFavorite(v: boolean) { - if (studio.id) { - updateStudio({ - variables: { - input: { - id: studio.id, - favorite: v, +export const StudioCard: React.FC = PatchComponent( + "StudioCard", + ({ + studio, + cardWidth, + hideParent, + selecting, + selected, + zoomIndex, + onSelectedChanged, + }) => { + const [updateStudio] = useStudioUpdate(); + + function onToggleFavorite(v: boolean) { + if (studio.id) { + updateStudio({ + variables: { + input: { + id: studio.id, + favorite: v, + }, }, - }, - }); + }); + } } - } - - function maybeRenderScenesPopoverButton() { - if (!studio.scene_count) return; - - return ( - - ); - } - function maybeRenderImagesPopoverButton() { - if (!studio.image_count) return; + function maybeRenderScenesPopoverButton() { + if (!studio.scene_count) return; - return ( - - ); - } - - function maybeRenderGalleriesPopoverButton() { - if (!studio.gallery_count) return; + return ( + + ); + } - return ( - - ); - } + function maybeRenderImagesPopoverButton() { + if (!studio.image_count) return; - function maybeRenderGroupsPopoverButton() { - if (!studio.group_count) return; + return ( + + ); + } - return ( - - ); - } + function maybeRenderGalleriesPopoverButton() { + if (!studio.gallery_count) return; - function maybeRenderPerformersPopoverButton() { - if (!studio.performer_count) return; + return ( + + ); + } - return ( - - ); - } + function maybeRenderGroupsPopoverButton() { + if (!studio.group_count) return; - function maybeRenderTagPopoverButton() { - if (studio.tags.length <= 0) return; + return ( + + ); + } - const popoverContent = studio.tags.map((tag) => ( - - )); + function maybeRenderPerformersPopoverButton() { + if (!studio.performer_count) return; - return ( - - - - ); - } + return ( + + ); + } - function maybeRenderOCounter() { - if (!studio.o_counter) return; + function maybeRenderTagPopoverButton() { + if (studio.tags.length <= 0) return; - return ; - } + const popoverContent = studio.tags.map((tag) => ( + + )); - function maybeRenderPopoverButtonGroup() { - if ( - studio.scene_count || - studio.image_count || - studio.gallery_count || - studio.group_count || - studio.performer_count || - studio.o_counter || - studio.tags.length > 0 - ) { return ( - <> -
- - {maybeRenderScenesPopoverButton()} - {maybeRenderGroupsPopoverButton()} - {maybeRenderImagesPopoverButton()} - {maybeRenderGalleriesPopoverButton()} - {maybeRenderPerformersPopoverButton()} - {maybeRenderTagPopoverButton()} - {maybeRenderOCounter()} - - + + + ); } - } - return ( - - } - details={ -
- {maybeRenderParent(studio, hideParent)} - {maybeRenderChildren(studio)} - -
- } - overlays={ - onToggleFavorite(v)} - size="2x" - className="hide-not-favorite" - /> + function maybeRenderOCounter() { + if (!studio.o_counter) return; + + return ; + } + + function maybeRenderPopoverButtonGroup() { + if ( + studio.scene_count || + studio.image_count || + studio.gallery_count || + studio.group_count || + studio.performer_count || + studio.o_counter || + studio.tags.length > 0 + ) { + return ( + <> +
+ + {maybeRenderScenesPopoverButton()} + {maybeRenderGroupsPopoverButton()} + {maybeRenderImagesPopoverButton()} + {maybeRenderGalleriesPopoverButton()} + {maybeRenderPerformersPopoverButton()} + {maybeRenderTagPopoverButton()} + {maybeRenderOCounter()} + + + ); } - popovers={maybeRenderPopoverButtonGroup()} - selected={selected} - selecting={selecting} - onSelectedChanged={onSelectedChanged} - /> - ); -}; + } + + return ( + + } + details={ +
+ {maybeRenderParent(studio, hideParent)} + {maybeRenderChildren(studio)} + +
+ } + overlays={ + onToggleFavorite(v)} + size="2x" + className="hide-not-favorite" + /> + } + popovers={maybeRenderPopoverButtonGroup()} + selected={selected} + selecting={selecting} + onSelectedChanged={onSelectedChanged} + /> + ); + } +); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 4ef63e0b68..e2fa384c7a 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -733,6 +733,7 @@ declare namespace PluginApi { SettingModal: React.FC; StringListSetting: React.FC; StringSetting: React.FC; + StudioCard: React.FC; StudioDetailsPanel: React.FC; StudioIDSelect: React.FC; StudioList: React.FC; From c82498ebf930f3893ad19e6e7ed9733e930e41b4 Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:14:09 +0000 Subject: [PATCH 03/11] GridCard --- .../components/Shared/GridCard/GridCard.tsx | 188 +++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 99 insertions(+), 90 deletions(-) diff --git a/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx b/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx index ecb914fa11..7bdac36c69 100644 --- a/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx +++ b/ui/v2.5/src/components/Shared/GridCard/GridCard.tsx @@ -15,6 +15,7 @@ import { Icon } from "../Icon"; import { faGripLines } from "@fortawesome/free-solid-svg-icons"; import { DragSide, useDragMoveSelect } from "./dragMoveSelect"; import { useDebounce } from "src/hooks/debounce"; +import { PatchComponent } from "src/patch"; interface ICardProps { className?: string; @@ -164,106 +165,113 @@ const MoveTarget: React.FC<{ dragSide: DragSide }> = ({ dragSide }) => { ); }; -export const GridCard: React.FC = (props: ICardProps) => { - const { setInHandle, moveTarget, dragProps } = useDragMoveSelect({ - selecting: props.selecting || false, - selected: props.selected || false, - onSelectedChanged: props.onSelectedChanged, - objectId: props.objectId, - onMove: props.onMove, - }); +export const GridCard: React.FC = PatchComponent( + "GridCard", + (props: ICardProps) => { + const { setInHandle, moveTarget, dragProps } = useDragMoveSelect({ + selecting: props.selecting || false, + selected: props.selected || false, + onSelectedChanged: props.onSelectedChanged, + objectId: props.objectId, + onMove: props.onMove, + }); - function handleImageClick(event: React.MouseEvent) { - const { shiftKey } = event; + function handleImageClick( + event: React.MouseEvent + ) { + const { shiftKey } = event; - if (!props.onSelectedChanged) { - return; - } + if (!props.onSelectedChanged) { + return; + } - if (props.selecting) { - props.onSelectedChanged(!props.selected, shiftKey); - event.preventDefault(); - event.stopPropagation(); + if (props.selecting) { + props.onSelectedChanged(!props.selected, shiftKey); + event.preventDefault(); + event.stopPropagation(); + } } - } - function maybeRenderInteractiveHeatmap() { - if (props.interactiveHeatmap) { - return ( - interactive heatmap - ); + function maybeRenderInteractiveHeatmap() { + if (props.interactiveHeatmap) { + return ( + interactive heatmap + ); + } } - } - function maybeRenderProgressBar() { - if ( - props.resumeTime && - props.duration && - props.duration > props.resumeTime - ) { - const percentValue = (100 / props.duration) * props.resumeTime; - const percentStr = percentValue + "%"; - return ( -
-
-
- ); + function maybeRenderProgressBar() { + if ( + props.resumeTime && + props.duration && + props.duration > props.resumeTime + ) { + const percentValue = (100 / props.duration) * props.resumeTime; + const percentStr = percentValue + "%"; + return ( +
+
+
+ ); + } } - } - return ( - - {moveTarget !== undefined && } - - {props.onSelectedChanged && ( - - )} + return ( + + {moveTarget !== undefined && } + + {props.onSelectedChanged && ( + + )} - {!!props.objectId && props.onMove && ( - - )} - + {!!props.objectId && props.onMove && ( + + )} + -
- - {props.image} - - {props.overlays} - {maybeRenderProgressBar()} -
- {maybeRenderInteractiveHeatmap()} -
- -
- {props.pretitleIcon} - -
- - {props.details} -
+ + {props.image} + + {props.overlays} + {maybeRenderProgressBar()} +
+ {maybeRenderInteractiveHeatmap()} +
+ +
+ {props.pretitleIcon} + +
+ + {props.details} +
- {props.popovers} - - ); -}; + {props.popovers} + + ); + } +); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index e2fa384c7a..6806a8ff87 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -676,6 +676,7 @@ declare namespace PluginApi { GalleryImagesPanel: React.FC; GalleryList: React.FC; GallerySelect: React.FC; + GridCard: React.FC; GroupIDSelect: React.FC; GroupList: React.FC; GroupSelect: React.FC; From 7ee8ec0abac52414beb24adf2ac3b4ec9172c7db Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:22:26 +0000 Subject: [PATCH 04/11] ImageGridCard --- .../src/components/Images/ImageGridCard.tsx | 58 +++++++++---------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 29 insertions(+), 30 deletions(-) diff --git a/ui/v2.5/src/components/Images/ImageGridCard.tsx b/ui/v2.5/src/components/Images/ImageGridCard.tsx index cbb76d8530..3ba31e3f67 100644 --- a/ui/v2.5/src/components/Images/ImageGridCard.tsx +++ b/ui/v2.5/src/components/Images/ImageGridCard.tsx @@ -1,5 +1,6 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; +import { PatchComponent } from "src/patch"; import { ImageCard } from "./ImageCard"; import { useCardWidth, @@ -16,34 +17,31 @@ interface IImageCardGrid { const zoomWidths = [280, 340, 480, 640]; -export const ImageGridCard: React.FC = ({ - images, - selectedIds, - zoomIndex, - onSelectChange, - onPreview, -}) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); +export const ImageGridCard: React.FC = PatchComponent( + "ImageGridCard", + ({ images, selectedIds, zoomIndex, onSelectChange, onPreview }) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - return ( -
- {images.map((image, index) => ( - 0} - selected={selectedIds.has(image.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(image.id, selected, shiftKey) - } - onPreview={ - selectedIds.size < 1 ? (ev) => onPreview(index, ev) : undefined - } - /> - ))} -
- ); -}; + return ( +
+ {images.map((image, index) => ( + 0} + selected={selectedIds.has(image.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(image.id, selected, shiftKey) + } + onPreview={ + selectedIds.size < 1 ? (ev) => onPreview(index, ev) : undefined + } + /> + ))} +
+ ); + } +); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 6806a8ff87..e40aeefe2b 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -684,6 +684,7 @@ declare namespace PluginApi { HeaderImage: React.FC; HoverPopover: React.FC; Icon: React.FC; + ImageGridCard: React.FC; ImageInput: React.FC; ImageList: React.FC; LightboxLink: React.FC; From 5fdd6041cd53c7ded81ca720b3161a41207f292b Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:23:48 +0000 Subject: [PATCH 05/11] ImageCard --- ui/v2.5/src/components/Images/ImageCard.tsx | 292 ++++++++++---------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 149 insertions(+), 144 deletions(-) diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index a22e48139f..0b60a77ff9 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -15,6 +15,7 @@ import { faTag, } from "@fortawesome/free-solid-svg-icons"; import { imageTitle } from "src/core/files"; +import { PatchComponent } from "src/patch"; import { TruncatedText } from "../Shared/TruncatedText"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; import { OCounterButton } from "../Shared/CountButton"; @@ -29,168 +30,171 @@ interface IImageCardProps { onPreview?: (ev: MouseEvent) => void; } -export const ImageCard: React.FC = ( - props: IImageCardProps -) => { - const file = useMemo( - () => - props.image.visual_files.length > 0 - ? props.image.visual_files[0] - : undefined, - [props.image] - ); - - function maybeRenderTagPopoverButton() { - if (props.image.tags.length <= 0) return; +export const ImageCard: React.FC = PatchComponent( + "ImageCard", + (props: IImageCardProps) => { + const file = useMemo( + () => + props.image.visual_files.length > 0 + ? props.image.visual_files[0] + : undefined, + [props.image] + ); - const popoverContent = props.image.tags.map((tag) => ( - - )); + function maybeRenderTagPopoverButton() { + if (props.image.tags.length <= 0) return; - return ( - - - - ); - } + const popoverContent = props.image.tags.map((tag) => ( + + )); - function maybeRenderPerformerPopoverButton() { - if (props.image.performers.length <= 0) return; + return ( + + + + ); + } - return ( - - ); - } + function maybeRenderPerformerPopoverButton() { + if (props.image.performers.length <= 0) return; - function maybeRenderOCounter() { - if (props.image.o_counter) { - return ; + return ( + + ); } - } - function maybeRenderGallery() { - if (props.image.galleries.length <= 0) return; + function maybeRenderOCounter() { + if (props.image.o_counter) { + return ; + } + } - const popoverContent = props.image.galleries.map((gallery) => ( - - )); + function maybeRenderGallery() { + if (props.image.galleries.length <= 0) return; - return ( - - - - ); - } + const popoverContent = props.image.galleries.map((gallery) => ( + + )); - function maybeRenderOrganized() { - if (props.image.organized) { return ( -
+ -
+ ); } - } - function maybeRenderPopoverButtonGroup() { - if ( - props.image.tags.length > 0 || - props.image.performers.length > 0 || - props.image.o_counter || - props.image.galleries.length > 0 || - props.image.organized - ) { - return ( - <> -
- - {maybeRenderTagPopoverButton()} - {maybeRenderPerformerPopoverButton()} - {maybeRenderOCounter()} - {maybeRenderGallery()} - {maybeRenderOrganized()} - - - ); + function maybeRenderOrganized() { + if (props.image.organized) { + return ( +
+ +
+ ); + } } - } - function isPortrait() { - const width = file?.width ? file.width : 0; - const height = file?.height ? file.height : 0; - return height > width; - } + function maybeRenderPopoverButtonGroup() { + if ( + props.image.tags.length > 0 || + props.image.performers.length > 0 || + props.image.o_counter || + props.image.galleries.length > 0 || + props.image.organized + ) { + return ( + <> +
+ + {maybeRenderTagPopoverButton()} + {maybeRenderPerformerPopoverButton()} + {maybeRenderOCounter()} + {maybeRenderGallery()} + {maybeRenderOrganized()} + + + ); + } + } + + function isPortrait() { + const width = file?.width ? file.width : 0; + const height = file?.height ? file.height : 0; + return height > width; + } - const source = - props.image.paths.preview != "" - ? props.image.paths.preview ?? "" - : props.image.paths.thumbnail ?? ""; - const video = source.includes("preview"); - const ImagePreview = video ? "video" : "img"; - - return ( - -
- +
+ + {props.onPreview ? ( +
+ +
+ ) : undefined} +
+ + + } + details={ +
+ {props.image.date} + - {props.onPreview ? ( -
- -
- ) : undefined}
- - - } - details={ -
- {props.image.date} - -
- } - overlays={} - popovers={maybeRenderPopoverButtonGroup()} - selected={props.selected} - selecting={props.selecting} - onSelectedChanged={props.onSelectedChanged} - /> - ); -}; + } + overlays={} + popovers={maybeRenderPopoverButtonGroup()} + selected={props.selected} + selecting={props.selecting} + onSelectedChanged={props.onSelectedChanged} + /> + ); + } +); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index e40aeefe2b..6249367f5f 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -684,6 +684,7 @@ declare namespace PluginApi { HeaderImage: React.FC; HoverPopover: React.FC; Icon: React.FC; + ImageCard: React.FC; ImageGridCard: React.FC; ImageInput: React.FC; ImageList: React.FC; From 7617da6cbf562dfe819986d04b2809029cf0923b Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:25:15 +0000 Subject: [PATCH 06/11] ImageGridCard removed --- .../src/components/Images/ImageGridCard.tsx | 58 ++++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 - 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/ui/v2.5/src/components/Images/ImageGridCard.tsx b/ui/v2.5/src/components/Images/ImageGridCard.tsx index 3ba31e3f67..cbb76d8530 100644 --- a/ui/v2.5/src/components/Images/ImageGridCard.tsx +++ b/ui/v2.5/src/components/Images/ImageGridCard.tsx @@ -1,6 +1,5 @@ import React from "react"; import * as GQL from "src/core/generated-graphql"; -import { PatchComponent } from "src/patch"; import { ImageCard } from "./ImageCard"; import { useCardWidth, @@ -17,31 +16,34 @@ interface IImageCardGrid { const zoomWidths = [280, 340, 480, 640]; -export const ImageGridCard: React.FC = PatchComponent( - "ImageGridCard", - ({ images, selectedIds, zoomIndex, onSelectChange, onPreview }) => { - const [componentRef, { width: containerWidth }] = useContainerDimensions(); - const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); +export const ImageGridCard: React.FC = ({ + images, + selectedIds, + zoomIndex, + onSelectChange, + onPreview, +}) => { + const [componentRef, { width: containerWidth }] = useContainerDimensions(); + const cardWidth = useCardWidth(containerWidth, zoomIndex, zoomWidths); - return ( -
- {images.map((image, index) => ( - 0} - selected={selectedIds.has(image.id)} - onSelectedChanged={(selected: boolean, shiftKey: boolean) => - onSelectChange(image.id, selected, shiftKey) - } - onPreview={ - selectedIds.size < 1 ? (ev) => onPreview(index, ev) : undefined - } - /> - ))} -
- ); - } -); + return ( +
+ {images.map((image, index) => ( + 0} + selected={selectedIds.has(image.id)} + onSelectedChanged={(selected: boolean, shiftKey: boolean) => + onSelectChange(image.id, selected, shiftKey) + } + onPreview={ + selectedIds.size < 1 ? (ev) => onPreview(index, ev) : undefined + } + /> + ))} +
+ ); +}; diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 6249367f5f..28b528c613 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -685,7 +685,6 @@ declare namespace PluginApi { HoverPopover: React.FC; Icon: React.FC; ImageCard: React.FC; - ImageGridCard: React.FC; ImageInput: React.FC; ImageList: React.FC; LightboxLink: React.FC; From ea714f48d46a8224fa41371ef1b9de65a8c523c1 Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:27:02 +0000 Subject: [PATCH 07/11] GroupCard --- ui/v2.5/src/components/Groups/GroupCard.tsx | 246 ++++++++++---------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 126 insertions(+), 121 deletions(-) diff --git a/ui/v2.5/src/components/Groups/GroupCard.tsx b/ui/v2.5/src/components/Groups/GroupCard.tsx index 7412c986aa..b9b206985e 100644 --- a/ui/v2.5/src/components/Groups/GroupCard.tsx +++ b/ui/v2.5/src/components/Groups/GroupCard.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from "react"; import { Button, ButtonGroup } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; +import { PatchComponent } from "src/patch"; import { GridCard } from "../Shared/GridCard/GridCard"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; @@ -47,137 +48,140 @@ interface IProps { onMove?: (srcIds: string[], targetId: string, after: boolean) => void; } -export const GroupCard: React.FC = ({ - group, - sceneNumber, - cardWidth, - selecting, - selected, - zoomIndex, - onSelectedChanged, - fromGroupId, - onMove, -}) => { - const groupDescription = useMemo(() => { - if (!fromGroupId) { - return undefined; - } - - const containingGroup = group.containing_groups.find( - (cg) => cg.group.id === fromGroupId - ); +export const GroupCard: React.FC = PatchComponent( + "GroupCard", + ({ + group, + sceneNumber, + cardWidth, + selecting, + selected, + zoomIndex, + onSelectedChanged, + fromGroupId, + onMove, + }) => { + const groupDescription = useMemo(() => { + if (!fromGroupId) { + return undefined; + } - return containingGroup?.description ?? undefined; - }, [fromGroupId, group.containing_groups]); + const containingGroup = group.containing_groups.find( + (cg) => cg.group.id === fromGroupId + ); - function maybeRenderScenesPopoverButton() { - if (group.scenes.length === 0) return; + return containingGroup?.description ?? undefined; + }, [fromGroupId, group.containing_groups]); - const popoverContent = group.scenes.map((scene) => ( - - )); + function maybeRenderScenesPopoverButton() { + if (group.scenes.length === 0) return; - return ( - - - - ); - } + const popoverContent = group.scenes.map((scene) => ( + + )); - function maybeRenderTagPopoverButton() { - if (group.tags.length <= 0) return; - - const popoverContent = group.tags.map((tag) => ( - - )); - - return ( - - - - ); - } + return ( + + + + ); + } - function maybeRenderOCounter() { - if (!group.o_counter) return; + function maybeRenderTagPopoverButton() { + if (group.tags.length <= 0) return; - return ; - } + const popoverContent = group.tags.map((tag) => ( + + )); - function maybeRenderPopoverButtonGroup() { - if ( - sceneNumber || - groupDescription || - group.scenes.length > 0 || - group.tags.length > 0 || - group.containing_groups.length > 0 || - group.sub_group_count > 0 - ) { return ( - <> - -
- - {maybeRenderScenesPopoverButton()} - {maybeRenderTagPopoverButton()} - {(group.sub_group_count > 0 || - group.containing_groups.length > 0) && ( - - )} - {maybeRenderOCounter()} - - + + + ); } - } - return ( - - {group.name - - - } - details={ -
- {group.date} - -
+ function maybeRenderOCounter() { + if (!group.o_counter) return; + + return ; + } + + function maybeRenderPopoverButtonGroup() { + if ( + sceneNumber || + groupDescription || + group.scenes.length > 0 || + group.tags.length > 0 || + group.containing_groups.length > 0 || + group.sub_group_count > 0 + ) { + return ( + <> + +
+ + {maybeRenderScenesPopoverButton()} + {maybeRenderTagPopoverButton()} + {(group.sub_group_count > 0 || + group.containing_groups.length > 0) && ( + + )} + {maybeRenderOCounter()} + + + ); } - selected={selected} - selecting={selecting} - onSelectedChanged={onSelectedChanged} - popovers={maybeRenderPopoverButtonGroup()} - /> - ); -}; + } + + return ( + + {group.name + + + } + details={ +
+ {group.date} + +
+ } + selected={selected} + selecting={selecting} + onSelectedChanged={onSelectedChanged} + popovers={maybeRenderPopoverButtonGroup()} + /> + ); + } +); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 28b528c613..c34d188c6f 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -677,6 +677,7 @@ declare namespace PluginApi { GalleryList: React.FC; GallerySelect: React.FC; GridCard: React.FC; + GroupCard: React.FC; GroupIDSelect: React.FC; GroupList: React.FC; GroupSelect: React.FC; From faf8fcd8c24e119218fcb7921e4b616214aa9cf3 Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:38:30 +0000 Subject: [PATCH 08/11] SceneMarkerCard.Popovers --- .../src/components/Scenes/SceneMarkerCard.tsx | 104 +++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx index e76beda0ad..ff4b59a250 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx @@ -12,6 +12,7 @@ import { faTag } from "@fortawesome/free-solid-svg-icons"; import { markerTitle } from "src/core/markers"; import { Link } from "react-router-dom"; import { objectTitle } from "src/core/files"; +import { PatchComponent } from "src/patch"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { ScenePreview } from "./SceneCard"; import { TruncatedText } from "../Shared/TruncatedText"; @@ -28,63 +29,66 @@ interface ISceneMarkerCardProps { onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void; } -const SceneMarkerCardPopovers = (props: ISceneMarkerCardProps) => { - function maybeRenderPerformerPopoverButton() { - if (props.marker.scene.performers.length <= 0) return; +const SceneMarkerCardPopovers = PatchComponent( + "SceneMarkerCard.Popovers", + (props: ISceneMarkerCardProps) => { + function maybeRenderPerformerPopoverButton() { + if (props.marker.scene.performers.length <= 0) return; - return ( - - ); - } - - function renderTagPopoverButton() { - const popoverContent = [ - , - ]; - - props.marker.tags.map((tag) => - popoverContent.push( - - ) - ); + return ( + + ); + } - return ( - - - - ); - } + function renderTagPopoverButton() { + const popoverContent = [ + , + ]; + + props.marker.tags.map((tag) => + popoverContent.push( + + ) + ); - function renderPopoverButtonGroup() { - if (!props.compact) { return ( - <> -
- - {maybeRenderPerformerPopoverButton()} - {renderTagPopoverButton()} - - + + + ); } - } - return <>{renderPopoverButtonGroup()}; -}; + function renderPopoverButtonGroup() { + if (!props.compact) { + return ( + <> +
+ + {maybeRenderPerformerPopoverButton()} + {renderTagPopoverButton()} + + + ); + } + } + + return <>{renderPopoverButtonGroup()}; + } +); const SceneMarkerCardDetails = (props: ISceneMarkerCardProps) => { return ( diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index c34d188c6f..8a950604b1 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -729,6 +729,7 @@ declare namespace PluginApi { "SceneCard.Popovers": React.FC; SceneList: React.FC; SceneListOperations: React.FC; + "SceneMarkerCard.Popovers": React.FC; SceneMarkerList: React.FC; SelectSetting: React.FC; Setting: React.FC; From a77f86b725fa58376afb5c33fca99f4242f15f75 Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:39:23 +0000 Subject: [PATCH 09/11] SceneMarkerCard.Details --- .../src/components/Scenes/SceneMarkerCard.tsx | 45 ++++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx index ff4b59a250..5f67412a07 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx @@ -90,27 +90,30 @@ const SceneMarkerCardPopovers = PatchComponent( } ); -const SceneMarkerCardDetails = (props: ISceneMarkerCardProps) => { - return ( -
- - {TextUtils.formatTimestampRange( - props.marker.seconds, - props.marker.end_seconds ?? undefined - )} - - - {objectTitle(props.marker.scene)} - - } - /> -
- ); -}; +const SceneMarkerCardDetails = PatchComponent( + "SceneMarkerCard.Details", + (props: ISceneMarkerCardProps) => { + return ( +
+ + {TextUtils.formatTimestampRange( + props.marker.seconds, + props.marker.end_seconds ?? undefined + )} + + + {objectTitle(props.marker.scene)} + + } + /> +
+ ); + } +); const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => { const { configuration } = useConfigurationContext(); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 8a950604b1..d506200e5e 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -729,6 +729,7 @@ declare namespace PluginApi { "SceneCard.Popovers": React.FC; SceneList: React.FC; SceneListOperations: React.FC; + "SceneMarkerCard.Details": React.FC; "SceneMarkerCard.Popovers": React.FC; SceneMarkerList: React.FC; SelectSetting: React.FC; From e0d2d7d765ea9fccab23b6abcbed859d8c22ec0e Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:40:08 +0000 Subject: [PATCH 10/11] SceneMarkerCard.Image --- .../src/components/Scenes/SceneMarkerCard.tsx | 79 ++++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx index 5f67412a07..4246cdd52b 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx @@ -115,49 +115,52 @@ const SceneMarkerCardDetails = PatchComponent( } ); -const SceneMarkerCardImage = (props: ISceneMarkerCardProps) => { - const { configuration } = useConfigurationContext(); - - const file = useMemo( - () => - props.marker.scene.files.length > 0 - ? props.marker.scene.files[0] - : undefined, - [props.marker.scene] - ); +const SceneMarkerCardImage = PatchComponent( + "SceneMarkerCard.Image", + (props: ISceneMarkerCardProps) => { + const { configuration } = useConfigurationContext(); + + const file = useMemo( + () => + props.marker.scene.files.length > 0 + ? props.marker.scene.files[0] + : undefined, + [props.marker.scene] + ); - function isPortrait() { - const width = file?.width ? file.width : 0; - const height = file?.height ? file.height : 0; - return height > width; - } + function isPortrait() { + const width = file?.width ? file.width : 0; + const height = file?.height ? file.height : 0; + return height > width; + } + + function maybeRenderSceneSpecsOverlay() { + return ( +
+ {props.marker.end_seconds && ( + + {TextUtils.secondsToTimestamp( + props.marker.end_seconds - props.marker.seconds + )} + + )} +
+ ); + } - function maybeRenderSceneSpecsOverlay() { return ( -
- {props.marker.end_seconds && ( - - {TextUtils.secondsToTimestamp( - props.marker.end_seconds - props.marker.seconds - )} - - )} -
+ <> + + {maybeRenderSceneSpecsOverlay()} + ); } - - return ( - <> - - {maybeRenderSceneSpecsOverlay()} - - ); -}; +); export const SceneMarkerCard = (props: ISceneMarkerCardProps) => { function zoomIndex() { diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index d506200e5e..7b57ca90f1 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -730,6 +730,7 @@ declare namespace PluginApi { SceneList: React.FC; SceneListOperations: React.FC; "SceneMarkerCard.Details": React.FC; + "SceneMarkerCard.Image": React.FC; "SceneMarkerCard.Popovers": React.FC; SceneMarkerList: React.FC; SelectSetting: React.FC; From c2a7a2a4109cff8fb438a184532e0366ded9d60b Mon Sep 17 00:00:00 2001 From: ValkyrJS Date: Mon, 29 Dec 2025 15:44:05 +0000 Subject: [PATCH 11/11] SceneMarkerCard --- .../src/components/Scenes/SceneMarkerCard.tsx | 51 ++++++++++--------- ui/v2.5/src/pluginApi.d.ts | 1 + 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx index 4246cdd52b..96961d68b4 100644 --- a/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMarkerCard.tsx @@ -162,30 +162,33 @@ const SceneMarkerCardImage = PatchComponent( } ); -export const SceneMarkerCard = (props: ISceneMarkerCardProps) => { - function zoomIndex() { - if (!props.compact && props.zoomIndex !== undefined) { - return `zoom-${props.zoomIndex}`; +export const SceneMarkerCard = PatchComponent( + "SceneMarkerCard", + (props: ISceneMarkerCardProps) => { + function zoomIndex() { + if (!props.compact && props.zoomIndex !== undefined) { + return `zoom-${props.zoomIndex}`; + } + + return ""; } - return ""; + return ( + } + details={} + popovers={} + selected={props.selected} + selecting={props.selecting} + onSelectedChanged={props.onSelectedChanged} + /> + ); } - - return ( - } - details={} - popovers={} - selected={props.selected} - selecting={props.selecting} - onSelectedChanged={props.onSelectedChanged} - /> - ); -}; +); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 7b57ca90f1..1746cbc44b 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -729,6 +729,7 @@ declare namespace PluginApi { "SceneCard.Popovers": React.FC; SceneList: React.FC; SceneListOperations: React.FC; + SceneMarkerCard: React.FC; "SceneMarkerCard.Details": React.FC; "SceneMarkerCard.Image": React.FC; "SceneMarkerCard.Popovers": React.FC;