From 636b0a31679b4049396466d6f8d4a4a1c9d1ac61 Mon Sep 17 00:00:00 2001 From: elkorol <57588838+elkorol@users.noreply.github.com> Date: Wed, 20 Sep 2023 05:08:00 +0100 Subject: [PATCH] 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> --- graphql/documents/data/scene-marker.graphql | 2 - graphql/documents/data/tag-slim.graphql | 2 + graphql/schema/types/tag.graphql | 3 + internal/api/resolver_model_tag.go | 22 ++ pkg/models/mocks/TagReaderWriter.go | 42 +++ pkg/models/repository_tag.go | 2 + pkg/sqlite/tag.go | 14 + .../src/components/Galleries/GalleryCard.tsx | 6 +- .../GalleryDetails/GalleryDetailPanel.tsx | 2 +- ui/v2.5/src/components/Images/ImageCard.tsx | 6 +- .../Images/ImageDetails/ImageDetailPanel.tsx | 10 +- ui/v2.5/src/components/Movies/MovieCard.tsx | 4 +- .../components/Performers/PerformerCard.tsx | 2 +- .../PerformerDetailsPanel.tsx | 2 +- .../SceneDuplicateChecker.tsx | 15 +- ui/v2.5/src/components/Scenes/SceneCard.tsx | 15 +- .../Scenes/SceneDetails/PrimaryTags.tsx | 19 +- .../Shared/PerformerPopoverButton.tsx | 8 +- ui/v2.5/src/components/Shared/Select.tsx | 8 +- ui/v2.5/src/components/Shared/TagLink.tsx | 289 ++++++++++++++---- .../components/Tagger/scenes/TaggerScene.tsx | 4 +- .../Tags/TagDetails/TagDetailsPanel.tsx | 8 +- ui/v2.5/src/components/Tags/TagPopover.tsx | 20 +- ui/v2.5/src/components/Tags/styles.scss | 18 ++ ui/v2.5/src/core/files.ts | 8 +- ui/v2.5/src/locales/en-GB.json | 2 + ui/v2.5/src/utils/navigation.ts | 7 +- 27 files changed, 412 insertions(+), 128 deletions(-) diff --git a/graphql/documents/data/scene-marker.graphql b/graphql/documents/data/scene-marker.graphql index 61439bd1e80..9fd0c7d3ded 100644 --- a/graphql/documents/data/scene-marker.graphql +++ b/graphql/documents/data/scene-marker.graphql @@ -13,12 +13,10 @@ fragment SceneMarkerData on SceneMarker { primary_tag { id name - aliases } tags { id name - aliases } } diff --git a/graphql/documents/data/tag-slim.graphql b/graphql/documents/data/tag-slim.graphql index 26b7c277a5b..e35660de624 100644 --- a/graphql/documents/data/tag-slim.graphql +++ b/graphql/documents/data/tag-slim.graphql @@ -3,4 +3,6 @@ fragment SlimTagData on Tag { name aliases image_path + parent_count + child_count } diff --git a/graphql/schema/types/tag.graphql b/graphql/schema/types/tag.graphql index 6260856572c..eba9b1996ef 100644 --- a/graphql/schema/types/tag.graphql +++ b/graphql/schema/types/tag.graphql @@ -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 { diff --git a/internal/api/resolver_model_tag.go b/internal/api/resolver_model_tag.go index 778dc7fa623..9124b18f483 100644 --- a/internal/api/resolver_model_tag.go +++ b/internal/api/resolver_model_tag.go @@ -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 +} diff --git a/pkg/models/mocks/TagReaderWriter.go b/pkg/models/mocks/TagReaderWriter.go index a061b79b2c9..9b610e49b6e 100644 --- a/pkg/models/mocks/TagReaderWriter.go +++ b/pkg/models/mocks/TagReaderWriter.go @@ -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) diff --git a/pkg/models/repository_tag.go b/pkg/models/repository_tag.go index 6351c2bdfa6..ca8f6971bf7 100644 --- a/pkg/models/repository_tag.go +++ b/pkg/models/repository_tag.go @@ -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. diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index 33273525402..ace5f8346da 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -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) diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 88fe37f2aae..c62b5b7833a 100644 --- a/ui/v2.5/src/components/Galleries/GalleryCard.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryCard.tsx @@ -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 = (props) => { if (props.gallery.scenes.length === 0) return; const popoverContent = props.gallery.scenes.map((scene) => ( - + )); return ( @@ -52,7 +52,7 @@ export const GalleryCard: React.FC = (props) => { if (props.gallery.tags.length <= 0) return; const popoverContent = props.gallery.tags.map((tag) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index 463ced50611..83ffe2bc3d3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -34,7 +34,7 @@ export const GalleryDetailPanel: React.FC = ({ function renderTags() { if (gallery.tags.length === 0) return; const tags = gallery.tags.map((tag) => ( - + )); return ( <> diff --git a/ui/v2.5/src/components/Images/ImageCard.tsx b/ui/v2.5/src/components/Images/ImageCard.tsx index 28598d417c2..5f8c57a53bf 100644 --- a/ui/v2.5/src/components/Images/ImageCard.tsx +++ b/ui/v2.5/src/components/Images/ImageCard.tsx @@ -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 = ( if (props.image.tags.length <= 0) return; const popoverContent = props.image.tags.map((tag) => ( - + )); return ( @@ -83,7 +83,7 @@ export const ImageCard: React.FC = ( if (props.image.galleries.length <= 0) return; const popoverContent = props.image.galleries.map((gallery) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx index c4e840e2cbb..417d425cc7b 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageDetailPanel.tsx @@ -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 = (props) => { function renderTags() { if (props.image.tags.length === 0) return; const tags = props.image.tags.map((tag) => ( - + )); return ( <> @@ -67,8 +67,8 @@ export const ImageDetailPanel: React.FC = (props) => { function renderGalleries() { if (props.image.galleries.length === 0) return; - const tags = props.image.galleries.map((gallery) => ( - + const galleries = props.image.galleries.map((gallery) => ( + )); return ( <> @@ -78,7 +78,7 @@ export const ImageDetailPanel: React.FC = (props) => { values={{ count: props.image.galleries.length }} /> - {tags} + {galleries} ); } diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx index dc10872644b..206b0a8ca79 100644 --- a/ui/v2.5/src/components/Movies/MovieCard.tsx +++ b/ui/v2.5/src/components/Movies/MovieCard.tsx @@ -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 = (props: IProps) => { if (props.movie.scenes.length === 0) return; const popoverContent = props.movie.scenes.map((scene) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index c34b184a5bf..fab6acad865 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -168,7 +168,7 @@ export const PerformerCard: React.FC = ({ if (performer.tags.length <= 0) return; const popoverContent = performer.tags.map((tag) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 84faefe6389..b9c9c2855e7 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -29,7 +29,7 @@ export const PerformerDetailsPanel: React.FC = ({ return (
    {(performer.tags ?? []).map((tag) => ( - + ))}
); diff --git a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx index 3d6a1c6fdb3..d64d15a1585 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -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 ?? ""} /> - { if (scene.scene_markers.length <= 0) return; const popoverContent = scene.scene_markers.map((marker) => { - const markerPopover = { ...marker, scene: { id: scene.id } }; - return ; + const markerWithScene = { ...marker, scene: { id: scene.id } }; + return ; }); return ( @@ -410,7 +415,7 @@ export const SceneDuplicateChecker: React.FC = () => { if (scene.galleries.length <= 0) return; const popoverContent = scene.galleries.map((gallery) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index b01cf698faf..0672ae4a61f 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -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 = ( src={sceneMovie.movie.front_image_path ?? ""} /> - = ( 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 ; + const markerWithScene = { ...marker, scene: { id: props.scene.id } }; + return ; }); return ( @@ -282,7 +287,7 @@ export const SceneCard: React.FC = ( if (props.scene.galleries.length <= 0) return; const popoverContent = props.scene.galleries.map((gallery) => ( - + )); return ( diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx index f658d34b16f..9694ca9ed29 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx @@ -18,18 +18,19 @@ export const PrimaryTags: React.FC = ({ }) => { if (!sceneMarkers?.length) return
; - const primaries: Record = {}; - const primaryTags: Record = {}; + const primaryTagNames: Record = {}; + const markersByTag: Record = {}; 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) => ( {tag.name} @@ -59,7 +60,7 @@ export const PrimaryTags: React.FC = ({ return ( -

{primaries[id].name}

+

{primaryTagNames[id]}

{markers}
); diff --git a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx index 9d0cfb6fea5..0f98f732b63 100644 --- a/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx +++ b/ui/v2.5/src/components/Shared/PerformerPopoverButton.tsx @@ -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[]; @@ -26,7 +26,11 @@ export const PerformerPopoverButton: React.FC = ({ performers }) => { src={performer.image_path ?? ""} /> - +
)); diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 5a1c5b2fecc..e99556a6da3 100644 --- a/ui/v2.5/src/components/Shared/Select.tsx +++ b/ui/v2.5/src/components/Shared/Select.tsx @@ -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 ( - + ); diff --git a/ui/v2.5/src/components/Shared/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index 38cb1326623..2af75a8ae11 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -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"; +import { faFolderTree } from "@fortawesome/free-solid-svg-icons"; +import { Icon } from "../Shared/Icon"; +import { FormattedMessage } from "react-intl"; -interface IFile { - path: string; +type SceneMarkerFragment = Pick & { + scene: Pick; + primary_tag: Pick; +}; + +interface ICommonLinkProps { + link: string; + className?: string; +} + +const CommonLinkComponent: React.FC = ({ + link, + className, + children, +}) => { + return ( + + {children} + + ); +}; + +interface IPerformerLinkProps { + performer: INamedObject; + linkType?: "scene" | "gallery" | "image"; + className?: string; } -interface IGallery { + +export const PerformerLink: React.FC = ({ + 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 ( + + {title} + + ); +}; + +interface IMovieLinkProps { + movie: INamedObject; + linkType?: "scene"; + className?: string; +} + +export const MovieLink: React.FC = ({ + movie, + linkType = "scene", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "scene": + return NavUtils.makeMovieScenesUrl(movie); + } + }, [movie, linkType]); + + const title = movie.name || ""; + + return ( + + {title} + + ); +}; + +interface ISceneMarkerLinkProps { + marker: SceneMarkerFragment; + linkType?: "scene"; + className?: string; +} + +export const SceneMarkerLink: React.FC = ({ + 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 ( + + {title} + + ); +}; + +interface IObjectWithIDTitleFiles extends IObjectWithTitleFiles { id: string; - files: IFile[]; +} + +interface ISceneLinkProps { + scene: IObjectWithIDTitleFiles; + linkType?: "details"; + className?: string; +} + +export const SceneLink: React.FC = ({ + scene, + linkType = "details", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "details": + return `/scenes/${scene.id}`; + } + }, [scene, linkType]); + + const title = objectTitle(scene); + + return ( + + {title} + + ); +}; + +interface IGallery extends IObjectWithIDTitleFiles { folder?: GQL.Maybe; - title: GQL.Maybe; } -type SceneMarkerFragment = Pick & { - scene: Pick; - primary_tag: Pick; +interface IGalleryLinkProps { + gallery: IGallery; + linkType?: "details"; + className?: string; +} + +export const GalleryLink: React.FC = ({ + gallery, + linkType = "details", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { + case "details": + return `/galleries/${gallery.id}`; + } + }, [gallery, linkType]); + + const title = galleryTitle(gallery); + + return ( + + {title} + + ); }; -interface IProps { - tag?: Partial; - tagType?: "performer" | "scene" | "gallery" | "image" | "details"; - performer?: Partial; - marker?: SceneMarkerFragment; - movie?: Partial; - scene?: Partial>; - gallery?: Partial; +interface ITagLinkProps { + tag: INamedObject; + linkType?: "scene" | "gallery" | "image" | "details" | "performer"; className?: string; hoverPlacement?: Placement; + showHierarchyIcon?: boolean; + hierarchyTooltipID?: string; } -export const TagLink: React.FC = (props: IProps) => { - let id: string = ""; - let link: string = "#"; - let title: string = ""; - if (props.tag) { - id = props.tag.id || ""; - switch (props.tagType) { +export const TagLink: React.FC = ({ + tag, + linkType = "scene", + className, + hoverPlacement, + showHierarchyIcon = false, + hierarchyTooltipID, +}) => { + const link = useMemo(() => { + switch (linkType) { case "scene": - case undefined: - link = NavUtils.makeTagScenesUrl(props.tag); - break; + return NavUtils.makeTagScenesUrl(tag); case "performer": - link = NavUtils.makeTagPerformersUrl(props.tag); - break; + return NavUtils.makeTagPerformersUrl(tag); case "gallery": - link = NavUtils.makeTagGalleriesUrl(props.tag); - break; + return NavUtils.makeTagGalleriesUrl(tag); case "image": - link = NavUtils.makeTagImagesUrl(props.tag); - break; + return NavUtils.makeTagImagesUrl(tag); case "details": - link = NavUtils.makeTagUrl(id); - break; + return NavUtils.makeTagUrl(tag.id ?? ""); } - 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); - } + }, [tag, linkType]); + + const title = tag.name || ""; + + const tooltip = useMemo(() => { + if (!hierarchyTooltipID) { + return <>; + } + + return ( + + + + ); + }, [hierarchyTooltipID]); + return ( - - - {title} + + + + {title} + {showHierarchyIcon && ( + + + | + + + + )} + - + ); }; diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 7988a5b828e..327bb3ff507 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx @@ -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 = ({ scene }) => { src={performer.image_path ?? ""} /> - = ({ 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 = ({ tag, fullWidth }) => { key={c.id} tag={c} hoverPlacement="bottom" - tagType="details" + linkType="details" + showHierarchyIcon={c.child_count !== 0} + hierarchyTooltipID="tag_sub_tag_tooltip" /> ))} diff --git a/ui/v2.5/src/components/Tags/TagPopover.tsx b/ui/v2.5/src/components/Tags/TagPopover.tsx index 1edd2ea4f31..e85b64a5a98 100644 --- a/ui/v2.5/src/components/Tags/TagPopover.tsx +++ b/ui/v2.5/src/components/Tags/TagPopover.tsx @@ -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 = ({ 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 = ({ id }) => { ); }; +interface ITagPopoverProps { + id: string; + hide?: boolean; + placement?: Placement; +} + export const TagPopover: React.FC = ({ id, + hide, children, placement = "top", }) => { @@ -45,7 +51,7 @@ export const TagPopover: React.FC = ({ 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 = ({ ); }; - -interface ITagPopoverCardProps { - id?: string; -} diff --git a/ui/v2.5/src/components/Tags/styles.scss b/ui/v2.5/src/components/Tags/styles.scss index d5aeccc7c41..8b84555b2ae 100644 --- a/ui/v2.5/src/components/Tags/styles.scss +++ b/ui/v2.5/src/components/Tags/styles.scss @@ -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); + } +} diff --git a/ui/v2.5/src/core/files.ts b/ui/v2.5/src/core/files.ts index 1c2505840c3..52bac6ec036 100644 --- a/ui/v2.5/src/core/files.ts +++ b/ui/v2.5/src/core/files.ts @@ -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; +export interface IObjectWithTitleFiles extends IObjectWithFiles { + title?: GQL.Maybe; } export function objectTitle(s: Partial) { diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 76c6be33c14..a911ad14e8c 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -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", diff --git a/ui/v2.5/src/utils/navigation.ts b/ui/v2.5/src/utils/navigation.ts index ddefaeec75f..df7b5b5fc53 100644 --- a/ui/v2.5/src/utils/navigation.ts +++ b/ui/v2.5/src/utils/navigation.ts @@ -73,8 +73,13 @@ const makePerformerImagesUrl = ( return `/images?${filter.makeQueryParameters()}`; }; +export interface INamedObject { + id?: string; + name?: string; +} + const makePerformerGalleriesUrl = ( - performer: Partial, + performer: INamedObject, extraPerformer?: ILabeledId, extraCriteria?: Criterion[] ) => {