diff --git a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx index 1fddfa967..3a93abca0 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/Group.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useMemo, useState } from "react"; -import { Button } from "react-bootstrap"; import { FormattedMessage, useIntl } from "react-intl"; import { Helmet } from "react-helmet"; import cx from "classnames"; @@ -14,7 +13,6 @@ import { useHistory, RouteComponentProps } from "react-router-dom"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; -import { useLightbox } from "src/hooks/Lightbox/hooks"; import { ModalComponent } from "src/components/Shared/Modal"; import { useToast } from "src/hooks/Toast"; import { GroupScenesPanel } from "./GroupScenesPanel"; @@ -23,12 +21,7 @@ import { GroupDetailsPanel, } from "./GroupDetailsPanel"; import { GroupEditPanel } from "./GroupEditPanel"; -import { - faChevronDown, - faChevronUp, - faTrashAlt, -} from "@fortawesome/free-solid-svg-icons"; -import { Icon } from "src/components/Shared/Icon"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { ConfigurationContext } from "src/hooks/Config"; import { DetailImage } from "src/components/Shared/DetailImage"; @@ -36,6 +29,12 @@ import { useRatingKeybinds } from "src/hooks/keybinds"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton"; +import { BackgroundImage } from "src/components/Shared/DetailsPage/BackgroundImage"; +import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; +import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; +import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; +import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; +import { LightboxLink } from "src/hooks/Lightbox/LightboxLink"; interface IProps { group: GQL.GroupDataFragment; @@ -69,42 +68,64 @@ const GroupPage: React.FC = ({ group }) => { const [backImage, setBackImage] = useState(); const [encodingImage, setEncodingImage] = useState(false); - const defaultImage = - group.front_image_path && group.front_image_path.includes("default=true") - ? true - : false; + const aliases = useMemo( + () => (group.aliases ? [group.aliases] : []), + [group.aliases] + ); + + const isDefaultImage = + group.front_image_path && group.front_image_path.includes("default=true"); const lightboxImages = useMemo(() => { - const covers = [ - ...(group.front_image_path && !defaultImage - ? [ - { - paths: { - thumbnail: group.front_image_path, - image: group.front_image_path, - }, - }, - ] - : []), - ...(group.back_image_path - ? [ - { - paths: { - thumbnail: group.back_image_path, - image: group.back_image_path, - }, - }, - ] - : []), - ]; + const covers = []; + + if (group.front_image_path && !isDefaultImage) { + covers.push({ + paths: { + thumbnail: group.front_image_path, + image: group.front_image_path, + }, + }); + } + + if (group.back_image_path) { + covers.push({ + paths: { + thumbnail: group.back_image_path, + image: group.back_image_path, + }, + }); + } return covers; - }, [group.front_image_path, group.back_image_path, defaultImage]); + }, [group.front_image_path, group.back_image_path, isDefaultImage]); - const index = lightboxImages.length; + const activeFrontImage = useMemo(() => { + let existingImage = group.front_image_path; + if (isEditing) { + if (frontImage === null && existingImage) { + const imageURL = new URL(existingImage); + imageURL.searchParams.set("default", "true"); + return imageURL.toString(); + } else if (frontImage) { + return frontImage; + } + } - const showLightbox = useLightbox({ - images: lightboxImages, - }); + return existingImage; + }, [isEditing, group.front_image_path, frontImage]); + + const activeBackImage = useMemo(() => { + let existingImage = group.back_image_path; + if (isEditing) { + if (backImage === null) { + return undefined; + } else if (backImage) { + return backImage; + } + } + + return existingImage; + }, [isEditing, group.back_image_path, backImage]); const [updateGroup, { loading: updating }] = useGroupUpdate(); const [deleteGroup, { loading: deleting }] = useGroupDestroy({ @@ -196,95 +217,6 @@ const GroupPage: React.FC = ({ group }) => { ); } - function getCollapseButtonIcon() { - return collapsed ? faChevronDown : faChevronUp; - } - - function maybeRenderShowCollapseButton() { - if (!isEditing) { - return ( - - - - ); - } - } - - function renderFrontImage() { - let image = group.front_image_path; - if (isEditing) { - if (frontImage === null && image) { - const imageURL = new URL(image); - imageURL.searchParams.set("default", "true"); - image = imageURL.toString(); - } else if (frontImage) { - image = frontImage; - } - } - - if (image && defaultImage) { - return ( -
- -
- ); - } else if (image) { - return ( - - ); - } - } - - function renderBackImage() { - let image = group.back_image_path; - if (isEditing) { - if (backImage === null) { - image = undefined; - } else if (backImage) { - image = backImage; - } - } - - if (image) { - return ( - - ); - } - } - - const renderClickableIcons = () => ( - - {group.urls.length > 0 && } - - ); - - function maybeRenderAliases() { - if (group?.aliases) { - return ( -
- {group?.aliases} -
- ); - } - } - function setRating(v: number | null) { if (group.id) { updateGroup({ @@ -300,75 +232,6 @@ const GroupPage: React.FC = ({ group }) => { const renderTabs = () => ; - function maybeRenderDetails() { - if (!isEditing) { - return ( - - ); - } - } - - function maybeRenderEditPanel() { - if (isEditing) { - return ( - toggleEditing()} - onDelete={onDelete} - setFrontImage={setFrontImage} - setBackImage={setBackImage} - setEncodingImage={setEncodingImage} - /> - ); - } - { - return ( - toggleEditing()} - onSave={() => {}} - onImageChange={() => {}} - onDelete={onDelete} - /> - ); - } - } - - function maybeRenderCompressedDetails() { - if (!isEditing && loadStickyHeader) { - return ; - } - } - - function maybeRenderHeaderBackgroundImage() { - let image = group.front_image_path; - if (enableBackgroundImage && !isEditing && image) { - const imageURL = new URL(image); - let isDefaultImage = imageURL.searchParams.get("default"); - if (!isDefaultImage) { - return ( -
- - - {`${group.name} - -
- ); - } - } - } - function maybeRenderTab() { if (!isEditing) { return renderTabs(); @@ -390,43 +253,86 @@ const GroupPage: React.FC = ({ group }) => {
- {maybeRenderHeaderBackgroundImage()} +
-
-
- {encodingImage ? ( - - ) : ( -
- {renderFrontImage()} - {renderBackImage()} -
+ +
+ {!!activeFrontImage && ( + + + + )} + {!!activeBackImage && ( + + + )}
-
+
-

- {group.name} - {maybeRenderShowCollapseButton()} - {renderClickableIcons()} -

- {maybeRenderAliases()} + + {!isEditing && ( + setCollapsed(v)} + /> + )} + + + + + + setRating(value)} clickToRate withoutContext /> - {maybeRenderDetails()} - {maybeRenderEditPanel()} + {!isEditing && ( + + )} + {isEditing ? ( + toggleEditing()} + onDelete={onDelete} + setFrontImage={setFrontImage} + setBackImage={setBackImage} + setEncodingImage={setEncodingImage} + /> + ) : ( + toggleEditing()} + onSave={() => {}} + onImageChange={() => {}} + onDelete={onDelete} + /> + )}
- {maybeRenderCompressedDetails()} + + {!isEditing && loadStickyHeader && ( + + )} +
{maybeRenderTab()}
diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index eae5d121c..04e7fe912 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -277,6 +277,8 @@ export const PerformerCard: React.FC = ({ {maybeRenderRatingBanner()} {maybeRenderFlag()} diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index b5046e0fc..41a37b61d 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -1,5 +1,5 @@ -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Button, Tabs, Tab, Col, Row } from "react-bootstrap"; +import React, { useEffect, useMemo, useState } from "react"; +import { Tabs, Tab, Col, Row } from "react-bootstrap"; import { useIntl } from "react-intl"; import { useHistory, Redirect, RouteComponentProps } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -12,12 +12,9 @@ import { usePerformerDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; -import { Counter } from "src/components/Shared/Counter"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; -import { Icon } from "src/components/Shared/Icon"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; -import { useLightbox } from "src/hooks/Lightbox/hooks"; import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; @@ -32,18 +29,22 @@ import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerAppearsWithPanel } from "./performerAppearsWithPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; import { PerformerSubmitButton } from "./PerformerSubmitButton"; -import { - faChevronDown, - faChevronUp, - faHeart, - faLink, -} from "@fortawesome/free-solid-svg-icons"; -import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons"; import { useRatingKeybinds } from "src/hooks/keybinds"; import { DetailImage } from "src/components/Shared/DetailImage"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; -import { ExternalLinksButton } from "src/components/Shared/ExternalLinksButton"; +import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton"; +import { BackgroundImage } from "src/components/Shared/DetailsPage/BackgroundImage"; +import { + TabTitleCounter, + useTabKey, +} from "src/components/Shared/DetailsPage/Tabs"; +import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; +import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; +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"; interface IProps { performer: GQL.PerformerDataFragment; @@ -69,6 +70,136 @@ function isTabKey(tab: string): tab is TabKey { return validTabs.includes(tab as TabKey); } +const PerformerTabs: React.FC<{ + tabKey?: TabKey; + performer: GQL.PerformerDataFragment; + abbreviateCounter: boolean; +}> = ({ tabKey, performer, abbreviateCounter }) => { + const populatedDefaultTab = useMemo(() => { + let ret: TabKey = "scenes"; + if (performer.scene_count == 0) { + if (performer.gallery_count != 0) { + ret = "galleries"; + } else if (performer.image_count != 0) { + ret = "images"; + } else if (performer.group_count != 0) { + ret = "groups"; + } + } + + return ret; + }, [performer]); + + const { setTabKey } = useTabKey({ + tabKey, + validTabs, + defaultTabKey: populatedDefaultTab, + baseURL: `/performers/${performer.id}`, + }); + + useEffect(() => { + Mousetrap.bind("c", () => setTabKey("scenes")); + Mousetrap.bind("g", () => setTabKey("galleries")); + Mousetrap.bind("m", () => setTabKey("groups")); + + return () => { + Mousetrap.unbind("c"); + Mousetrap.unbind("g"); + Mousetrap.unbind("m"); + }; + }); + + return ( + + + } + > + + + + + } + > + + + + + } + > + + + + + } + > + + + + + } + > + + + + ); +}; + const PerformerPage: React.FC = ({ performer, tabKey }) => { const Toast = useToast(); const history = useHistory(); @@ -89,29 +220,6 @@ const PerformerPage: React.FC = ({ performer, tabKey }) => { const [encodingImage, setEncodingImage] = useState(false); const loadStickyHeader = useLoadStickyHeader(); - // a list of urls to display in the performer details - const urls = useMemo(() => { - if (!performer.urls?.length) { - return []; - } - - const twitter = performer.urls.filter((u) => - u.match(/https?:\/\/(?:www\.)?twitter.com\//) - ); - const instagram = performer.urls.filter((u) => - u.match(/https?:\/\/(?:www\.)?instagram.com\//) - ); - const others = performer.urls.filter( - (u) => !twitter.includes(u) && !instagram.includes(u) - ); - - return [ - { icon: faLink, className: "", urls: others }, - { icon: faTwitter, className: "twitter", urls: twitter }, - { icon: faInstagram, className: "instagram", urls: instagram }, - ]; - }, [performer.urls]); - const activeImage = useMemo(() => { const performerImage = performer.image_path; if (isEditing) { @@ -131,46 +239,9 @@ const PerformerPage: React.FC = ({ performer, tabKey }) => { [activeImage] ); - const showLightbox = useLightbox({ - images: lightboxImages, - }); - const [updatePerformer] = usePerformerUpdate(); const [deletePerformer, { loading: isDestroying }] = usePerformerDestroy(); - const populatedDefaultTab = useMemo(() => { - let ret: TabKey = "scenes"; - if (performer.scene_count == 0) { - if (performer.gallery_count != 0) { - ret = "galleries"; - } else if (performer.image_count != 0) { - ret = "images"; - } else if (performer.group_count != 0) { - ret = "groups"; - } - } - - return ret; - }, [performer]); - - const setTabKey = useCallback( - (newTabKey: string | null) => { - if (!newTabKey) newTabKey = populatedDefaultTab; - if (newTabKey === tabKey) return; - - if (isTabKey(newTabKey)) { - history.replace(`/performers/${performer.id}/${newTabKey}`); - } - }, - [populatedDefaultTab, tabKey, history, performer.id] - ); - - useEffect(() => { - if (!tabKey) { - setTabKey(populatedDefaultTab); - } - }, [setTabKey, populatedDefaultTab, tabKey]); - async function onAutoTag() { try { await mutateMetadataAutoTag({ performers: [performer.id] }); @@ -189,17 +260,11 @@ const PerformerPage: React.FC = ({ performer, tabKey }) => { // set up hotkeys useEffect(() => { Mousetrap.bind("e", () => toggleEditing()); - Mousetrap.bind("c", () => setTabKey("scenes")); - Mousetrap.bind("g", () => setTabKey("galleries")); - Mousetrap.bind("m", () => setTabKey("groups")); Mousetrap.bind("f", () => setFavorite(!performer.favorite)); Mousetrap.bind(",", () => setCollapsed(!collapsed)); return () => { Mousetrap.unbind("e"); - Mousetrap.unbind("c"); - Mousetrap.unbind("g"); - Mousetrap.unbind("m"); Mousetrap.unbind("f"); Mousetrap.unbind(","); }; @@ -243,221 +308,6 @@ const PerformerPage: React.FC = ({ performer, tabKey }) => { setImage(undefined); } - function renderImage() { - if (activeImage) { - return ( - - ); - } - } - const renderTabs = () => ( - - - {intl.formatMessage({ id: "scenes" })} - - - } - > - - - - {intl.formatMessage({ id: "galleries" })} - - - } - > - - - - {intl.formatMessage({ id: "images" })} - - - } - > - - - - {intl.formatMessage({ id: "groups" })} - - - } - > - - - - {intl.formatMessage({ id: "appears_with" })} - - - } - > - - - - ); - - function maybeRenderHeaderBackgroundImage() { - if (enableBackgroundImage && !isEditing && activeImage) { - const activeImageURL = new URL(activeImage); - let isDefaultImage = activeImageURL.searchParams.get("default"); - if (!isDefaultImage) { - return ( -
- - - {`${performer.name} - -
- ); - } - } - } - - function maybeRenderEditPanel() { - if (isEditing) { - return ( - toggleEditing()} - setImage={setImage} - setEncodingImage={setEncodingImage} - /> - ); - } - { - return ( - - - toggleEditing()} - onDelete={onDelete} - onAutoTag={onAutoTag} - autoTagDisabled={performer.ignore_auto_tag} - isNew={false} - isEditing={false} - onSave={() => {}} - onImageChange={() => {}} - classNames="mb-2" - customButtons={ -
- -
- } - >
-
- - ); - } - } - - function getCollapseButtonIcon() { - return collapsed ? faChevronDown : faChevronUp; - } - - function maybeRenderDetails() { - if (!isEditing) { - return ( - - ); - } - } - - function maybeRenderCompressedDetails() { - if (!isEditing && loadStickyHeader) { - return ; - } - } - - function maybeRenderTab() { - if (!isEditing) { - return renderTabs(); - } - } - - function maybeRenderAliases() { - if (performer?.alias_list?.length) { - return ( -
- {performer.alias_list?.join(", ")} -
- ); - } - } - function setFavorite(v: boolean) { if (performer.id) { updatePerformer({ @@ -484,45 +334,6 @@ const PerformerPage: React.FC = ({ performer, tabKey }) => { } } - function maybeRenderShowCollapseButton() { - if (!isEditing) { - return ( - - - - ); - } - } - - function renderClickableIcons() { - return ( - - - {urls.map((url) => ( - - ))} - - ); - } - if (isDestroying) return ( = ({ performer, tabKey }) => {
- {maybeRenderHeaderBackgroundImage()} +
-
- {encodingImage ? ( - - ) : ( - renderImage() + + {!!activeImage && ( + + + )} -
+ +
-

- {performer.name} - {performer.disambiguation && ( - - {` (${performer.disambiguation})`} - + + {!isEditing && ( + setCollapsed(v)} + /> )} - {maybeRenderShowCollapseButton()} - {renderClickableIcons()} -

- {maybeRenderAliases()} + + setFavorite(v)} + /> + + + + setRating(value)} clickToRate withoutContext /> - {maybeRenderDetails()} - {maybeRenderEditPanel()} + {!isEditing && ( + + )} + {isEditing ? ( + toggleEditing()} + setImage={setImage} + setEncodingImage={setEncodingImage} + /> + ) : ( + + + toggleEditing()} + onDelete={onDelete} + onAutoTag={onAutoTag} + autoTagDisabled={performer.ignore_auto_tag} + isNew={false} + isEditing={false} + onSave={() => {}} + onImageChange={() => {}} + classNames="mb-2" + customButtons={ +
+ +
+ } + >
+
+ + )}
- {maybeRenderCompressedDetails()} + + {!isEditing && loadStickyHeader && ( + + )} +
-
{maybeRenderTab()}
+
+ {!isEditing && ( + + )} +
diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx index 7f70cf0ed..d74338ec9 100644 --- a/ui/v2.5/src/components/Shared/CollapseButton.tsx +++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx @@ -1,6 +1,7 @@ import { faChevronDown, faChevronRight, + faChevronUp, } from "@fortawesome/free-solid-svg-icons"; import React, { useState } from "react"; import { Button, Collapse } from "react-bootstrap"; @@ -30,3 +31,21 @@ export const CollapseButton: React.FC> = (
); }; + +export const ExpandCollapseButton: React.FC<{ + collapsed: boolean; + setCollapsed: (collapsed: boolean) => void; +}> = ({ collapsed, setCollapsed }) => { + const buttonIcon = collapsed ? faChevronDown : faChevronUp; + + return ( + + + + ); +}; diff --git a/ui/v2.5/src/components/Shared/DetailsPage/AliasList.tsx b/ui/v2.5/src/components/Shared/DetailsPage/AliasList.tsx new file mode 100644 index 000000000..8f5952070 --- /dev/null +++ b/ui/v2.5/src/components/Shared/DetailsPage/AliasList.tsx @@ -0,0 +1,13 @@ +export const AliasList: React.FC<{ aliases: string[] | undefined }> = ({ + aliases, +}) => { + if (!aliases?.length) { + return null; + } + + return ( +
+ {aliases.join(", ")} +
+ ); +}; diff --git a/ui/v2.5/src/components/Shared/DetailsPage/BackgroundImage.tsx b/ui/v2.5/src/components/Shared/DetailsPage/BackgroundImage.tsx new file mode 100644 index 000000000..cd5f3ae27 --- /dev/null +++ b/ui/v2.5/src/components/Shared/DetailsPage/BackgroundImage.tsx @@ -0,0 +1,24 @@ +import React from "react"; + +export const BackgroundImage: React.FC<{ + imagePath: string | undefined; + show: boolean; + alt?: string; +}> = ({ imagePath, show, alt }) => { + if (imagePath && show) { + const imageURL = new URL(imagePath); + let isDefaultImage = imageURL.searchParams.get("default"); + if (!isDefaultImage) { + return ( +
+ + + {alt} + +
+ ); + } + } + + return null; +}; diff --git a/ui/v2.5/src/components/Shared/DetailsPage/DetailTitle.tsx b/ui/v2.5/src/components/Shared/DetailsPage/DetailTitle.tsx new file mode 100644 index 000000000..b518d66e6 --- /dev/null +++ b/ui/v2.5/src/components/Shared/DetailsPage/DetailTitle.tsx @@ -0,0 +1,21 @@ +import React, { PropsWithChildren } from "react"; + +export const DetailTitle: React.FC< + PropsWithChildren<{ + name: string; + disambiguation?: string; + classNamePrefix: string; + }> +> = ({ name, disambiguation, classNamePrefix, children }) => { + return ( +

+ {name} + {disambiguation && ( + + {` (${disambiguation})`} + + )} + {children} +

+ ); +}; diff --git a/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx b/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx new file mode 100644 index 000000000..057115308 --- /dev/null +++ b/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx @@ -0,0 +1,21 @@ +import { PropsWithChildren } from "react"; +import { LoadingIndicator } from "../LoadingIndicator"; +import { FormattedMessage } from "react-intl"; + +export const HeaderImage: React.FC< + PropsWithChildren<{ + encodingImage: boolean; + }> +> = ({ encodingImage, children }) => { + return ( +
+ {encodingImage ? ( + } + /> + ) : ( + children + )} +
+ ); +}; diff --git a/ui/v2.5/src/components/Shared/DetailsPage/Tabs.tsx b/ui/v2.5/src/components/Shared/DetailsPage/Tabs.tsx new file mode 100644 index 000000000..a29b2b5bf --- /dev/null +++ b/ui/v2.5/src/components/Shared/DetailsPage/Tabs.tsx @@ -0,0 +1,48 @@ +import { FormattedMessage } from "react-intl"; +import { Counter } from "../Counter"; +import { useCallback, useEffect } from "react"; +import { useHistory } from "react-router-dom"; + +export const TabTitleCounter: React.FC<{ + messageID: string; + count: number; + abbreviateCounter: boolean; +}> = ({ messageID, count, abbreviateCounter }) => { + return ( + <> + + + + ); +}; + +export function useTabKey(props: { + tabKey: string | undefined; + validTabs: readonly string[]; + defaultTabKey: string; + baseURL: string; +}) { + const { tabKey, validTabs, defaultTabKey, baseURL } = props; + + const history = useHistory(); + + const setTabKey = useCallback( + (newTabKey: string | null) => { + if (!newTabKey) newTabKey = defaultTabKey; + if (newTabKey === tabKey) return; + + if (validTabs.includes(newTabKey)) { + history.replace(`${baseURL}/${newTabKey}`); + } + }, + [defaultTabKey, validTabs, tabKey, history, baseURL] + ); + + useEffect(() => { + if (!tabKey) { + setTabKey(defaultTabKey); + } + }, [setTabKey, defaultTabKey, tabKey]); + + return { setTabKey }; +} diff --git a/ui/v2.5/src/components/Shared/ExternalLinksButton.tsx b/ui/v2.5/src/components/Shared/ExternalLinksButton.tsx index 7124d789c..00b318c65 100644 --- a/ui/v2.5/src/components/Shared/ExternalLinksButton.tsx +++ b/ui/v2.5/src/components/Shared/ExternalLinksButton.tsx @@ -3,6 +3,8 @@ import { ExternalLink } from "./ExternalLink"; import TextUtils from "src/utils/text"; import { Icon } from "./Icon"; import { IconDefinition, faLink } from "@fortawesome/free-solid-svg-icons"; +import { useMemo } from "react"; +import { faInstagram, faTwitter } from "@fortawesome/free-brands-svg-icons"; export const ExternalLinksButton: React.FC<{ icon?: IconDefinition; @@ -47,3 +49,37 @@ export const ExternalLinksButton: React.FC<{ ); }; + +export const ExternalLinkButtons: React.FC<{ urls: string[] | undefined }> = ({ + urls, +}) => { + const urlSpecs = useMemo(() => { + if (!urls?.length) { + return []; + } + + const twitter = urls.filter((u) => + u.match(/https?:\/\/(?:www\.)?(?:twitter|x).com\//) + ); + const instagram = urls.filter((u) => + u.match(/https?:\/\/(?:www\.)?instagram.com\//) + ); + const others = urls.filter( + (u) => !twitter.includes(u) && !instagram.includes(u) + ); + + return [ + { icon: faLink, className: "", urls: others }, + { icon: faTwitter, className: "twitter", urls: twitter }, + { icon: faInstagram, className: "instagram", urls: instagram }, + ]; + }, [urls]); + + return ( + <> + {urlSpecs.map((spec, i) => ( + + ))} + + ); +}; diff --git a/ui/v2.5/src/components/Shared/FavoriteIcon.tsx b/ui/v2.5/src/components/Shared/FavoriteIcon.tsx index 874209522..ff3db29bd 100644 --- a/ui/v2.5/src/components/Shared/FavoriteIcon.tsx +++ b/ui/v2.5/src/components/Shared/FavoriteIcon.tsx @@ -3,22 +3,26 @@ import { Icon } from "../Shared/Icon"; import { Button } from "react-bootstrap"; import { faHeart } from "@fortawesome/free-solid-svg-icons"; import cx from "classnames"; +import { SizeProp } from "@fortawesome/fontawesome-svg-core"; export const FavoriteIcon: React.FC<{ favorite: boolean; onToggleFavorite: (v: boolean) => void; -}> = ({ favorite, onToggleFavorite }) => { + size?: SizeProp; + className?: string; +}> = ({ favorite, onToggleFavorite, size, className }) => { return ( ); }; diff --git a/ui/v2.5/src/components/Shared/LoadingIndicator.tsx b/ui/v2.5/src/components/Shared/LoadingIndicator.tsx index 661722d73..e5294ec3e 100644 --- a/ui/v2.5/src/components/Shared/LoadingIndicator.tsx +++ b/ui/v2.5/src/components/Shared/LoadingIndicator.tsx @@ -4,7 +4,7 @@ import cx from "classnames"; import { useIntl } from "react-intl"; interface ILoadingProps { - message?: string; + message?: JSX.Element | string; inline?: boolean; small?: boolean; card?: boolean; diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 00ce5e663..c3bcf684d 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -536,7 +536,10 @@ button.btn.favorite-button { &.not-favorite { color: rgba(191, 204, 214, 0.5); filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9)); - opacity: 0; + + &.hide-not-favorite { + opacity: 0; + } } &.favorite { diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index e2a7bada2..e66316c74 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -236,6 +236,8 @@ export const StudioCard: React.FC = ({ onToggleFavorite(v)} + size="2x" + className="hide-not-favorite" /> } popovers={maybeRenderPopoverButtonGroup()} diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 63eb47ed4..cc4c9ac2b 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -1,5 +1,5 @@ -import { Button, Tabs, Tab } from "react-bootstrap"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Tabs, Tab } from "react-bootstrap"; +import React, { useEffect, useMemo, useState } from "react"; import { useHistory, Redirect, RouteComponentProps } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; import { Helmet } from "react-helmet"; @@ -13,14 +13,12 @@ import { useStudioDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; -import { Counter } from "src/components/Shared/Counter"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ModalComponent } from "src/components/Shared/Modal"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { useToast } from "src/hooks/Toast"; import { ConfigurationContext } from "src/hooks/Config"; -import { Icon } from "src/components/Shared/Icon"; import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel"; @@ -32,20 +30,23 @@ import { StudioDetailsPanel, } from "./StudioDetailsPanel"; import { StudioGroupsPanel } from "./StudioGroupsPanel"; -import { - faTrashAlt, - faLink, - faChevronDown, - faChevronUp, - faHeart, -} from "@fortawesome/free-solid-svg-icons"; -import TextUtils from "src/utils/text"; +import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { DetailImage } from "src/components/Shared/DetailImage"; import { useRatingKeybinds } from "src/hooks/keybinds"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; -import { ExternalLink } from "src/components/Shared/ExternalLink"; +import { BackgroundImage } from "src/components/Shared/DetailsPage/BackgroundImage"; +import { + TabTitleCounter, + useTabKey, +} from "src/components/Shared/DetailsPage/Tabs"; +import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; +import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; +import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; +import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton"; +import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; +import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; interface IProps { studio: GQL.StudioDataFragment; @@ -72,34 +73,12 @@ function isTabKey(tab: string): tab is TabKey { return validTabs.includes(tab as TabKey); } -const StudioPage: React.FC = ({ studio, tabKey }) => { - const history = useHistory(); - const Toast = useToast(); - const intl = useIntl(); - - // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); - const uiConfig = configuration?.ui; - const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; - const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; - const showAllDetails = uiConfig?.showAllDetails ?? true; - const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; - - const [collapsed, setCollapsed] = useState(!showAllDetails); - const loadStickyHeader = useLoadStickyHeader(); - - // Editing state - const [isEditing, setIsEditing] = useState(false); - const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - - // Editing studio state - const [image, setImage] = useState(); - const [encodingImage, setEncodingImage] = useState(false); - - const [updateStudio] = useStudioUpdate(); - const [deleteStudio] = useStudioDestroy({ id: studio.id }); - - const showAllCounts = uiConfig?.showChildStudioContent; +const StudioTabs: React.FC<{ + tabKey?: TabKey; + studio: GQL.StudioDataFragment; + abbreviateCounter: boolean; + showAllCounts?: boolean; +}> = ({ tabKey, studio, abbreviateCounter, showAllCounts = false }) => { const sceneCount = (showAllCounts ? studio.scene_count_all : studio.scene_count) ?? 0; const galleryCount = @@ -137,23 +116,151 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { studio, ]); - const setTabKey = useCallback( - (newTabKey: string | null) => { - if (!newTabKey) newTabKey = populatedDefaultTab; - if (newTabKey === tabKey) return; + const { setTabKey } = useTabKey({ + tabKey, + validTabs, + defaultTabKey: populatedDefaultTab, + baseURL: `/studios/${studio.id}`, + }); - if (isTabKey(newTabKey)) { - history.replace(`/studios/${studio.id}/${newTabKey}`); - } - }, - [populatedDefaultTab, tabKey, history, studio.id] + return ( + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + ); +}; - useEffect(() => { - if (!tabKey) { - setTabKey(populatedDefaultTab); +const StudioPage: React.FC = ({ studio, tabKey }) => { + const history = useHistory(); + const Toast = useToast(); + const intl = useIntl(); + + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const uiConfig = configuration?.ui; + const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const enableBackgroundImage = uiConfig?.enableStudioBackgroundImage ?? false; + const showAllDetails = uiConfig?.showAllDetails ?? true; + const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; + + const [collapsed, setCollapsed] = useState(!showAllDetails); + const loadStickyHeader = useLoadStickyHeader(); + + // Editing state + const [isEditing, setIsEditing] = useState(false); + const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + + // Editing studio state + const [image, setImage] = useState(); + const [encodingImage, setEncodingImage] = useState(false); + + const [updateStudio] = useStudioUpdate(); + const [deleteStudio] = useStudioDestroy({ id: studio.id }); + + const showAllCounts = uiConfig?.showChildStudioContent; + + // make array of url so that it doesn't re-render on every change + const urls = useMemo(() => { + return studio?.url ? [studio.url] : []; + }, [studio.url]); + + const studioImage = useMemo(() => { + const existingPath = studio.image_path; + if (isEditing) { + if (image === null && existingPath) { + const studioImageURL = new URL(existingPath); + studioImageURL.searchParams.set("default", "true"); + return studioImageURL.toString(); + } else if (image) { + return image; + } } - }, [setTabKey, populatedDefaultTab, tabKey]); + + return existingPath; + }, [isEditing, image, studio.image_path]); function setFavorite(v: boolean) { if (studio.id) { @@ -256,20 +363,6 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { ); } - function maybeRenderAliases() { - if (studio?.aliases?.length) { - return ( -
- {studio?.aliases?.join(", ")} -
- ); - } - } - - function getCollapseButtonIcon() { - return collapsed ? faChevronDown : faChevronUp; - } - function toggleEditing(value?: boolean) { if (value !== undefined) { setIsEditing(value); @@ -279,46 +372,6 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { setImage(undefined); } - function renderImage() { - let studioImage = studio.image_path; - if (isEditing) { - if (image === null && studioImage) { - const studioImageURL = new URL(studioImage); - studioImageURL.searchParams.set("default", "true"); - studioImage = studioImageURL.toString(); - } else if (image) { - studioImage = image; - } - } - - if (studioImage) { - return ( - - ); - } - } - - const renderClickableIcons = () => ( - - - {studio.url && ( - - )} - - ); - function setRating(v: number | null) { if (studio.id) { updateStudio({ @@ -332,205 +385,6 @@ const StudioPage: React.FC = ({ studio, tabKey }) => { } } - function maybeRenderDetails() { - if (!isEditing) { - return ( - - ); - } - } - - function maybeRenderShowCollapseButton() { - if (!isEditing) { - return ( - - - - ); - } - } - - function maybeRenderCompressedDetails() { - if (!isEditing && loadStickyHeader) { - return ; - } - } - - const renderTabs = () => ( - - - {intl.formatMessage({ id: "scenes" })} - - - } - > - - - - {intl.formatMessage({ id: "galleries" })} - - - } - > - - - - {intl.formatMessage({ id: "images" })} - - - } - > - - - - {intl.formatMessage({ id: "performers" })} - - - } - > - - - - {intl.formatMessage({ id: "groups" })} - - - } - > - - - - {intl.formatMessage({ id: "subsidiary_studios" })} - - - } - > - - - - ); - - function maybeRenderHeaderBackgroundImage() { - let studioImage = studio.image_path; - if (enableBackgroundImage && !isEditing && studioImage) { - const studioImageURL = new URL(studioImage); - let isDefaultImage = studioImageURL.searchParams.get("default"); - if (!isDefaultImage) { - return ( -
- - - {`${studio.name} - -
- ); - } - } - } - - function maybeRenderTab() { - if (!isEditing) { - return renderTabs(); - } - } - - function maybeRenderEditPanel() { - if (isEditing) { - return ( - toggleEditing()} - onDelete={onDelete} - setImage={setImage} - setEncodingImage={setEncodingImage} - /> - ); - } - { - return ( - toggleEditing()} - onSave={() => {}} - onImageChange={() => {}} - onClearImage={() => {}} - onAutoTag={onAutoTag} - autoTagDisabled={studio.ignore_auto_tag} - onDelete={onDelete} - /> - ); - } - } - const headerClassName = cx("detail-header", { edit: isEditing, collapsed, @@ -544,41 +398,98 @@ const StudioPage: React.FC = ({ studio, tabKey }) => {
- {maybeRenderHeaderBackgroundImage()} +
-
- {encodingImage ? ( - + {studioImage && ( + - ) : ( - renderImage() )} -
+
-

- {studio.name} - {maybeRenderShowCollapseButton()} - {renderClickableIcons()} -

- {maybeRenderAliases()} + + {!isEditing && ( + setCollapsed(v)} + /> + )} + + setFavorite(v)} + /> + + + + + setRating(value)} clickToRate withoutContext /> - {maybeRenderDetails()} - {maybeRenderEditPanel()} + {!isEditing && ( + + )} + {isEditing ? ( + toggleEditing()} + onDelete={onDelete} + setImage={setImage} + setEncodingImage={setEncodingImage} + /> + ) : ( + toggleEditing()} + onSave={() => {}} + onImageChange={() => {}} + onClearImage={() => {}} + onAutoTag={onAutoTag} + autoTagDisabled={studio.ignore_auto_tag} + onDelete={onDelete} + /> + )}
- {maybeRenderCompressedDetails()} + + {!isEditing && loadStickyHeader && ( + + )} +
-
{maybeRenderTab()}
+
+ {!isEditing && ( + + )} +
{renderDeleteAlert()} diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 13fb2664f..d392707d1 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -1,5 +1,5 @@ -import { Tabs, Tab, Dropdown, Button } from "react-bootstrap"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Tabs, Tab, Dropdown } from "react-bootstrap"; +import React, { useEffect, useMemo, useState } from "react"; import { useHistory, Redirect, RouteComponentProps } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; import { Helmet } from "react-helmet"; @@ -13,7 +13,6 @@ import { useTagDestroy, mutateMetadataAutoTag, } from "src/core/StashService"; -import { Counter } from "src/components/Shared/Counter"; import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar"; import { ErrorMessage } from "src/components/Shared/ErrorMessage"; import { ModalComponent } from "src/components/Shared/Modal"; @@ -32,9 +31,6 @@ import { CompressedTagDetailsPanel, TagDetailsPanel } from "./TagDetailsPanel"; import { TagEditPanel } from "./TagEditPanel"; import { TagMergeModal } from "./TagMergeDialog"; import { - faChevronDown, - faChevronUp, - faHeart, faSignInAlt, faSignOutAlt, faTrashAlt, @@ -43,6 +39,16 @@ import { DetailImage } from "src/components/Shared/DetailImage"; import { useLoadStickyHeader } from "src/hooks/detailsPanel"; import { useScrollToTopOnMount } from "src/hooks/scrollToTop"; import { TagGroupsPanel } from "./TagGroupsPanel"; +import { BackgroundImage } from "src/components/Shared/DetailsPage/BackgroundImage"; +import { + TabTitleCounter, + useTabKey, +} from "src/components/Shared/DetailsPage/Tabs"; +import { DetailTitle } from "src/components/Shared/DetailsPage/DetailTitle"; +import { ExpandCollapseButton } from "src/components/Shared/CollapseButton"; +import { FavoriteIcon } from "src/components/Shared/FavoriteIcon"; +import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; +import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; interface IProps { tag: GQL.TagDataFragment; @@ -70,35 +76,12 @@ function isTabKey(tab: string): tab is TabKey { return validTabs.includes(tab as TabKey); } -const TagPage: React.FC = ({ tag, tabKey }) => { - const history = useHistory(); - const Toast = useToast(); - const intl = useIntl(); - - // Configuration settings - const { configuration } = React.useContext(ConfigurationContext); - const uiConfig = configuration?.ui; - const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; - const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false; - const showAllDetails = uiConfig?.showAllDetails ?? true; - const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; - - const [collapsed, setCollapsed] = useState(!showAllDetails); - const loadStickyHeader = useLoadStickyHeader(); - - // Editing state - const [isEditing, setIsEditing] = useState(false); - const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - const [mergeType, setMergeType] = useState<"from" | "into" | undefined>(); - - // Editing tag state - const [image, setImage] = useState(); - const [encodingImage, setEncodingImage] = useState(false); - - const [updateTag] = useTagUpdate(); - const [deleteTag] = useTagDestroy({ id: tag.id }); - - const showAllCounts = uiConfig?.showChildTagContent; +const TagTabs: React.FC<{ + tabKey?: TabKey; + tag: GQL.TagDataFragment; + abbreviateCounter: boolean; + showAllCounts?: boolean; +}> = ({ tabKey, tag, abbreviateCounter, showAllCounts = false }) => { const sceneCount = (showAllCounts ? tag.scene_count_all : tag.scene_count) ?? 0; const imageCount = @@ -143,23 +126,153 @@ const TagPage: React.FC = ({ tag, tabKey }) => { groupCount, ]); - const setTabKey = useCallback( - (newTabKey: string | null) => { - if (!newTabKey) newTabKey = populatedDefaultTab; - if (newTabKey === tabKey) return; + const { setTabKey } = useTabKey({ + tabKey, + validTabs, + defaultTabKey: populatedDefaultTab, + baseURL: `/tags/${tag.id}`, + }); - if (isTabKey(newTabKey)) { - history.replace(`/tags/${tag.id}/${newTabKey}`); - } - }, - [populatedDefaultTab, tabKey, history, tag.id] + return ( + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + + } + > + + + ); +}; - useEffect(() => { - if (!tabKey) { - setTabKey(populatedDefaultTab); +const TagPage: React.FC = ({ tag, tabKey }) => { + const history = useHistory(); + const Toast = useToast(); + const intl = useIntl(); + + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const uiConfig = configuration?.ui; + const abbreviateCounter = uiConfig?.abbreviateCounters ?? false; + const enableBackgroundImage = uiConfig?.enableTagBackgroundImage ?? false; + const showAllDetails = uiConfig?.showAllDetails ?? true; + const compactExpandedDetails = uiConfig?.compactExpandedDetails ?? false; + + const [collapsed, setCollapsed] = useState(!showAllDetails); + const loadStickyHeader = useLoadStickyHeader(); + + // Editing state + const [isEditing, setIsEditing] = useState(false); + const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [mergeType, setMergeType] = useState<"from" | "into" | undefined>(); + + // Editing tag state + const [image, setImage] = useState(); + const [encodingImage, setEncodingImage] = useState(false); + + const [updateTag] = useTagUpdate(); + const [deleteTag] = useTagDestroy({ id: tag.id }); + + const showAllCounts = uiConfig?.showChildTagContent; + + const tagImage = useMemo(() => { + let existingImage = tag.image_path; + if (isEditing) { + if (image === null && existingImage) { + const tagImageURL = new URL(existingImage); + tagImageURL.searchParams.set("default", "true"); + return tagImageURL.toString(); + } else if (image) { + return image; + } } - }, [setTabKey, populatedDefaultTab, tabKey]); + + return existingImage; + }, [isEditing, tag.image_path, image]); function setFavorite(v: boolean) { if (tag.id) { @@ -279,35 +392,6 @@ const TagPage: React.FC = ({ tag, tabKey }) => { ); } - function getCollapseButtonIcon() { - return collapsed ? faChevronDown : faChevronUp; - } - - function maybeRenderShowCollapseButton() { - if (!isEditing) { - return ( - - - - ); - } - } - - function maybeRenderAliases() { - if (tag?.aliases?.length) { - return ( -
- {tag?.aliases?.join(", ")} -
- ); - } - } - function toggleEditing(value?: boolean) { if (value !== undefined) { setIsEditing(value); @@ -317,34 +401,6 @@ const TagPage: React.FC = ({ tag, tabKey }) => { setImage(undefined); } - function renderImage() { - let tagImage = tag.image_path; - if (isEditing) { - if (image === null && tagImage) { - const tagImageURL = new URL(tagImage); - tagImageURL.searchParams.set("default", "true"); - tagImage = tagImageURL.toString(); - } else if (image) { - tagImage = image; - } - } - - if (tagImage) { - return ; - } - } - - const renderClickableIcons = () => ( - - - - ); - function renderMergeButton() { return ( @@ -386,200 +442,6 @@ const TagPage: React.FC = ({ tag, tabKey }) => { ); } - function maybeRenderDetails() { - if (!isEditing) { - return ( - - ); - } - } - - function maybeRenderEditPanel() { - if (isEditing) { - return ( - toggleEditing()} - onDelete={onDelete} - setImage={setImage} - setEncodingImage={setEncodingImage} - /> - ); - } - { - return ( - toggleEditing()} - onSave={() => {}} - onImageChange={() => {}} - onClearImage={() => {}} - onAutoTag={onAutoTag} - autoTagDisabled={tag.ignore_auto_tag} - onDelete={onDelete} - classNames="mb-2" - customButtons={renderMergeButton()} - /> - ); - } - } - - const renderTabs = () => ( - - - {intl.formatMessage({ id: "scenes" })} - - - } - > - - - - {intl.formatMessage({ id: "images" })} - - - } - > - - - - {intl.formatMessage({ id: "galleries" })} - - - } - > - - - - {intl.formatMessage({ id: "groups" })} - - - } - > - - - - {intl.formatMessage({ id: "markers" })} - - - } - > - - - - {intl.formatMessage({ id: "performers" })} - - - } - > - - - - {intl.formatMessage({ id: "studios" })} - - - } - > - - - - ); - - function maybeRenderHeaderBackgroundImage() { - let tagImage = tag.image_path; - if (enableBackgroundImage && !isEditing && tagImage) { - const tagImageURL = new URL(tagImage); - let isDefaultImage = tagImageURL.searchParams.get("default"); - if (!isDefaultImage) { - return ( -
- - - {`${tag.name} - -
- ); - } - } - } - - function maybeRenderTab() { - if (!isEditing) { - return renderTabs(); - } - } - - function maybeRenderCompressedDetails() { - if (!isEditing && loadStickyHeader) { - return ; - } - } - const headerClassName = cx("detail-header", { edit: isEditing, collapsed, @@ -593,35 +455,86 @@ const TagPage: React.FC = ({ tag, tabKey }) => {
- {maybeRenderHeaderBackgroundImage()} +
-
- {encodingImage ? ( - - ) : ( - renderImage() + + {tagImage && ( + )} -
+
-

- {tag.name} - {maybeRenderShowCollapseButton()} - {renderClickableIcons()} -

- {maybeRenderAliases()} - {maybeRenderDetails()} - {maybeRenderEditPanel()} + + {!isEditing && ( + setCollapsed(v)} + /> + )} + + setFavorite(v)} + /> + + + + + {!isEditing && ( + + )} + {isEditing ? ( + toggleEditing()} + onDelete={onDelete} + setImage={setImage} + setEncodingImage={setEncodingImage} + /> + ) : ( + toggleEditing()} + onSave={() => {}} + onImageChange={() => {}} + onClearImage={() => {}} + onAutoTag={onAutoTag} + autoTagDisabled={tag.ignore_auto_tag} + onDelete={onDelete} + classNames="mb-2" + customButtons={renderMergeButton()} + /> + )}
- {maybeRenderCompressedDetails()} + + {!isEditing && loadStickyHeader && ( + + )} +
-
{maybeRenderTab()}
+
+ {!isEditing && ( + + )} +
{renderDeleteAlert()} diff --git a/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx b/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx new file mode 100644 index 000000000..9fcdee1f7 --- /dev/null +++ b/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx @@ -0,0 +1,22 @@ +import { PropsWithChildren } from "react"; +import { useLightbox } from "./hooks"; +import { ILightboxImage } from "./types"; +import { Button } from "react-bootstrap"; + +export const LightboxLink: React.FC< + PropsWithChildren<{ images?: ILightboxImage[] | undefined; index?: number }> +> = ({ images, index, children }) => { + const showLightbox = useLightbox({ + images, + }); + + if (!images || images.length === 0) { + return <>{children}; + } + + return ( + + ); +};