mirror of https://github.com/stashapp/stash.git
Add Icons to tags if they have parent/child tags (#3931)
* Add Icons to tags if they have parent/child tags * Refactor TagLink --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
36e9ed7a6c
commit
636b0a3167
|
@ -13,12 +13,10 @@ fragment SceneMarkerData on SceneMarker {
|
|||
primary_tag {
|
||||
id
|
||||
name
|
||||
aliases
|
||||
}
|
||||
|
||||
tags {
|
||||
id
|
||||
name
|
||||
aliases
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,4 +3,6 @@ fragment SlimTagData on Tag {
|
|||
name
|
||||
aliases
|
||||
image_path
|
||||
parent_count
|
||||
child_count
|
||||
}
|
||||
|
|
|
@ -15,6 +15,9 @@ type Tag {
|
|||
performer_count(depth: Int): Int! # Resolver
|
||||
parents: [Tag!]!
|
||||
children: [Tag!]!
|
||||
|
||||
parent_count: Int! # Resolver
|
||||
child_count: Int! # Resolver
|
||||
}
|
||||
|
||||
input TagCreateInput {
|
||||
|
|
|
@ -113,3 +113,25 @@ func (r *tagResolver) ImagePath(ctx context.Context, obj *models.Tag) (*string,
|
|||
imagePath := urlbuilders.NewTagURLBuilder(baseURL, obj).GetTagImageURL(hasImage)
|
||||
return &imagePath, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) ParentCount(ctx context.Context, obj *models.Tag) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.CountByParentTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (r *tagResolver) ChildCount(ctx context.Context, obj *models.Tag) (ret int, err error) {
|
||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||
ret, err = r.repository.Tag.CountByChildTagID(ctx, obj.ID)
|
||||
return err
|
||||
}); err != nil {
|
||||
return ret, err
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
|
@ -58,6 +58,48 @@ func (_m *TagReaderWriter) Count(ctx context.Context) (int, error) {
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// CountByChildTagID provides a mock function with given fields: ctx, childID
|
||||
func (_m *TagReaderWriter) CountByChildTagID(ctx context.Context, childID int) (int, error) {
|
||||
ret := _m.Called(ctx, childID)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
|
||||
r0 = rf(ctx, childID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||
r1 = rf(ctx, childID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CountByParentTagID provides a mock function with given fields: ctx, parentID
|
||||
func (_m *TagReaderWriter) CountByParentTagID(ctx context.Context, parentID int) (int, error) {
|
||||
ret := _m.Called(ctx, parentID)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
|
||||
r0 = rf(ctx, parentID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
|
||||
r1 = rf(ctx, parentID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: ctx, newTag
|
||||
func (_m *TagReaderWriter) Create(ctx context.Context, newTag *models.Tag) error {
|
||||
ret := _m.Called(ctx, newTag)
|
||||
|
|
|
@ -42,6 +42,8 @@ type TagAutoTagQueryer interface {
|
|||
// TagCounter provides methods to count tags.
|
||||
type TagCounter interface {
|
||||
Count(ctx context.Context) (int, error)
|
||||
CountByParentTagID(ctx context.Context, parentID int) (int, error)
|
||||
CountByChildTagID(ctx context.Context, childID int) (int, error)
|
||||
}
|
||||
|
||||
// TagCreator provides methods to create tags.
|
||||
|
|
|
@ -396,6 +396,20 @@ func (qb *TagStore) FindByChildTagID(ctx context.Context, parentID int) ([]*mode
|
|||
return qb.queryTags(ctx, query, args)
|
||||
}
|
||||
|
||||
func (qb *TagStore) CountByParentTagID(ctx context.Context, parentID int) (int, error) {
|
||||
q := dialect.Select(goqu.COUNT("*")).From(goqu.T("tags")).
|
||||
InnerJoin(goqu.T("tags_relations"), goqu.On(goqu.I("tags_relations.parent_id").Eq(goqu.I("tags.id")))).
|
||||
Where(goqu.I("tags_relations.child_id").Eq(goqu.V(parentID))) // Pass the parentID here
|
||||
return count(ctx, q)
|
||||
}
|
||||
|
||||
func (qb *TagStore) CountByChildTagID(ctx context.Context, childID int) (int, error) {
|
||||
q := dialect.Select(goqu.COUNT("*")).From(goqu.T("tags")).
|
||||
InnerJoin(goqu.T("tags_relations"), goqu.On(goqu.I("tags_relations.child_id").Eq(goqu.I("tags.id")))).
|
||||
Where(goqu.I("tags_relations.parent_id").Eq(goqu.V(childID))) // Pass the childID here
|
||||
return count(ctx, q)
|
||||
}
|
||||
|
||||
func (qb *TagStore) Count(ctx context.Context) (int, error) {
|
||||
q := dialect.Select(goqu.COUNT("*")).From(qb.table())
|
||||
return count(ctx, q)
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { GridCard } from "../Shared/GridCard";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { SceneLink, TagLink } from "../Shared/TagLink";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
|
||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||
|
@ -31,7 +31,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||
if (props.gallery.scenes.length === 0) return;
|
||||
|
||||
const popoverContent = props.gallery.scenes.map((scene) => (
|
||||
<TagLink key={scene.id} scene={scene} />
|
||||
<SceneLink key={scene.id} scene={scene} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
@ -52,7 +52,7 @@ export const GalleryCard: React.FC<IProps> = (props) => {
|
|||
if (props.gallery.tags.length <= 0) return;
|
||||
|
||||
const popoverContent = props.gallery.tags.map((tag) => (
|
||||
<TagLink key={tag.id} tag={tag} tagType="gallery" />
|
||||
<TagLink key={tag.id} tag={tag} linkType="gallery" />
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -34,7 +34,7 @@ export const GalleryDetailPanel: React.FC<IGalleryDetailProps> = ({
|
|||
function renderTags() {
|
||||
if (gallery.tags.length === 0) return;
|
||||
const tags = gallery.tags.map((tag) => (
|
||||
<TagLink key={tag.id} tag={tag} tagType="gallery" />
|
||||
<TagLink key={tag.id} tag={tag} linkType="gallery" />
|
||||
));
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Button, ButtonGroup } from "react-bootstrap";
|
|||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import { GalleryLink, TagLink } from "src/components/Shared/TagLink";
|
||||
import { HoverPopover } from "src/components/Shared/HoverPopover";
|
||||
import { SweatDrops } from "src/components/Shared/SweatDrops";
|
||||
import { PerformerPopoverButton } from "src/components/Shared/PerformerPopoverButton";
|
||||
|
@ -41,7 +41,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||
if (props.image.tags.length <= 0) return;
|
||||
|
||||
const popoverContent = props.image.tags.map((tag) => (
|
||||
<TagLink key={tag.id} tag={tag} tagType="image" />
|
||||
<TagLink key={tag.id} tag={tag} linkType="image" />
|
||||
));
|
||||
|
||||
return (
|
||||
|
@ -83,7 +83,7 @@ export const ImageCard: React.FC<IImageCardProps> = (
|
|||
if (props.image.galleries.length <= 0) return;
|
||||
|
||||
const popoverContent = props.image.galleries.map((gallery) => (
|
||||
<TagLink key={gallery.id} gallery={gallery} />
|
||||
<GalleryLink key={gallery.id} gallery={gallery} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useMemo } from "react";
|
|||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import { GalleryLink, TagLink } from "src/components/Shared/TagLink";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { PerformerCard } from "src/components/Performers/PerformerCard";
|
||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||
|
@ -24,7 +24,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||
function renderTags() {
|
||||
if (props.image.tags.length === 0) return;
|
||||
const tags = props.image.tags.map((tag) => (
|
||||
<TagLink key={tag.id} tag={tag} tagType="image" />
|
||||
<TagLink key={tag.id} tag={tag} linkType="image" />
|
||||
));
|
||||
return (
|
||||
<>
|
||||
|
@ -67,8 +67,8 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||
|
||||
function renderGalleries() {
|
||||
if (props.image.galleries.length === 0) return;
|
||||
const tags = props.image.galleries.map((gallery) => (
|
||||
<TagLink key={gallery.id} gallery={gallery} />
|
||||
const galleries = props.image.galleries.map((gallery) => (
|
||||
<GalleryLink key={gallery.id} gallery={gallery} />
|
||||
));
|
||||
return (
|
||||
<>
|
||||
|
@ -78,7 +78,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
|
|||
values={{ count: props.image.galleries.length }}
|
||||
/>
|
||||
</h6>
|
||||
{tags}
|
||||
{galleries}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { GridCard } from "../Shared/GridCard";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import { SceneLink } from "../Shared/TagLink";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { RatingBanner } from "../Shared/RatingBanner";
|
||||
|
@ -36,7 +36,7 @@ export const MovieCard: React.FC<IProps> = (props: IProps) => {
|
|||
if (props.movie.scenes.length === 0) return;
|
||||
|
||||
const popoverContent = props.movie.scenes.map((scene) => (
|
||||
<TagLink key={scene.id} scene={scene} />
|
||||
<SceneLink key={scene.id} scene={scene} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -168,7 +168,7 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
|
|||
if (performer.tags.length <= 0) return;
|
||||
|
||||
const popoverContent = performer.tags.map((tag) => (
|
||||
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
||||
<TagLink key={tag.id} linkType="performer" tag={tag} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -29,7 +29,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
return (
|
||||
<ul className="pl-0">
|
||||
{(performer.tags ?? []).map((tag) => (
|
||||
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
||||
<TagLink key={tag.id} linkType="performer" tag={tag} />
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
|
|
|
@ -19,7 +19,12 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
|||
import { ErrorMessage } from "../Shared/ErrorMessage";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import {
|
||||
GalleryLink,
|
||||
MovieLink,
|
||||
SceneMarkerLink,
|
||||
TagLink,
|
||||
} from "../Shared/TagLink";
|
||||
import { SweatDrops } from "../Shared/SweatDrops";
|
||||
import { Pagination } from "src/components/List/Pagination";
|
||||
import TextUtils from "src/utils/text";
|
||||
|
@ -349,7 +354,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||
src={sceneMovie.movie.front_image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink
|
||||
<MovieLink
|
||||
key={sceneMovie.movie.id}
|
||||
movie={sceneMovie.movie}
|
||||
className="d-block"
|
||||
|
@ -377,8 +382,8 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||
if (scene.scene_markers.length <= 0) return;
|
||||
|
||||
const popoverContent = scene.scene_markers.map((marker) => {
|
||||
const markerPopover = { ...marker, scene: { id: scene.id } };
|
||||
return <TagLink key={marker.id} marker={markerPopover} />;
|
||||
const markerWithScene = { ...marker, scene: { id: scene.id } };
|
||||
return <SceneMarkerLink key={marker.id} marker={markerWithScene} />;
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -410,7 +415,7 @@ export const SceneDuplicateChecker: React.FC = () => {
|
|||
if (scene.galleries.length <= 0) return;
|
||||
|
||||
const popoverContent = scene.galleries.map((gallery) => (
|
||||
<TagLink key={gallery.id} gallery={gallery} />
|
||||
<GalleryLink key={gallery.id} gallery={gallery} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -4,7 +4,12 @@ import { Link, useHistory } from "react-router-dom";
|
|||
import cx from "classnames";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { TagLink } from "../Shared/TagLink";
|
||||
import {
|
||||
GalleryLink,
|
||||
TagLink,
|
||||
MovieLink,
|
||||
SceneMarkerLink,
|
||||
} from "../Shared/TagLink";
|
||||
import { HoverPopover } from "../Shared/HoverPopover";
|
||||
import { SweatDrops } from "../Shared/SweatDrops";
|
||||
import { TruncatedText } from "../Shared/TruncatedText";
|
||||
|
@ -219,7 +224,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
src={sceneMovie.movie.front_image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink
|
||||
<MovieLink
|
||||
key={sceneMovie.movie.id}
|
||||
movie={sceneMovie.movie}
|
||||
className="d-block"
|
||||
|
@ -245,8 +250,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
if (props.scene.scene_markers.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.scene_markers.map((marker) => {
|
||||
const markerPopover = { ...marker, scene: { id: props.scene.id } };
|
||||
return <TagLink key={marker.id} marker={markerPopover} />;
|
||||
const markerWithScene = { ...marker, scene: { id: props.scene.id } };
|
||||
return <SceneMarkerLink key={marker.id} marker={markerWithScene} />;
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -282,7 +287,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
if (props.scene.galleries.length <= 0) return;
|
||||
|
||||
const popoverContent = props.scene.galleries.map((gallery) => (
|
||||
<TagLink key={gallery.id} gallery={gallery} />
|
||||
<GalleryLink key={gallery.id} gallery={gallery} />
|
||||
));
|
||||
|
||||
return (
|
||||
|
|
|
@ -18,18 +18,19 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
|
|||
}) => {
|
||||
if (!sceneMarkers?.length) return <div />;
|
||||
|
||||
const primaries: Record<string, GQL.SlimTagDataFragment> = {};
|
||||
const primaryTags: Record<string, GQL.SceneMarkerDataFragment[]> = {};
|
||||
const primaryTagNames: Record<string, string> = {};
|
||||
const markersByTag: Record<string, GQL.SceneMarkerDataFragment[]> = {};
|
||||
sceneMarkers.forEach((m) => {
|
||||
if (primaryTags[m.primary_tag.id]) primaryTags[m.primary_tag.id].push(m);
|
||||
else {
|
||||
primaryTags[m.primary_tag.id] = [m];
|
||||
primaries[m.primary_tag.id] = m.primary_tag;
|
||||
if (primaryTagNames[m.primary_tag.id]) {
|
||||
markersByTag[m.primary_tag.id].push(m);
|
||||
} else {
|
||||
primaryTagNames[m.primary_tag.id] = m.primary_tag.name;
|
||||
markersByTag[m.primary_tag.id] = [m];
|
||||
}
|
||||
});
|
||||
|
||||
const primaryCards = Object.keys(primaryTags).map((id) => {
|
||||
const markers = primaryTags[id].map((marker) => {
|
||||
const primaryCards = Object.keys(markersByTag).map((id) => {
|
||||
const markers = markersByTag[id].map((marker) => {
|
||||
const tags = marker.tags.map((tag) => (
|
||||
<Badge key={tag.id} variant="secondary" className="tag-item">
|
||||
{tag.name}
|
||||
|
@ -59,7 +60,7 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
|
|||
|
||||
return (
|
||||
<Card className="primary-card col-12 col-sm-6 col-xl-6" key={id}>
|
||||
<h3>{primaries[id].name}</h3>
|
||||
<h3>{primaryTagNames[id]}</h3>
|
||||
<Card.Body className="primary-card-body">{markers}</Card.Body>
|
||||
</Card>
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||
import { sortPerformers } from "src/core/performers";
|
||||
import { HoverPopover } from "./HoverPopover";
|
||||
import { Icon } from "./Icon";
|
||||
import { TagLink } from "./TagLink";
|
||||
import { PerformerLink } from "./TagLink";
|
||||
|
||||
interface IProps {
|
||||
performers: Partial<GQL.PerformerDataFragment>[];
|
||||
|
@ -26,7 +26,11 @@ export const PerformerPopoverButton: React.FC<IProps> = ({ performers }) => {
|
|||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink key={performer.id} performer={performer} className="d-block" />
|
||||
<PerformerLink
|
||||
key={performer.id}
|
||||
performer={performer}
|
||||
className="d-block"
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
|
|
|
@ -767,12 +767,12 @@ export const TagSelect: React.FC<
|
|||
};
|
||||
}
|
||||
|
||||
const id = (optionProps.data as Option & { __isNew__: boolean }).__isNew__
|
||||
? ""
|
||||
: optionProps.data.value;
|
||||
const id = optionProps.data.value;
|
||||
const hide = (optionProps.data as Option & { __isNew__: boolean })
|
||||
.__isNew__;
|
||||
|
||||
return (
|
||||
<TagPopover id={id} placement={props.hoverPlacement}>
|
||||
<TagPopover id={id} hide={hide} placement={props.hoverPlacement}>
|
||||
<reactSelectComponents.Option {...thisOptionProps} />
|
||||
</TagPopover>
|
||||
);
|
||||
|
|
|
@ -1,97 +1,252 @@
|
|||
import { Badge } from "react-bootstrap";
|
||||
import React from "react";
|
||||
import { Badge, OverlayTrigger, Tooltip } from "react-bootstrap";
|
||||
import React, { useMemo } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
PerformerDataFragment,
|
||||
TagDataFragment,
|
||||
MovieDataFragment,
|
||||
SceneDataFragment,
|
||||
} from "src/core/generated-graphql";
|
||||
import NavUtils from "src/utils/navigation";
|
||||
import NavUtils, { INamedObject } from "src/utils/navigation";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { objectTitle } from "src/core/files";
|
||||
import { IFile, IObjectWithTitleFiles, objectTitle } from "src/core/files";
|
||||
import { galleryTitle } from "src/core/galleries";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TagPopover } from "../Tags/TagPopover";
|
||||
import { markerTitle } from "src/core/markers";
|
||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
|
||||
interface IFile {
|
||||
path: string;
|
||||
}
|
||||
interface IGallery {
|
||||
id: string;
|
||||
files: IFile[];
|
||||
folder?: GQL.Maybe<IFile>;
|
||||
title: GQL.Maybe<string>;
|
||||
}
|
||||
import { faFolderTree } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
type SceneMarkerFragment = Pick<GQL.SceneMarker, "id" | "title" | "seconds"> & {
|
||||
scene: Pick<GQL.Scene, "id">;
|
||||
primary_tag: Pick<GQL.Tag, "id" | "name">;
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
tag?: Partial<TagDataFragment>;
|
||||
tagType?: "performer" | "scene" | "gallery" | "image" | "details";
|
||||
performer?: Partial<PerformerDataFragment>;
|
||||
marker?: SceneMarkerFragment;
|
||||
movie?: Partial<MovieDataFragment>;
|
||||
scene?: Partial<Pick<SceneDataFragment, "id" | "title" | "files">>;
|
||||
gallery?: Partial<IGallery>;
|
||||
interface ICommonLinkProps {
|
||||
link: string;
|
||||
className?: string;
|
||||
hoverPlacement?: Placement;
|
||||
}
|
||||
|
||||
export const TagLink: React.FC<IProps> = (props: IProps) => {
|
||||
let id: string = "";
|
||||
let link: string = "#";
|
||||
let title: string = "";
|
||||
if (props.tag) {
|
||||
id = props.tag.id || "";
|
||||
switch (props.tagType) {
|
||||
case "scene":
|
||||
case undefined:
|
||||
link = NavUtils.makeTagScenesUrl(props.tag);
|
||||
break;
|
||||
case "performer":
|
||||
link = NavUtils.makeTagPerformersUrl(props.tag);
|
||||
break;
|
||||
case "gallery":
|
||||
link = NavUtils.makeTagGalleriesUrl(props.tag);
|
||||
break;
|
||||
case "image":
|
||||
link = NavUtils.makeTagImagesUrl(props.tag);
|
||||
break;
|
||||
case "details":
|
||||
link = NavUtils.makeTagUrl(id);
|
||||
break;
|
||||
}
|
||||
title = props.tag.name || "";
|
||||
} else if (props.performer) {
|
||||
link = NavUtils.makePerformerScenesUrl(props.performer);
|
||||
title = props.performer.name || "";
|
||||
} else if (props.movie) {
|
||||
link = NavUtils.makeMovieScenesUrl(props.movie);
|
||||
title = props.movie.name || "";
|
||||
} else if (props.marker) {
|
||||
link = NavUtils.makeSceneMarkerUrl(props.marker);
|
||||
title = `${markerTitle(props.marker)} - ${TextUtils.secondsToTimestamp(
|
||||
props.marker.seconds || 0
|
||||
)}`;
|
||||
} else if (props.gallery) {
|
||||
link = `/galleries/${props.gallery.id}`;
|
||||
title = galleryTitle(props.gallery);
|
||||
} else if (props.scene) {
|
||||
link = `/scenes/${props.scene.id}`;
|
||||
title = objectTitle(props.scene);
|
||||
}
|
||||
const CommonLinkComponent: React.FC<ICommonLinkProps> = ({
|
||||
link,
|
||||
className,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Badge className={cx("tag-item", props.className)} variant="secondary">
|
||||
<TagPopover id={id} placement={props.hoverPlacement}>
|
||||
<Link to={link}>{title}</Link>
|
||||
</TagPopover>
|
||||
<Badge className={cx("tag-item", className)} variant="secondary">
|
||||
<Link to={link}>{children}</Link>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
interface IPerformerLinkProps {
|
||||
performer: INamedObject;
|
||||
linkType?: "scene" | "gallery" | "image";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PerformerLink: React.FC<IPerformerLinkProps> = ({
|
||||
performer,
|
||||
linkType = "scene",
|
||||
className,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "gallery":
|
||||
return NavUtils.makePerformerGalleriesUrl(performer);
|
||||
case "image":
|
||||
return NavUtils.makePerformerImagesUrl(performer);
|
||||
case "scene":
|
||||
default:
|
||||
return NavUtils.makePerformerScenesUrl(performer);
|
||||
}
|
||||
}, [performer, linkType]);
|
||||
|
||||
const title = performer.name || "";
|
||||
|
||||
return (
|
||||
<CommonLinkComponent link={link} className={className}>
|
||||
{title}
|
||||
</CommonLinkComponent>
|
||||
);
|
||||
};
|
||||
|
||||
interface IMovieLinkProps {
|
||||
movie: INamedObject;
|
||||
linkType?: "scene";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MovieLink: React.FC<IMovieLinkProps> = ({
|
||||
movie,
|
||||
linkType = "scene",
|
||||
className,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "scene":
|
||||
return NavUtils.makeMovieScenesUrl(movie);
|
||||
}
|
||||
}, [movie, linkType]);
|
||||
|
||||
const title = movie.name || "";
|
||||
|
||||
return (
|
||||
<CommonLinkComponent link={link} className={className}>
|
||||
{title}
|
||||
</CommonLinkComponent>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISceneMarkerLinkProps {
|
||||
marker: SceneMarkerFragment;
|
||||
linkType?: "scene";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SceneMarkerLink: React.FC<ISceneMarkerLinkProps> = ({
|
||||
marker,
|
||||
linkType = "scene",
|
||||
className,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "scene":
|
||||
return NavUtils.makeSceneMarkerUrl(marker);
|
||||
}
|
||||
}, [marker, linkType]);
|
||||
|
||||
const title = `${markerTitle(marker)} - ${TextUtils.secondsToTimestamp(
|
||||
marker.seconds || 0
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<CommonLinkComponent link={link} className={className}>
|
||||
{title}
|
||||
</CommonLinkComponent>
|
||||
);
|
||||
};
|
||||
|
||||
interface IObjectWithIDTitleFiles extends IObjectWithTitleFiles {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ISceneLinkProps {
|
||||
scene: IObjectWithIDTitleFiles;
|
||||
linkType?: "details";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SceneLink: React.FC<ISceneLinkProps> = ({
|
||||
scene,
|
||||
linkType = "details",
|
||||
className,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "details":
|
||||
return `/scenes/${scene.id}`;
|
||||
}
|
||||
}, [scene, linkType]);
|
||||
|
||||
const title = objectTitle(scene);
|
||||
|
||||
return (
|
||||
<CommonLinkComponent link={link} className={className}>
|
||||
{title}
|
||||
</CommonLinkComponent>
|
||||
);
|
||||
};
|
||||
|
||||
interface IGallery extends IObjectWithIDTitleFiles {
|
||||
folder?: GQL.Maybe<IFile>;
|
||||
}
|
||||
|
||||
interface IGalleryLinkProps {
|
||||
gallery: IGallery;
|
||||
linkType?: "details";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const GalleryLink: React.FC<IGalleryLinkProps> = ({
|
||||
gallery,
|
||||
linkType = "details",
|
||||
className,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "details":
|
||||
return `/galleries/${gallery.id}`;
|
||||
}
|
||||
}, [gallery, linkType]);
|
||||
|
||||
const title = galleryTitle(gallery);
|
||||
|
||||
return (
|
||||
<CommonLinkComponent link={link} className={className}>
|
||||
{title}
|
||||
</CommonLinkComponent>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITagLinkProps {
|
||||
tag: INamedObject;
|
||||
linkType?: "scene" | "gallery" | "image" | "details" | "performer";
|
||||
className?: string;
|
||||
hoverPlacement?: Placement;
|
||||
showHierarchyIcon?: boolean;
|
||||
hierarchyTooltipID?: string;
|
||||
}
|
||||
|
||||
export const TagLink: React.FC<ITagLinkProps> = ({
|
||||
tag,
|
||||
linkType = "scene",
|
||||
className,
|
||||
hoverPlacement,
|
||||
showHierarchyIcon = false,
|
||||
hierarchyTooltipID,
|
||||
}) => {
|
||||
const link = useMemo(() => {
|
||||
switch (linkType) {
|
||||
case "scene":
|
||||
return NavUtils.makeTagScenesUrl(tag);
|
||||
case "performer":
|
||||
return NavUtils.makeTagPerformersUrl(tag);
|
||||
case "gallery":
|
||||
return NavUtils.makeTagGalleriesUrl(tag);
|
||||
case "image":
|
||||
return NavUtils.makeTagImagesUrl(tag);
|
||||
case "details":
|
||||
return NavUtils.makeTagUrl(tag.id ?? "");
|
||||
}
|
||||
}, [tag, linkType]);
|
||||
|
||||
const title = tag.name || "";
|
||||
|
||||
const tooltip = useMemo(() => {
|
||||
if (!hierarchyTooltipID) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip id="tag-hierarchy-tooltip">
|
||||
<FormattedMessage id={hierarchyTooltipID} />
|
||||
</Tooltip>
|
||||
);
|
||||
}, [hierarchyTooltipID]);
|
||||
|
||||
return (
|
||||
<CommonLinkComponent link={link} className={className}>
|
||||
<TagPopover id={tag.id ?? ""} placement={hoverPlacement}>
|
||||
<Link to={link}>
|
||||
{title}
|
||||
{showHierarchyIcon && (
|
||||
<OverlayTrigger placement="top" overlay={tooltip}>
|
||||
<span className="icon-wrapper">
|
||||
<span className="vertical-line">|</span>
|
||||
<Icon icon={faFolderTree} className="tag-icon" />
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
</Link>
|
||||
</TagPopover>
|
||||
</CommonLinkComponent>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -7,7 +7,7 @@ import { FormattedMessage } from "react-intl";
|
|||
import { sortPerformers } from "src/core/performers";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
import { OperationButton } from "src/components/Shared/OperationButton";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import { PerformerLink, TagLink } from "src/components/Shared/TagLink";
|
||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||
import { parsePath, prepareQueryString } from "src/components/Tagger/utils";
|
||||
import { ScenePreview } from "src/components/Scenes/SceneCard";
|
||||
|
@ -54,7 +54,7 @@ const TaggerSceneDetails: React.FC<ITaggerSceneDetails> = ({ scene }) => {
|
|||
src={performer.image_path ?? ""}
|
||||
/>
|
||||
</Link>
|
||||
<TagLink
|
||||
<PerformerLink
|
||||
key={performer.id}
|
||||
performer={performer}
|
||||
className="d-block"
|
||||
|
|
|
@ -21,7 +21,9 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
|
|||
key={p.id}
|
||||
tag={p}
|
||||
hoverPlacement="bottom"
|
||||
tagType="details"
|
||||
linkType="details"
|
||||
showHierarchyIcon={p.parent_count !== 0}
|
||||
hierarchyTooltipID="tag_parent_tooltip"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -40,7 +42,9 @@ export const TagDetailsPanel: React.FC<ITagDetails> = ({ tag, fullWidth }) => {
|
|||
key={c.id}
|
||||
tag={c}
|
||||
hoverPlacement="bottom"
|
||||
tagType="details"
|
||||
linkType="details"
|
||||
showHierarchyIcon={c.child_count !== 0}
|
||||
hierarchyTooltipID="tag_sub_tag_tooltip"
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -8,13 +8,12 @@ import { ConfigurationContext } from "../../hooks/Config";
|
|||
import { IUIConfig } from "src/core/config";
|
||||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
|
||||
interface ITagPopoverProps {
|
||||
id?: string;
|
||||
placement?: Placement;
|
||||
interface ITagPopoverCardProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const TagPopoverCard: React.FC<ITagPopoverCardProps> = ({ id }) => {
|
||||
const { data, loading, error } = useFindTag(id ?? "");
|
||||
const { data, loading, error } = useFindTag(id);
|
||||
|
||||
if (loading)
|
||||
return (
|
||||
|
@ -35,8 +34,15 @@ export const TagPopoverCard: React.FC<ITagPopoverCardProps> = ({ id }) => {
|
|||
);
|
||||
};
|
||||
|
||||
interface ITagPopoverProps {
|
||||
id: string;
|
||||
hide?: boolean;
|
||||
placement?: Placement;
|
||||
}
|
||||
|
||||
export const TagPopover: React.FC<ITagPopoverProps> = ({
|
||||
id,
|
||||
hide,
|
||||
children,
|
||||
placement = "top",
|
||||
}) => {
|
||||
|
@ -45,7 +51,7 @@ export const TagPopover: React.FC<ITagPopoverProps> = ({
|
|||
const showTagCardOnHover =
|
||||
(config?.ui as IUIConfig)?.showTagCardOnHover ?? true;
|
||||
|
||||
if (!id || !showTagCardOnHover) {
|
||||
if (hide || !showTagCardOnHover) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
|
@ -60,7 +66,3 @@ export const TagPopover: React.FC<ITagPopoverProps> = ({
|
|||
</HoverPopover>
|
||||
);
|
||||
};
|
||||
|
||||
interface ITagPopoverCardProps {
|
||||
id?: string;
|
||||
}
|
||||
|
|
|
@ -72,3 +72,21 @@
|
|||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
.icon-wrapper {
|
||||
color: #202b33;
|
||||
opacity: 0.5;
|
||||
padding-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
.tag-icon {
|
||||
color: #202b33;
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
padding-left: 3px;
|
||||
transform: scale(0.7);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import TextUtils from "src/utils/text";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
interface IFile {
|
||||
export interface IFile {
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface IObjectWithFiles {
|
||||
files: IFile[];
|
||||
files?: IFile[];
|
||||
}
|
||||
|
||||
interface IObjectWithTitleFiles extends IObjectWithFiles {
|
||||
title: GQL.Maybe<string>;
|
||||
export interface IObjectWithTitleFiles extends IObjectWithFiles {
|
||||
title?: GQL.Maybe<string>;
|
||||
}
|
||||
|
||||
export function objectTitle(s: Partial<IObjectWithTitleFiles>) {
|
||||
|
|
|
@ -1319,6 +1319,8 @@
|
|||
"synopsis": "Synopsis",
|
||||
"tag": "Tag",
|
||||
"tag_count": "Tag Count",
|
||||
"tag_parent_tooltip": "Has parent tags",
|
||||
"tag_sub_tag_tooltip": "Has sub-tags",
|
||||
"tags": "Tags",
|
||||
"tattoos": "Tattoos",
|
||||
"title": "Title",
|
||||
|
|
|
@ -73,8 +73,13 @@ const makePerformerImagesUrl = (
|
|||
return `/images?${filter.makeQueryParameters()}`;
|
||||
};
|
||||
|
||||
export interface INamedObject {
|
||||
id?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
const makePerformerGalleriesUrl = (
|
||||
performer: Partial<GQL.PerformerDataFragment>,
|
||||
performer: INamedObject,
|
||||
extraPerformer?: ILabeledId,
|
||||
extraCriteria?: Criterion<CriterionValue>[]
|
||||
) => {
|
||||
|
|
Loading…
Reference in New Issue