mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Various detail page refactoring (#5037)
* Refactor repeated code into BackgroundImage * Move BackgroundImage into Details folder * Refactor performer tabs * Refactor studio tabs * Refactor tag tabs * Refactor repeated code into DetailTitle * Refactor repeated collapse button code into component * Reuse FavoriteIcon in details pages * Refactor performer urls into component * Refactor alias list into component * Refactor repeated image code into HeaderImage and LightboxLink components * Replace render functions with inline conditional rendering * Support new twitter hostname
This commit is contained in:
@@ -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<IProps> = ({ group }) => {
|
||||
const [backImage, setBackImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(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<IProps> = ({ group }) => {
|
||||
);
|
||||
}
|
||||
|
||||
function getCollapseButtonIcon() {
|
||||
return collapsed ? faChevronDown : faChevronUp;
|
||||
}
|
||||
|
||||
function maybeRenderShowCollapseButton() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<span className="detail-expand-collapse">
|
||||
<Button
|
||||
className="minimal expand-collapse"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="group-image-container">
|
||||
<DetailImage alt="Front Cover" src={image} />
|
||||
</div>
|
||||
);
|
||||
} else if (image) {
|
||||
return (
|
||||
<Button
|
||||
className="group-image-container"
|
||||
variant="link"
|
||||
onClick={() => showLightbox()}
|
||||
>
|
||||
<DetailImage alt="Front Cover" src={image} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderBackImage() {
|
||||
let image = group.back_image_path;
|
||||
if (isEditing) {
|
||||
if (backImage === null) {
|
||||
image = undefined;
|
||||
} else if (backImage) {
|
||||
image = backImage;
|
||||
}
|
||||
}
|
||||
|
||||
if (image) {
|
||||
return (
|
||||
<Button
|
||||
className="group-image-container"
|
||||
variant="link"
|
||||
onClick={() => showLightbox(index - 1)}
|
||||
>
|
||||
<DetailImage alt="Back Cover" src={image} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderClickableIcons = () => (
|
||||
<span className="name-icons">
|
||||
{group.urls.length > 0 && <ExternalLinksButton urls={group.urls} />}
|
||||
</span>
|
||||
);
|
||||
|
||||
function maybeRenderAliases() {
|
||||
if (group?.aliases) {
|
||||
return (
|
||||
<div>
|
||||
<span className="alias-head">{group?.aliases}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setRating(v: number | null) {
|
||||
if (group.id) {
|
||||
updateGroup({
|
||||
@@ -300,75 +232,6 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
||||
|
||||
const renderTabs = () => <GroupScenesPanel active={true} group={group} />;
|
||||
|
||||
function maybeRenderDetails() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<GroupDetailsPanel
|
||||
group={group}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderEditPanel() {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<GroupEditPanel
|
||||
group={group}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setFrontImage={setFrontImage}
|
||||
setBackImage={setBackImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{
|
||||
return (
|
||||
<DetailsEditNavbar
|
||||
objectName={group.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderCompressedDetails() {
|
||||
if (!isEditing && loadStickyHeader) {
|
||||
return <CompressedGroupDetailsPanel group={group} />;
|
||||
}
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="background-image-container">
|
||||
<picture>
|
||||
<source src={image} />
|
||||
<img
|
||||
className="background-image"
|
||||
src={image}
|
||||
alt={`${group.name} background`}
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderTab() {
|
||||
if (!isEditing) {
|
||||
return renderTabs();
|
||||
@@ -390,43 +253,86 @@ const GroupPage: React.FC<IProps> = ({ group }) => {
|
||||
</Helmet>
|
||||
|
||||
<div className={headerClassName}>
|
||||
{maybeRenderHeaderBackgroundImage()}
|
||||
<BackgroundImage
|
||||
imagePath={group.front_image_path ?? undefined}
|
||||
show={!enableBackgroundImage && !isEditing}
|
||||
/>
|
||||
<div className="detail-container">
|
||||
<div className="detail-header-image">
|
||||
<div className="logo w-100">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||
/>
|
||||
) : (
|
||||
<div className="group-images">
|
||||
{renderFrontImage()}
|
||||
{renderBackImage()}
|
||||
</div>
|
||||
<HeaderImage encodingImage={encodingImage}>
|
||||
<div className="group-images">
|
||||
{!!activeFrontImage && (
|
||||
<LightboxLink images={lightboxImages}>
|
||||
<DetailImage alt="Front Cover" src={activeFrontImage} />
|
||||
</LightboxLink>
|
||||
)}
|
||||
{!!activeBackImage && (
|
||||
<LightboxLink
|
||||
images={lightboxImages}
|
||||
index={lightboxImages.length - 1}
|
||||
>
|
||||
<DetailImage alt="Back Cover" src={activeBackImage} />
|
||||
</LightboxLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</HeaderImage>
|
||||
<div className="row">
|
||||
<div className="group-head col">
|
||||
<h2>
|
||||
<span className="group-name">{group.name}</span>
|
||||
{maybeRenderShowCollapseButton()}
|
||||
{renderClickableIcons()}
|
||||
</h2>
|
||||
{maybeRenderAliases()}
|
||||
<DetailTitle name={group.name} classNamePrefix="group">
|
||||
{!isEditing && (
|
||||
<ExpandCollapseButton
|
||||
collapsed={collapsed}
|
||||
setCollapsed={(v) => setCollapsed(v)}
|
||||
/>
|
||||
)}
|
||||
<span className="name-icons">
|
||||
<ExternalLinksButton urls={group.urls} />
|
||||
</span>
|
||||
</DetailTitle>
|
||||
|
||||
<AliasList aliases={aliases} />
|
||||
<RatingSystem
|
||||
value={group.rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
clickToRate
|
||||
withoutContext
|
||||
/>
|
||||
{maybeRenderDetails()}
|
||||
{maybeRenderEditPanel()}
|
||||
{!isEditing && (
|
||||
<GroupDetailsPanel
|
||||
group={group}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<GroupEditPanel
|
||||
group={group}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setFrontImage={setFrontImage}
|
||||
setBackImage={setBackImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
) : (
|
||||
<DetailsEditNavbar
|
||||
objectName={group.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{maybeRenderCompressedDetails()}
|
||||
|
||||
{!isEditing && loadStickyHeader && (
|
||||
<CompressedGroupDetailsPanel group={group} />
|
||||
)}
|
||||
|
||||
<div className="detail-body">
|
||||
<div className="group-body">
|
||||
<div className="group-tabs">{maybeRenderTab()}</div>
|
||||
|
||||
@@ -277,6 +277,8 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
||||
<FavoriteIcon
|
||||
favorite={performer.favorite}
|
||||
onToggleFavorite={onToggleFavorite}
|
||||
size="2x"
|
||||
className="hide-not-favorite"
|
||||
/>
|
||||
{maybeRenderRatingBanner()}
|
||||
{maybeRenderFlag()}
|
||||
|
||||
@@ -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 (
|
||||
<Tabs
|
||||
id="performer-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={tabKey}
|
||||
onSelect={setTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="scenes"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="scenes"
|
||||
count={performer.scene_count}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PerformerScenesPanel
|
||||
active={tabKey === "scenes"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="galleries"
|
||||
count={performer.gallery_count}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PerformerGalleriesPanel
|
||||
active={tabKey === "galleries"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="images"
|
||||
count={performer.image_count}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PerformerImagesPanel
|
||||
active={tabKey === "images"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="groups"
|
||||
count={performer.group_count}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PerformerGroupsPanel
|
||||
active={tabKey === "groups"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
eventKey="appearswith"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="appears_with"
|
||||
count={performer.performer_count}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<PerformerAppearsWithPanel
|
||||
active={tabKey === "appearswith"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
||||
const Toast = useToast();
|
||||
const history = useHistory();
|
||||
@@ -89,29 +220,6 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(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<IProps> = ({ 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<IProps> = ({ 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<IProps> = ({ performer, tabKey }) => {
|
||||
setImage(undefined);
|
||||
}
|
||||
|
||||
function renderImage() {
|
||||
if (activeImage) {
|
||||
return (
|
||||
<Button variant="link" onClick={() => showLightbox()}>
|
||||
<DetailImage
|
||||
className="performer"
|
||||
src={activeImage}
|
||||
alt={performer.name}
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
const renderTabs = () => (
|
||||
<Tabs
|
||||
id="performer-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={tabKey}
|
||||
onSelect={setTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="scenes"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "scenes" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.scene_count}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PerformerScenesPanel
|
||||
active={tabKey === "scenes"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "galleries" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.gallery_count}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PerformerGalleriesPanel
|
||||
active={tabKey === "galleries"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "images" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.image_count}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PerformerImagesPanel
|
||||
active={tabKey === "images"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "groups" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.group_count}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PerformerGroupsPanel
|
||||
active={tabKey === "groups"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="appearswith"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "appears_with" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.performer_count}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<PerformerAppearsWithPanel
|
||||
active={tabKey === "appearswith"}
|
||||
performer={performer}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
function maybeRenderHeaderBackgroundImage() {
|
||||
if (enableBackgroundImage && !isEditing && activeImage) {
|
||||
const activeImageURL = new URL(activeImage);
|
||||
let isDefaultImage = activeImageURL.searchParams.get("default");
|
||||
if (!isDefaultImage) {
|
||||
return (
|
||||
<div className="background-image-container">
|
||||
<picture>
|
||||
<source src={activeImage} />
|
||||
<img
|
||||
className="background-image"
|
||||
src={activeImage}
|
||||
alt={`${performer.name} background`}
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderEditPanel() {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{
|
||||
return (
|
||||
<Col>
|
||||
<Row xs={8}>
|
||||
<DetailsEditNavbar
|
||||
objectName={
|
||||
performer?.name ?? intl.formatMessage({ id: "performer" })
|
||||
}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={performer.ignore_auto_tag}
|
||||
isNew={false}
|
||||
isEditing={false}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
classNames="mb-2"
|
||||
customButtons={
|
||||
<div>
|
||||
<PerformerSubmitButton performer={performer} />
|
||||
</div>
|
||||
}
|
||||
></DetailsEditNavbar>
|
||||
</Row>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getCollapseButtonIcon() {
|
||||
return collapsed ? faChevronDown : faChevronUp;
|
||||
}
|
||||
|
||||
function maybeRenderDetails() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderCompressedDetails() {
|
||||
if (!isEditing && loadStickyHeader) {
|
||||
return <CompressedPerformerDetailsPanel performer={performer} />;
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderTab() {
|
||||
if (!isEditing) {
|
||||
return renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderAliases() {
|
||||
if (performer?.alias_list?.length) {
|
||||
return (
|
||||
<div>
|
||||
<span className="alias-head">{performer.alias_list?.join(", ")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function setFavorite(v: boolean) {
|
||||
if (performer.id) {
|
||||
updatePerformer({
|
||||
@@ -484,45 +334,6 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderShowCollapseButton() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<span className="detail-expand-collapse">
|
||||
<Button
|
||||
className="minimal expand-collapse"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderClickableIcons() {
|
||||
return (
|
||||
<span className="name-icons">
|
||||
<Button
|
||||
className={cx(
|
||||
"minimal",
|
||||
performer.favorite ? "favorite" : "not-favorite"
|
||||
)}
|
||||
onClick={() => setFavorite(!performer.favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} />
|
||||
</Button>
|
||||
{urls.map((url) => (
|
||||
<ExternalLinksButton
|
||||
key={url.icon.iconName}
|
||||
icon={url.icon}
|
||||
className={url.className}
|
||||
urls={url.urls}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDestroying)
|
||||
return (
|
||||
<LoadingIndicator
|
||||
@@ -543,46 +354,113 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
||||
</Helmet>
|
||||
|
||||
<div className={headerClassName}>
|
||||
{maybeRenderHeaderBackgroundImage()}
|
||||
<BackgroundImage
|
||||
imagePath={activeImage ?? undefined}
|
||||
show={enableBackgroundImage && !isEditing}
|
||||
/>
|
||||
<div className="detail-container">
|
||||
<div className="detail-header-image">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||
/>
|
||||
) : (
|
||||
renderImage()
|
||||
<HeaderImage encodingImage={encodingImage}>
|
||||
{!!activeImage && (
|
||||
<LightboxLink images={lightboxImages}>
|
||||
<DetailImage
|
||||
className="performer"
|
||||
src={activeImage}
|
||||
alt={performer.name}
|
||||
/>
|
||||
</LightboxLink>
|
||||
)}
|
||||
</div>
|
||||
</HeaderImage>
|
||||
|
||||
<div className="row">
|
||||
<div className="performer-head col">
|
||||
<h2>
|
||||
<span className="performer-name">{performer.name}</span>
|
||||
{performer.disambiguation && (
|
||||
<span className="performer-disambiguation">
|
||||
{` (${performer.disambiguation})`}
|
||||
</span>
|
||||
<DetailTitle
|
||||
name={performer.name}
|
||||
disambiguation={performer.disambiguation ?? undefined}
|
||||
classNamePrefix="performer"
|
||||
>
|
||||
{!isEditing && (
|
||||
<ExpandCollapseButton
|
||||
collapsed={collapsed}
|
||||
setCollapsed={(v) => setCollapsed(v)}
|
||||
/>
|
||||
)}
|
||||
{maybeRenderShowCollapseButton()}
|
||||
{renderClickableIcons()}
|
||||
</h2>
|
||||
{maybeRenderAliases()}
|
||||
<span className="name-icons">
|
||||
<FavoriteIcon
|
||||
favorite={performer.favorite}
|
||||
onToggleFavorite={(v) => setFavorite(v)}
|
||||
/>
|
||||
<ExternalLinkButtons urls={performer.urls ?? undefined} />
|
||||
</span>
|
||||
</DetailTitle>
|
||||
<AliasList aliases={performer.alias_list} />
|
||||
<RatingSystem
|
||||
value={performer.rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
clickToRate
|
||||
withoutContext
|
||||
/>
|
||||
{maybeRenderDetails()}
|
||||
{maybeRenderEditPanel()}
|
||||
{!isEditing && (
|
||||
<PerformerDetailsPanel
|
||||
performer={performer}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
isVisible={isEditing}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
) : (
|
||||
<Col>
|
||||
<Row xs={8}>
|
||||
<DetailsEditNavbar
|
||||
objectName={
|
||||
performer?.name ??
|
||||
intl.formatMessage({ id: "performer" })
|
||||
}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={performer.ignore_auto_tag}
|
||||
isNew={false}
|
||||
isEditing={false}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
classNames="mb-2"
|
||||
customButtons={
|
||||
<div>
|
||||
<PerformerSubmitButton performer={performer} />
|
||||
</div>
|
||||
}
|
||||
></DetailsEditNavbar>
|
||||
</Row>
|
||||
</Col>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{maybeRenderCompressedDetails()}
|
||||
|
||||
{!isEditing && loadStickyHeader && (
|
||||
<CompressedPerformerDetailsPanel performer={performer} />
|
||||
)}
|
||||
|
||||
<div className="detail-body">
|
||||
<div className="performer-body">
|
||||
<div className="performer-tabs">{maybeRenderTab()}</div>
|
||||
<div className="performer-tabs">
|
||||
{!isEditing && (
|
||||
<PerformerTabs
|
||||
tabKey={tabKey}
|
||||
performer={performer}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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<React.PropsWithChildren<IProps>> = (
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandCollapseButton: React.FC<{
|
||||
collapsed: boolean;
|
||||
setCollapsed: (collapsed: boolean) => void;
|
||||
}> = ({ collapsed, setCollapsed }) => {
|
||||
const buttonIcon = collapsed ? faChevronDown : faChevronUp;
|
||||
|
||||
return (
|
||||
<span className="detail-expand-collapse">
|
||||
<Button
|
||||
className="minimal expand-collapse"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<Icon className="fa-fw" icon={buttonIcon} />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
13
ui/v2.5/src/components/Shared/DetailsPage/AliasList.tsx
Normal file
13
ui/v2.5/src/components/Shared/DetailsPage/AliasList.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
export const AliasList: React.FC<{ aliases: string[] | undefined }> = ({
|
||||
aliases,
|
||||
}) => {
|
||||
if (!aliases?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span className="alias-head">{aliases.join(", ")}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div className="background-image-container">
|
||||
<picture>
|
||||
<source src={imagePath} />
|
||||
<img className="background-image" src={imagePath} alt={alt} />
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
21
ui/v2.5/src/components/Shared/DetailsPage/DetailTitle.tsx
Normal file
21
ui/v2.5/src/components/Shared/DetailsPage/DetailTitle.tsx
Normal file
@@ -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 (
|
||||
<h2>
|
||||
<span className={`${classNamePrefix}-name`}>{name}</span>
|
||||
{disambiguation && (
|
||||
<span className={`${classNamePrefix}-disambiguation`}>
|
||||
{` (${disambiguation})`}
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
};
|
||||
21
ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx
Normal file
21
ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx
Normal file
@@ -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 (
|
||||
<div className="detail-header-image">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator
|
||||
message={<FormattedMessage id="actions.encoding_image" />}
|
||||
/>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
48
ui/v2.5/src/components/Shared/DetailsPage/Tabs.tsx
Normal file
48
ui/v2.5/src/components/Shared/DetailsPage/Tabs.tsx
Normal file
@@ -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 (
|
||||
<>
|
||||
<FormattedMessage id={messageID} />
|
||||
<Counter count={count} abbreviateCounter={abbreviateCounter} hideZero />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
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 };
|
||||
}
|
||||
@@ -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<{
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
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) => (
|
||||
<ExternalLinksButton key={i} {...spec} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 (
|
||||
<Button
|
||||
className={cx(
|
||||
"minimal",
|
||||
"mousetrap",
|
||||
"favorite-button",
|
||||
className,
|
||||
favorite ? "favorite" : "not-favorite"
|
||||
)}
|
||||
onClick={() => onToggleFavorite!(!favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} size="2x" />
|
||||
<Icon icon={faHeart} size={size} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -236,6 +236,8 @@ export const StudioCard: React.FC<IProps> = ({
|
||||
<FavoriteIcon
|
||||
favorite={studio.favorite}
|
||||
onToggleFavorite={(v) => onToggleFavorite(v)}
|
||||
size="2x"
|
||||
className="hide-not-favorite"
|
||||
/>
|
||||
}
|
||||
popovers={maybeRenderPopoverButtonGroup()}
|
||||
|
||||
@@ -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<IProps> = ({ 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<boolean>(!showAllDetails);
|
||||
const loadStickyHeader = useLoadStickyHeader();
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(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<IProps> = ({ 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 (
|
||||
<Tabs
|
||||
id="studio-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={tabKey}
|
||||
onSelect={setTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="scenes"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="scenes"
|
||||
count={sceneCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StudioScenesPanel active={tabKey === "scenes"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="galleries"
|
||||
count={galleryCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StudioGalleriesPanel active={tabKey === "galleries"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="images"
|
||||
count={imageCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StudioImagesPanel active={tabKey === "images"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="performers"
|
||||
count={performerCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StudioPerformersPanel
|
||||
active={tabKey === "performers"}
|
||||
studio={studio}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="groups"
|
||||
count={groupCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StudioGroupsPanel active={tabKey === "groups"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="childstudios"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="subsidiary_studios"
|
||||
count={studio.child_studios.length}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StudioChildrenPanel
|
||||
active={tabKey === "childstudios"}
|
||||
studio={studio}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabKey) {
|
||||
setTabKey(populatedDefaultTab);
|
||||
const StudioPage: React.FC<IProps> = ({ 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<boolean>(!showAllDetails);
|
||||
const loadStickyHeader = useLoadStickyHeader();
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
|
||||
// Editing studio state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(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<IProps> = ({ studio, tabKey }) => {
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderAliases() {
|
||||
if (studio?.aliases?.length) {
|
||||
return (
|
||||
<div>
|
||||
<span className="alias-head">{studio?.aliases?.join(", ")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getCollapseButtonIcon() {
|
||||
return collapsed ? faChevronDown : faChevronUp;
|
||||
}
|
||||
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
@@ -279,46 +372,6 @@ const StudioPage: React.FC<IProps> = ({ 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 (
|
||||
<DetailImage className="logo" alt={studio.name} src={studioImage} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderClickableIcons = () => (
|
||||
<span className="name-icons">
|
||||
<Button
|
||||
className={cx("minimal", studio.favorite ? "favorite" : "not-favorite")}
|
||||
onClick={() => setFavorite(!studio.favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} />
|
||||
</Button>
|
||||
{studio.url && (
|
||||
<Button
|
||||
as={ExternalLink}
|
||||
href={TextUtils.sanitiseURL(studio.url)}
|
||||
className="minimal link"
|
||||
title={studio.url}
|
||||
>
|
||||
<Icon icon={faLink} />
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
function setRating(v: number | null) {
|
||||
if (studio.id) {
|
||||
updateStudio({
|
||||
@@ -332,205 +385,6 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderDetails() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<StudioDetailsPanel
|
||||
studio={studio}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderShowCollapseButton() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<span className="detail-expand-collapse">
|
||||
<Button
|
||||
className="minimal expand-collapse"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderCompressedDetails() {
|
||||
if (!isEditing && loadStickyHeader) {
|
||||
return <CompressedStudioDetailsPanel studio={studio} />;
|
||||
}
|
||||
}
|
||||
|
||||
const renderTabs = () => (
|
||||
<Tabs
|
||||
id="studio-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={tabKey}
|
||||
onSelect={setTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="scenes"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "scenes" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={sceneCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StudioScenesPanel active={tabKey === "scenes"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "galleries" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={galleryCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StudioGalleriesPanel active={tabKey === "galleries"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "images" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={imageCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StudioImagesPanel active={tabKey === "images"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "performers" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performerCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StudioPerformersPanel
|
||||
active={tabKey === "performers"}
|
||||
studio={studio}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "groups" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={groupCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StudioGroupsPanel active={tabKey === "groups"} studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="childstudios"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "subsidiary_studios" })}
|
||||
<Counter
|
||||
abbreviateCounter={false}
|
||||
count={studio.child_studios.length}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<StudioChildrenPanel
|
||||
active={tabKey === "childstudios"}
|
||||
studio={studio}
|
||||
/>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="background-image-container">
|
||||
<picture>
|
||||
<source src={studioImage} />
|
||||
<img
|
||||
className="background-image"
|
||||
src={studioImage}
|
||||
alt={`${studio.name} background`}
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderTab() {
|
||||
if (!isEditing) {
|
||||
return renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderEditPanel() {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<StudioEditPanel
|
||||
studio={studio}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{
|
||||
return (
|
||||
<DetailsEditNavbar
|
||||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => 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<IProps> = ({ studio, tabKey }) => {
|
||||
</Helmet>
|
||||
|
||||
<div className={headerClassName}>
|
||||
{maybeRenderHeaderBackgroundImage()}
|
||||
<BackgroundImage
|
||||
imagePath={studio.image_path ?? undefined}
|
||||
show={!enableBackgroundImage && !isEditing}
|
||||
/>
|
||||
<div className="detail-container">
|
||||
<div className="detail-header-image">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||
<HeaderImage encodingImage={encodingImage}>
|
||||
{studioImage && (
|
||||
<DetailImage
|
||||
className="logo"
|
||||
alt={studio.name}
|
||||
src={studioImage}
|
||||
/>
|
||||
) : (
|
||||
renderImage()
|
||||
)}
|
||||
</div>
|
||||
</HeaderImage>
|
||||
<div className="row">
|
||||
<div className="studio-head col">
|
||||
<h2>
|
||||
<span className="studio-name">{studio.name}</span>
|
||||
{maybeRenderShowCollapseButton()}
|
||||
{renderClickableIcons()}
|
||||
</h2>
|
||||
{maybeRenderAliases()}
|
||||
<DetailTitle name={studio.name ?? ""} classNamePrefix="studio">
|
||||
{!isEditing && (
|
||||
<ExpandCollapseButton
|
||||
collapsed={collapsed}
|
||||
setCollapsed={(v) => setCollapsed(v)}
|
||||
/>
|
||||
)}
|
||||
<span className="name-icons">
|
||||
<FavoriteIcon
|
||||
favorite={studio.favorite}
|
||||
onToggleFavorite={(v) => setFavorite(v)}
|
||||
/>
|
||||
<ExternalLinkButtons urls={urls} />
|
||||
</span>
|
||||
</DetailTitle>
|
||||
|
||||
<AliasList aliases={studio.aliases} />
|
||||
<RatingSystem
|
||||
value={studio.rating100}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
clickToRate
|
||||
withoutContext
|
||||
/>
|
||||
{maybeRenderDetails()}
|
||||
{maybeRenderEditPanel()}
|
||||
{!isEditing && (
|
||||
<StudioDetailsPanel
|
||||
studio={studio}
|
||||
collapsed={collapsed}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<StudioEditPanel
|
||||
studio={studio}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
) : (
|
||||
<DetailsEditNavbar
|
||||
objectName={
|
||||
studio.name ?? intl.formatMessage({ id: "studio" })
|
||||
}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={studio.ignore_auto_tag}
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{maybeRenderCompressedDetails()}
|
||||
|
||||
{!isEditing && loadStickyHeader && (
|
||||
<CompressedStudioDetailsPanel studio={studio} />
|
||||
)}
|
||||
|
||||
<div className="detail-body">
|
||||
<div className="studio-body">
|
||||
<div className="studio-tabs">{maybeRenderTab()}</div>
|
||||
<div className="studio-tabs">
|
||||
{!isEditing && (
|
||||
<StudioTabs
|
||||
studio={studio}
|
||||
tabKey={tabKey}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
showAllCounts={showAllCounts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderDeleteAlert()}
|
||||
|
||||
@@ -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<IProps> = ({ 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<boolean>(!showAllDetails);
|
||||
const loadStickyHeader = useLoadStickyHeader();
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
const [mergeType, setMergeType] = useState<"from" | "into" | undefined>();
|
||||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(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<IProps> = ({ 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 (
|
||||
<Tabs
|
||||
id="tag-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={tabKey}
|
||||
onSelect={setTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="scenes"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="scenes"
|
||||
count={sceneCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagScenesPanel active={tabKey === "scenes"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="images"
|
||||
count={imageCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagImagesPanel active={tabKey === "images"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="galleries"
|
||||
count={galleryCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagGalleriesPanel active={tabKey === "galleries"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="groups"
|
||||
count={groupCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagGroupsPanel active={tabKey === "groups"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="markers"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="markers"
|
||||
count={sceneMarkerCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagMarkersPanel active={tabKey === "markers"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="performers"
|
||||
count={performerCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagPerformersPanel active={tabKey === "performers"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="studios"
|
||||
title={
|
||||
<TabTitleCounter
|
||||
messageID="studios"
|
||||
count={studioCount}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TagStudiosPanel active={tabKey === "studios"} tag={tag} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!tabKey) {
|
||||
setTabKey(populatedDefaultTab);
|
||||
const TagPage: React.FC<IProps> = ({ 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<boolean>(!showAllDetails);
|
||||
const loadStickyHeader = useLoadStickyHeader();
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
const [mergeType, setMergeType] = useState<"from" | "into" | undefined>();
|
||||
|
||||
// Editing tag state
|
||||
const [image, setImage] = useState<string | null>();
|
||||
const [encodingImage, setEncodingImage] = useState<boolean>(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<IProps> = ({ tag, tabKey }) => {
|
||||
);
|
||||
}
|
||||
|
||||
function getCollapseButtonIcon() {
|
||||
return collapsed ? faChevronDown : faChevronUp;
|
||||
}
|
||||
|
||||
function maybeRenderShowCollapseButton() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<span className="detail-expand-collapse">
|
||||
<Button
|
||||
className="minimal expand-collapse"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
>
|
||||
<Icon className="fa-fw" icon={getCollapseButtonIcon()} />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderAliases() {
|
||||
if (tag?.aliases?.length) {
|
||||
return (
|
||||
<div>
|
||||
<span className="alias-head">{tag?.aliases?.join(", ")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleEditing(value?: boolean) {
|
||||
if (value !== undefined) {
|
||||
setIsEditing(value);
|
||||
@@ -317,34 +401,6 @@ const TagPage: React.FC<IProps> = ({ 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 <DetailImage className="logo" alt={tag.name} src={tagImage} />;
|
||||
}
|
||||
}
|
||||
|
||||
const renderClickableIcons = () => (
|
||||
<span className="name-icons">
|
||||
<Button
|
||||
className={cx("minimal", tag.favorite ? "favorite" : "not-favorite")}
|
||||
onClick={() => setFavorite(!tag.favorite)}
|
||||
>
|
||||
<Icon icon={faHeart} />
|
||||
</Button>
|
||||
</span>
|
||||
);
|
||||
|
||||
function renderMergeButton() {
|
||||
return (
|
||||
<Dropdown>
|
||||
@@ -386,200 +442,6 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderDetails() {
|
||||
if (!isEditing) {
|
||||
return (
|
||||
<TagDetailsPanel
|
||||
tag={tag}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderEditPanel() {
|
||||
if (isEditing) {
|
||||
return (
|
||||
<TagEditPanel
|
||||
tag={tag}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
{
|
||||
return (
|
||||
<DetailsEditNavbar
|
||||
objectName={tag.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={tag.ignore_auto_tag}
|
||||
onDelete={onDelete}
|
||||
classNames="mb-2"
|
||||
customButtons={renderMergeButton()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderTabs = () => (
|
||||
<Tabs
|
||||
id="tag-tabs"
|
||||
mountOnEnter
|
||||
unmountOnExit
|
||||
activeKey={tabKey}
|
||||
onSelect={setTabKey}
|
||||
>
|
||||
<Tab
|
||||
eventKey="scenes"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "scenes" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={sceneCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagScenesPanel active={tabKey === "scenes"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="images"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "images" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={imageCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagImagesPanel active={tabKey === "images"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="galleries"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "galleries" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={galleryCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagGalleriesPanel active={tabKey === "galleries"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "groups" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={groupCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagGroupsPanel active={tabKey === "groups"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="markers"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "markers" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={sceneMarkerCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagMarkersPanel active={tabKey === "markers"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="performers"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "performers" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performerCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagPerformersPanel active={tabKey === "performers"} tag={tag} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="studios"
|
||||
title={
|
||||
<>
|
||||
{intl.formatMessage({ id: "studios" })}
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={studioCount}
|
||||
hideZero
|
||||
/>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TagStudiosPanel active={tabKey === "studios"} tag={tag} />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
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 (
|
||||
<div className="background-image-container">
|
||||
<picture>
|
||||
<source src={tagImage} />
|
||||
<img
|
||||
className="background-image"
|
||||
src={tagImage}
|
||||
alt={`${tag.name} background`}
|
||||
/>
|
||||
</picture>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderTab() {
|
||||
if (!isEditing) {
|
||||
return renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderCompressedDetails() {
|
||||
if (!isEditing && loadStickyHeader) {
|
||||
return <CompressedTagDetailsPanel tag={tag} />;
|
||||
}
|
||||
}
|
||||
|
||||
const headerClassName = cx("detail-header", {
|
||||
edit: isEditing,
|
||||
collapsed,
|
||||
@@ -593,35 +455,86 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
||||
</Helmet>
|
||||
|
||||
<div className={headerClassName}>
|
||||
{maybeRenderHeaderBackgroundImage()}
|
||||
<BackgroundImage
|
||||
imagePath={tag.image_path ?? undefined}
|
||||
show={enableBackgroundImage && !isEditing}
|
||||
/>
|
||||
<div className="detail-container">
|
||||
<div className="detail-header-image">
|
||||
{encodingImage ? (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "actions.encoding_image" })}
|
||||
/>
|
||||
) : (
|
||||
renderImage()
|
||||
<HeaderImage encodingImage={encodingImage}>
|
||||
{tagImage && (
|
||||
<DetailImage className="logo" alt={tag.name} src={tagImage} />
|
||||
)}
|
||||
</div>
|
||||
</HeaderImage>
|
||||
<div className="row">
|
||||
<div className="tag-head col">
|
||||
<h2>
|
||||
<span className="tag-name">{tag.name}</span>
|
||||
{maybeRenderShowCollapseButton()}
|
||||
{renderClickableIcons()}
|
||||
</h2>
|
||||
{maybeRenderAliases()}
|
||||
{maybeRenderDetails()}
|
||||
{maybeRenderEditPanel()}
|
||||
<DetailTitle name={tag.name} classNamePrefix="tag">
|
||||
{!isEditing && (
|
||||
<ExpandCollapseButton
|
||||
collapsed={collapsed}
|
||||
setCollapsed={(v) => setCollapsed(v)}
|
||||
/>
|
||||
)}
|
||||
<span className="name-icons">
|
||||
<FavoriteIcon
|
||||
favorite={tag.favorite}
|
||||
onToggleFavorite={(v) => setFavorite(v)}
|
||||
/>
|
||||
</span>
|
||||
</DetailTitle>
|
||||
|
||||
<AliasList aliases={tag.aliases} />
|
||||
{!isEditing && (
|
||||
<TagDetailsPanel
|
||||
tag={tag}
|
||||
fullWidth={!collapsed && !compactExpandedDetails}
|
||||
/>
|
||||
)}
|
||||
{isEditing ? (
|
||||
<TagEditPanel
|
||||
tag={tag}
|
||||
onSubmit={onSave}
|
||||
onCancel={() => toggleEditing()}
|
||||
onDelete={onDelete}
|
||||
setImage={setImage}
|
||||
setEncodingImage={setEncodingImage}
|
||||
/>
|
||||
) : (
|
||||
<DetailsEditNavbar
|
||||
objectName={tag.name}
|
||||
isNew={false}
|
||||
isEditing={isEditing}
|
||||
onToggleEdit={() => toggleEditing()}
|
||||
onSave={() => {}}
|
||||
onImageChange={() => {}}
|
||||
onClearImage={() => {}}
|
||||
onAutoTag={onAutoTag}
|
||||
autoTagDisabled={tag.ignore_auto_tag}
|
||||
onDelete={onDelete}
|
||||
classNames="mb-2"
|
||||
customButtons={renderMergeButton()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{maybeRenderCompressedDetails()}
|
||||
|
||||
{!isEditing && loadStickyHeader && (
|
||||
<CompressedTagDetailsPanel tag={tag} />
|
||||
)}
|
||||
|
||||
<div className="detail-body">
|
||||
<div className="tag-body">
|
||||
<div className="tag-tabs">{maybeRenderTab()}</div>
|
||||
<div className="tag-tabs">
|
||||
{!isEditing && (
|
||||
<TagTabs
|
||||
tabKey={tabKey}
|
||||
tag={tag}
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
showAllCounts={showAllCounts}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{renderDeleteAlert()}
|
||||
|
||||
22
ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx
Normal file
22
ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx
Normal file
@@ -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 (
|
||||
<Button variant="link" onClick={() => showLightbox(index)}>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user