From e1650d5ab351ba11b89fcd913062b3dc60c3f6e3 Mon Sep 17 00:00:00 2001 From: Angelo Date: Fri, 21 Jul 2023 13:40:12 +0100 Subject: [PATCH 1/6] Add Icons to tags if they have parent/child tags --- 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 | 39 +++++++++++++++++++ pkg/models/tag.go | 2 + pkg/sqlite/tag.go | 14 +++++++ .../Scenes/SceneDetails/PrimaryTags.tsx | 2 +- .../Tags/TagDetails/TagDetailsPanel.tsx | 16 +++++++- ui/v2.5/src/components/Tags/styles.scss | 4 ++ 9 files changed, 101 insertions(+), 3 deletions(-) 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 3af621111a2..dc2e4203047 100644 --- a/graphql/schema/types/tag.graphql +++ b/graphql/schema/types/tag.graphql @@ -16,6 +16,9 @@ type Tag { 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 b4553c3d755..acbc4e7f11f 100644 --- a/pkg/models/mocks/TagReaderWriter.go +++ b/pkg/models/mocks/TagReaderWriter.go @@ -58,6 +58,45 @@ func (_m *TagReaderWriter) Count(ctx context.Context) (int, error) { return r0, r1 } +func (_m *TagReaderWriter) CountByParentTagID(ctx context.Context, parentID int) (int, error) { + ret := _m.Called(ctx) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context) int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +func (_m *TagReaderWriter) CountByChildTagID(ctx context.Context, parentID int) (int, error) { + ret := _m.Called(ctx) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context) int); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } 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/tag.go b/pkg/models/tag.go index 0ddcc1d86cd..b8b8d78f98d 100644 --- a/pkg/models/tag.go +++ b/pkg/models/tag.go @@ -57,6 +57,8 @@ type TagReader interface { FindByParentTagID(ctx context.Context, parentID int) ([]*Tag, error) FindByChildTagID(ctx context.Context, childID int) ([]*Tag, error) Count(ctx context.Context) (int, error) + CountByParentTagID(ctx context.Context, parentID int) (int, error) + CountByChildTagID(ctx context.Context, childID int) (int, error) All(ctx context.Context) ([]*Tag, error) // TODO - this interface is temporary until the filter schema can fully // support the query needed diff --git a/pkg/sqlite/tag.go b/pkg/sqlite/tag.go index e39f6f8a180..2dbfc967294 100644 --- a/pkg/sqlite/tag.go +++ b/pkg/sqlite/tag.go @@ -395,6 +395,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/Scenes/SceneDetails/PrimaryTags.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx index f658d34b16f..74aa7dd426c 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx @@ -18,7 +18,7 @@ export const PrimaryTags: React.FC = ({ }) => { if (!sceneMarkers?.length) return
; - const primaries: Record = {}; + const primaries: Record> = {}; const primaryTags: Record = {}; sceneMarkers.forEach((m) => { if (primaryTags[m.primary_tag.id]) primaryTags[m.primary_tag.id].push(m); diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index 19b921a55ee..3a51654f701 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -2,7 +2,9 @@ import React from "react"; import { Badge } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; +import { Icon } from "../../Shared/Icon"; import * as GQL from "src/core/generated-graphql"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; interface ITagDetails { tag: GQL.TagDataFragment; @@ -43,7 +45,12 @@ export const TagDetailsPanel: React.FC = ({ tag }) => {
{tag.parents.map((p) => ( - {p.name} + + {p.name}{" "} + {p.parent_count !== 0 && ( + + )} + ))}
@@ -64,7 +71,12 @@ export const TagDetailsPanel: React.FC = ({ tag }) => {
{tag.children.map((c) => ( - {c.name} + + {c.name}{" "} + {c.child_count !== 0 && ( + + )} + ))}
diff --git a/ui/v2.5/src/components/Tags/styles.scss b/ui/v2.5/src/components/Tags/styles.scss index d5aeccc7c41..72b77f022e1 100644 --- a/ui/v2.5/src/components/Tags/styles.scss +++ b/ui/v2.5/src/components/Tags/styles.scss @@ -72,3 +72,7 @@ padding: 0; } } + +.tag-item .tag-icon { + margin: 0 0.1rem; +} From 257705a4e7a79e1370c4047d0061daca68715c6f Mon Sep 17 00:00:00 2001 From: Angelo Date: Fri, 21 Jul 2023 16:14:40 +0100 Subject: [PATCH 2/6] Applied Diff --- graphql/documents/data/scene-marker.graphql | 2 -- .../Scenes/SceneDetails/PrimaryTags.tsx | 19 ++++++++++--------- .../Tags/TagDetails/TagDetailsPanel.tsx | 4 ++-- ui/v2.5/src/components/Tags/styles.scss | 2 +- 4 files changed, 13 insertions(+), 14 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/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/PrimaryTags.tsx index 74aa7dd426c..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/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index 3a51654f701..ad3c0358f50 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -46,7 +46,7 @@ export const TagDetailsPanel: React.FC = ({ tag }) => { {tag.parents.map((p) => ( - {p.name}{" "} + {p.name} {p.parent_count !== 0 && ( )} @@ -72,7 +72,7 @@ export const TagDetailsPanel: React.FC = ({ tag }) => { {tag.children.map((c) => ( - {c.name}{" "} + {c.name} {c.child_count !== 0 && ( )} diff --git a/ui/v2.5/src/components/Tags/styles.scss b/ui/v2.5/src/components/Tags/styles.scss index 72b77f022e1..cb386c74cd3 100644 --- a/ui/v2.5/src/components/Tags/styles.scss +++ b/ui/v2.5/src/components/Tags/styles.scss @@ -74,5 +74,5 @@ } .tag-item .tag-icon { - margin: 0 0.1rem; + margin: 0 0.1rem 0 0.3rem; } From d195ad1ee4568648dde86ceb300dce997825bc91 Mon Sep 17 00:00:00 2001 From: Angelo Date: Sun, 23 Jul 2023 00:38:50 +0100 Subject: [PATCH 3/6] Add Tooltip & separator, changed icon & CSS --- pkg/models/mocks/TagReaderWriter.go | 27 +++++++------ .../Tags/TagDetails/TagDetailsPanel.tsx | 40 ++++++++++++++++--- ui/v2.5/src/components/Tags/styles.scss | 18 ++++++++- 3 files changed, 65 insertions(+), 20 deletions(-) diff --git a/pkg/models/mocks/TagReaderWriter.go b/pkg/models/mocks/TagReaderWriter.go index acbc4e7f11f..680c78c46c6 100644 --- a/pkg/models/mocks/TagReaderWriter.go +++ b/pkg/models/mocks/TagReaderWriter.go @@ -58,19 +58,20 @@ func (_m *TagReaderWriter) Count(ctx context.Context) (int, error) { return r0, r1 } -func (_m *TagReaderWriter) CountByParentTagID(ctx context.Context, parentID int) (int, error) { - ret := _m.Called(ctx) +// 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); ok { - r0 = rf(ctx) + 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) error); ok { - r1 = rf(ctx) + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, childID) } else { r1 = ret.Error(1) } @@ -78,25 +79,27 @@ func (_m *TagReaderWriter) CountByParentTagID(ctx context.Context, parentID int) return r0, r1 } -func (_m *TagReaderWriter) CountByChildTagID(ctx context.Context, parentID int) (int, error) { - ret := _m.Called(ctx) +// 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); ok { - r0 = rf(ctx) + 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) error); ok { - r1 = rf(ctx) + 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/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index ad3c0358f50..15532f7f8d9 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -1,10 +1,10 @@ import React from "react"; -import { Badge } from "react-bootstrap"; +import { Badge, OverlayTrigger, Tooltip } from "react-bootstrap"; import { FormattedMessage } from "react-intl"; import { Link } from "react-router-dom"; import { Icon } from "../../Shared/Icon"; import * as GQL from "src/core/generated-graphql"; -import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { faFolderTree } from "@fortawesome/free-solid-svg-icons"; interface ITagDetails { tag: GQL.TagDataFragment; @@ -43,12 +43,26 @@ export const TagDetailsPanel: React.FC = ({ tag }) => {
- {tag.parents.map((p) => ( + {tag.children.map((p) => ( {p.name} - {p.parent_count !== 0 && ( - + {p.child_count !== 0 && ( + <> + + | + + Explore tag hierarchy + + } + > + + + + )} @@ -74,7 +88,21 @@ export const TagDetailsPanel: React.FC = ({ tag }) => { {c.name} {c.child_count !== 0 && ( - + <> + + | + + Explore tag hierarchy + + } + > + + + + )} diff --git a/ui/v2.5/src/components/Tags/styles.scss b/ui/v2.5/src/components/Tags/styles.scss index cb386c74cd3..8b84555b2ae 100644 --- a/ui/v2.5/src/components/Tags/styles.scss +++ b/ui/v2.5/src/components/Tags/styles.scss @@ -73,6 +73,20 @@ } } -.tag-item .tag-icon { - margin: 0 0.1rem 0 0.3rem; +.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); + } } From 97ef26c0566a8c807dfee177dd7db770825c3181 Mon Sep 17 00:00:00 2001 From: Angelo Date: Wed, 30 Aug 2023 06:50:40 +0100 Subject: [PATCH 4/6] Made Multiple Link Types --- .../src/components/Galleries/GalleryCard.tsx | 6 +- .../GalleryDetails/GalleryDetailPanel.tsx | 4 +- ui/v2.5/src/components/Images/ImageCard.tsx | 6 +- .../Images/ImageDetails/ImageDetailPanel.tsx | 6 +- ui/v2.5/src/components/Movies/MovieCard.tsx | 4 +- .../components/Performers/PerformerCard.tsx | 4 +- .../PerformerDetailsPanel.tsx | 4 +- .../SceneDuplicateChecker.tsx | 10 +- ui/v2.5/src/components/Scenes/SceneCard.tsx | 10 +- .../Scenes/SceneDetails/SceneDetailPanel.tsx | 4 +- .../Shared/PerformerPopoverButton.tsx | 8 +- ui/v2.5/src/components/Shared/TagLink.tsx | 130 ++++++++++++++++-- .../components/Tagger/scenes/TaggerScene.tsx | 6 +- .../Tags/TagDetails/TagDetailsPanel.tsx | 13 +- 14 files changed, 166 insertions(+), 49 deletions(-) diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index 88fe37f2aae..d8a43c00171 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 { GalleryLink } 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..791d24a99c0 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { TagLink } from "src/components/Shared/TagLink"; +import { GalleryLink } 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"; @@ -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..02007ab937e 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 { ImageLink } 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..f4752444a86 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 { ImageLink } 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 ( <> @@ -68,7 +68,7 @@ export const ImageDetailPanel: React.FC = (props) => { function renderGalleries() { if (props.image.galleries.length === 0) return; const tags = props.image.galleries.map((gallery) => ( - + )); return ( <> 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..4e2dd102af5 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -9,7 +9,7 @@ import { CountryFlag } from "../Shared/CountryFlag"; import { SweatDrops } from "../Shared/SweatDrops"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { TagLink } from "../Shared/TagLink"; +import { PerformerLink } from "../Shared/TagLink"; import { Button, ButtonGroup } from "react-bootstrap"; import { Criterion, @@ -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..a9d18e9f03c 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useIntl } from "react-intl"; -import { TagLink } from "src/components/Shared/TagLink"; +import { PerformerLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; import { getStashboxBase } from "src/utils/stashbox"; @@ -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 c45d1b29362..3610d204408 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -18,7 +18,7 @@ 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, SceneLink } from "../Shared/TagLink"; import { SweatDrops } from "../Shared/SweatDrops"; import { Pagination } from "src/components/List/Pagination"; import TextUtils from "src/utils/text"; @@ -187,7 +187,7 @@ export const SceneDuplicateChecker: React.FC = () => { if (scene.tags.length <= 0) return; const popoverContent = scene.tags.map((tag) => ( - + )); return ( @@ -221,7 +221,7 @@ export const SceneDuplicateChecker: React.FC = () => { src={sceneMovie.movie.front_image_path ?? ""} /> - { const popoverContent = scene.scene_markers.map((marker) => { const markerPopover = { ...marker, scene: { id: scene.id } }; - return ; + return ; }); return ( @@ -282,7 +282,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 190c4b4697f..bceb91eac62 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -4,7 +4,7 @@ import { Link } 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 { SceneLink, GalleryLink } from "../Shared/TagLink"; import { HoverPopover } from "../Shared/HoverPopover"; import { SweatDrops } from "../Shared/SweatDrops"; import { TruncatedText } from "../Shared/TruncatedText"; @@ -174,7 +174,7 @@ export const SceneCard: React.FC = ( if (props.scene.tags.length <= 0) return; const popoverContent = props.scene.tags.map((tag) => ( - + )); return ( @@ -212,7 +212,7 @@ export const SceneCard: React.FC = ( src={sceneMovie.movie.front_image_path ?? ""} /> - = ( const popoverContent = props.scene.scene_markers.map((marker) => { const markerPopover = { ...marker, scene: { id: props.scene.id } }; - return ; + return ; }); return ( @@ -275,7 +275,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/SceneDetailPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx index e5277596054..41a05c9de06 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { TagLink } from "src/components/Shared/TagLink"; +import { DetailsLink } from "src/components/Shared/TagLink"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { sortPerformers } from "src/core/performers"; @@ -37,7 +37,7 @@ export const SceneDetailPanel: React.FC = (props) => { function renderTags() { if (props.scene.tags.length === 0) return; const tags = props.scene.tags.map((tag) => ( - + )); return ( <> 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/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index 38cb1326623..81686110939 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -1,4 +1,4 @@ -import { Badge } from "react-bootstrap"; +import { Badge, OverlayTrigger, Tooltip } from "react-bootstrap"; import React from "react"; import { Link } from "react-router-dom"; import cx from "classnames"; @@ -16,6 +16,8 @@ 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"; interface IFile { path: string; @@ -34,7 +36,7 @@ type SceneMarkerFragment = Pick & { interface IProps { tag?: Partial; - tagType?: "performer" | "scene" | "gallery" | "image" | "details"; + linkType?: "performer" | "scene" | "gallery" | "image" | "details"; performer?: Partial; marker?: SceneMarkerFragment; movie?: Partial; @@ -42,15 +44,59 @@ interface IProps { gallery?: Partial; className?: string; hoverPlacement?: Placement; + showHierarchyIcon?: boolean; } -export const TagLink: React.FC = (props: IProps) => { +interface ICommonLinkProps { + id: string; + link: string; + title: string; + className?: string; + hoverPlacement?: Placement; + showHierarchyIcon?: boolean; +} + +const CommonLinkComponent: React.FC = ({ + id, + link, + title, + className, + hoverPlacement, + showHierarchyIcon = false, +}) => { + return ( + + + + {title} + {showHierarchyIcon && ( + + Explore tag hierarchy + + } + > + + | + + + + )} + + + + ); +}; + +function getLinkAndTitle(props: IProps) { let id: string = ""; let link: string = "#"; let title: string = ""; if (props.tag) { id = props.tag.id || ""; - switch (props.tagType) { + switch (props.linkType) { case "scene": case undefined: link = NavUtils.makeTagScenesUrl(props.tag); @@ -87,11 +133,77 @@ export const TagLink: React.FC = (props: IProps) => { link = `/scenes/${props.scene.id}`; title = objectTitle(props.scene); } + + return { id, link, title }; +} + +export const PerformerLink: React.FC = (props: IProps) => { + const { id, link, title } = getLinkAndTitle(props); + return ( - - - {title} - - + + ); +}; + +export const SceneLink: React.FC = (props: IProps) => { + const { id, link, title } = getLinkAndTitle(props); + + return ( + + ); +}; + +export const GalleryLink: React.FC = (props: IProps) => { + const { id, link, title } = getLinkAndTitle(props); + + return ( + + ); +}; + +export const ImageLink: React.FC = (props: IProps) => { + const { id, link, title } = getLinkAndTitle(props); + + return ( + + ); +}; + +export const DetailsLink: React.FC = (props: IProps) => { + const { id, link, title } = getLinkAndTitle(props); + + return ( + ); }; diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 7988a5b828e..1dde0fe61ce 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, SceneLink } 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 ?? ""} /> - = ({ scene }) => {
{scene.tags.map((tag) => ( - + ))}
diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index f039160be2a..f0d61dad3bc 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -1,8 +1,7 @@ import React from "react"; -import { TagLink } from "src/components/Shared/TagLink"; +import { DetailsLink } from "src/components/Shared/TagLink"; import { DetailItem } from "src/components/Shared/DetailItem"; import * as GQL from "src/core/generated-graphql"; -import { faFolderTree } from "@fortawesome/free-solid-svg-icons"; interface ITagDetails { tag: GQL.TagDataFragment; @@ -18,11 +17,12 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { return ( <> {tag.parents.map((p) => ( - ))} @@ -37,11 +37,12 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { return ( <> {tag.children.map((c) => ( - ))} From 7e0afd8fc96c0098578903189bf8a96f882e0c05 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 8 Sep 2023 16:23:17 +1000 Subject: [PATCH 5/6] Refactor TagLink --- .../src/components/Galleries/GalleryCard.tsx | 6 +- .../GalleryDetails/GalleryDetailPanel.tsx | 4 +- ui/v2.5/src/components/Images/ImageCard.tsx | 6 +- .../Images/ImageDetails/ImageDetailPanel.tsx | 10 +- .../components/Performers/PerformerCard.tsx | 4 +- .../PerformerDetailsPanel.tsx | 4 +- .../SceneDuplicateChecker.tsx | 15 +- ui/v2.5/src/components/Scenes/SceneCard.tsx | 15 +- .../Scenes/SceneDetails/SceneDetailPanel.tsx | 4 +- ui/v2.5/src/components/Shared/Select.tsx | 8 +- ui/v2.5/src/components/Shared/TagLink.tsx | 331 ++++++++++-------- .../components/Tagger/scenes/TaggerScene.tsx | 4 +- .../Tags/TagDetails/TagDetailsPanel.tsx | 6 +- ui/v2.5/src/components/Tags/TagPopover.tsx | 20 +- ui/v2.5/src/core/files.ts | 8 +- ui/v2.5/src/utils/navigation.ts | 7 +- 16 files changed, 252 insertions(+), 200 deletions(-) diff --git a/ui/v2.5/src/components/Galleries/GalleryCard.tsx b/ui/v2.5/src/components/Galleries/GalleryCard.tsx index d8a43c00171..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 { GalleryLink } 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 791d24a99c0..83ffe2bc3d3 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { GalleryLink } from "src/components/Shared/TagLink"; +import { 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"; @@ -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 02007ab937e..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 { ImageLink } 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 f4752444a86..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 { ImageLink } 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/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 4e2dd102af5..fab6acad865 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -9,7 +9,7 @@ import { CountryFlag } from "../Shared/CountryFlag"; import { SweatDrops } from "../Shared/SweatDrops"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { PerformerLink } from "../Shared/TagLink"; +import { TagLink } from "../Shared/TagLink"; import { Button, ButtonGroup } from "react-bootstrap"; import { Criterion, @@ -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 a9d18e9f03c..b9c9c2855e7 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -1,6 +1,6 @@ import React from "react"; import { useIntl } from "react-intl"; -import { PerformerLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; import { getStashboxBase } from "src/utils/stashbox"; @@ -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 3610d204408..ac7ce554b2a 100644 --- a/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx +++ b/ui/v2.5/src/components/SceneDuplicateChecker/SceneDuplicateChecker.tsx @@ -18,7 +18,12 @@ import { LoadingIndicator } from "../Shared/LoadingIndicator"; import { ErrorMessage } from "../Shared/ErrorMessage"; import { HoverPopover } from "../Shared/HoverPopover"; import { Icon } from "../Shared/Icon"; -import { GalleryLink, SceneLink } 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"; @@ -187,7 +192,7 @@ export const SceneDuplicateChecker: React.FC = () => { if (scene.tags.length <= 0) return; const popoverContent = scene.tags.map((tag) => ( - + )); return ( @@ -221,7 +226,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 ( diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index 9d31aceb250..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 { SceneLink, GalleryLink } 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"; @@ -181,7 +186,7 @@ export const SceneCard: React.FC = ( if (props.scene.tags.length <= 0) return; const popoverContent = props.scene.tags.map((tag) => ( - + )); return ( @@ -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 ( diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx index 41a05c9de06..e5277596054 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import TextUtils from "src/utils/text"; -import { DetailsLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import { TruncatedText } from "src/components/Shared/TruncatedText"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { sortPerformers } from "src/core/performers"; @@ -37,7 +37,7 @@ export const SceneDetailPanel: React.FC = (props) => { function renderTags() { if (props.scene.tags.length === 0) return; const tags = props.scene.tags.map((tag) => ( - + )); return ( <> diff --git a/ui/v2.5/src/components/Shared/Select.tsx b/ui/v2.5/src/components/Shared/Select.tsx index 495df4f5e2d..09d0014e4bf 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 81686110939..21086b15b85 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -1,16 +1,10 @@ import { Badge, OverlayTrigger, Tooltip } from "react-bootstrap"; -import React from "react"; +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"; @@ -19,191 +13,232 @@ import { Placement } from "react-bootstrap/esm/Overlay"; import { faFolderTree } from "@fortawesome/free-solid-svg-icons"; import { Icon } from "../Shared/Icon"; -interface IFile { - path: string; -} -interface IGallery { - id: string; - files: IFile[]; - folder?: GQL.Maybe; - title: GQL.Maybe; -} - type SceneMarkerFragment = Pick & { scene: Pick; primary_tag: Pick; }; -interface IProps { - tag?: Partial; - linkType?: "performer" | "scene" | "gallery" | "image" | "details"; - performer?: Partial; - marker?: SceneMarkerFragment; - movie?: Partial; - scene?: Partial>; - gallery?: Partial; - className?: string; - hoverPlacement?: Placement; - showHierarchyIcon?: boolean; -} - interface ICommonLinkProps { - id: string; link: string; - title: string; className?: string; - hoverPlacement?: Placement; - showHierarchyIcon?: boolean; } const CommonLinkComponent: React.FC = ({ - id, link, - title, className, - hoverPlacement, - showHierarchyIcon = false, + children, }) => { return ( - - - {title} - {showHierarchyIcon && ( - - Explore tag hierarchy - - } - > - - | - - - - )} - - + {children} ); }; -function getLinkAndTitle(props: IProps) { - let id: string = ""; - let link: string = "#"; - let title: string = ""; - if (props.tag) { - id = props.tag.id || ""; - switch (props.linkType) { - case "scene": - case undefined: - link = NavUtils.makeTagScenesUrl(props.tag); - break; - case "performer": - link = NavUtils.makeTagPerformersUrl(props.tag); - break; +interface IPerformerLinkProps { + performer: INamedObject; + linkType?: "scene" | "gallery" | "image"; + className?: string; +} + +export const PerformerLink: React.FC = ({ + performer, + linkType = "scene", + className, +}) => { + const link = useMemo(() => { + switch (linkType) { case "gallery": - link = NavUtils.makeTagGalleriesUrl(props.tag); - break; + return NavUtils.makePerformerGalleriesUrl(performer); case "image": - link = NavUtils.makeTagImagesUrl(props.tag); - break; - case "details": - link = NavUtils.makeTagUrl(id); - break; + return NavUtils.makePerformerImagesUrl(performer); + case "scene": + default: + return NavUtils.makePerformerScenesUrl(performer); } - 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); - } - - return { id, link, title }; + }, [performer, linkType]); + + const title = performer.name || ""; + + return ( + + {title} + + ); +}; + +interface IMovieLinkProps { + movie: INamedObject; + linkType?: "scene"; + className?: string; } -export const PerformerLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +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} + ); }; -export const SceneLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +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} + ); }; -export const GalleryLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface IObjectWithIDTitleFiles extends IObjectWithTitleFiles { + id: string; +} + +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} + ); }; -export const ImageLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface IGallery extends IObjectWithIDTitleFiles { + folder?: GQL.Maybe; +} + +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} + ); }; -export const DetailsLink: React.FC = (props: IProps) => { - const { id, link, title } = getLinkAndTitle(props); +interface ITagLinkProps { + tag: INamedObject; + linkType?: "scene" | "gallery" | "image" | "details" | "performer"; + className?: string; + hoverPlacement?: Placement; + showHierarchyIcon?: boolean; +} + +export const TagLink: React.FC = ({ + tag, + linkType = "scene", + className, + hoverPlacement, + showHierarchyIcon = false, +}) => { + 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 || ""; return ( - + + + + {title} + {showHierarchyIcon && ( + + Explore tag hierarchy + + } + > + + | + + + + )} + + + ); }; diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerScene.tsx index 1dde0fe61ce..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 { PerformerLink, SceneLink } 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"; @@ -64,7 +64,7 @@ const TaggerSceneDetails: React.FC = ({ scene }) => {
{scene.tags.map((tag) => ( - + ))}
diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index f0d61dad3bc..f17ccd6ec9f 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { DetailsLink } from "src/components/Shared/TagLink"; +import { TagLink } from "src/components/Shared/TagLink"; import { DetailItem } from "src/components/Shared/DetailItem"; import * as GQL from "src/core/generated-graphql"; @@ -17,7 +17,7 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { return ( <> {tag.parents.map((p) => ( - = ({ tag, fullWidth }) => { return ( <> {tag.children.map((c) => ( - = ({ 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/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/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[] ) => { From 01c06394c1217ec0c546ef379366f94300bd308a Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:38:00 +1000 Subject: [PATCH 6/6] Internationalise tooltip --- ui/v2.5/src/components/Shared/TagLink.tsx | 24 ++++++++++++------- .../Tags/TagDetails/TagDetailsPanel.tsx | 2 ++ ui/v2.5/src/locales/en-GB.json | 2 ++ 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/ui/v2.5/src/components/Shared/TagLink.tsx b/ui/v2.5/src/components/Shared/TagLink.tsx index 21086b15b85..2af75a8ae11 100644 --- a/ui/v2.5/src/components/Shared/TagLink.tsx +++ b/ui/v2.5/src/components/Shared/TagLink.tsx @@ -12,6 +12,7 @@ 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"; type SceneMarkerFragment = Pick & { scene: Pick; @@ -191,6 +192,7 @@ interface ITagLinkProps { className?: string; hoverPlacement?: Placement; showHierarchyIcon?: boolean; + hierarchyTooltipID?: string; } export const TagLink: React.FC = ({ @@ -199,6 +201,7 @@ export const TagLink: React.FC = ({ className, hoverPlacement, showHierarchyIcon = false, + hierarchyTooltipID, }) => { const link = useMemo(() => { switch (linkType) { @@ -217,20 +220,25 @@ export const TagLink: React.FC = ({ const title = tag.name || ""; + const tooltip = useMemo(() => { + if (!hierarchyTooltipID) { + return <>; + } + + return ( + + + + ); + }, [hierarchyTooltipID]); + return ( {title} {showHierarchyIcon && ( - - Explore tag hierarchy - - } - > + | diff --git a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx index f17ccd6ec9f..9e368aa8b87 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/TagDetailsPanel.tsx @@ -23,6 +23,7 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { hoverPlacement="bottom" linkType="details" showHierarchyIcon={p.parent_count !== 0} + hierarchyTooltipID="tag_parent_tooltip" /> ))} @@ -43,6 +44,7 @@ export const TagDetailsPanel: React.FC = ({ tag, fullWidth }) => { hoverPlacement="bottom" linkType="details" showHierarchyIcon={c.child_count !== 0} + hierarchyTooltipID="tag_sub_tag_tooltip" /> ))} 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",