Add descriptions to tags and display tag cards on hover (#2708)

* add descriptions to tags
* display tag description and tag image on hover

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
pickleahead 2022-10-06 01:01:06 +02:00 committed by GitHub
parent 480ae46dde
commit 4c73f2f845
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 185 additions and 8 deletions

View file

@ -1,6 +1,7 @@
fragment TagData on Tag {
id
name
description
aliases
ignore_auto_tag
image_path

View file

@ -1,6 +1,7 @@
type Tag {
id: ID!
name: String!
description: String
aliases: [String!]!
ignore_auto_tag: Boolean!
created_at: Time!
@ -19,6 +20,7 @@ type Tag {
input TagCreateInput {
name: String!
description: String
aliases: [String!]
ignore_auto_tag: Boolean
@ -32,6 +34,7 @@ input TagCreateInput {
input TagUpdateInput {
id: ID!
name: String
description: String
aliases: [String!]
ignore_auto_tag: Boolean

View file

@ -10,6 +10,13 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *tagResolver) Description(ctx context.Context, obj *models.Tag) (*string, error) {
if obj.Description.Valid {
return &obj.Description.String, nil
}
return nil, nil
}
func (r *tagResolver) Parents(ctx context.Context, obj *models.Tag) (ret []*models.Tag, err error) {
if err := r.withTxn(ctx, func(ctx context.Context) error {
ret, err = r.repository.Tag.FindByChildTagID(ctx, obj.ID)

View file

@ -2,6 +2,7 @@ package api
import (
"context"
"database/sql"
"fmt"
"strconv"
"time"
@ -34,6 +35,10 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
UpdatedAt: models.SQLiteTimestamp{Timestamp: currentTime},
}
if input.Description != nil {
newTag.Description = sql.NullString{String: *input.Description, Valid: true}
}
if input.IgnoreAutoTag != nil {
newTag.IgnoreAutoTag = *input.IgnoreAutoTag
}
@ -195,6 +200,8 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
updatedTag.Name = input.Name
}
updatedTag.Description = translator.nullString(input.Description, "description")
t, err = qb.Update(ctx, updatedTag)
if err != nil {
return err

View file

@ -11,6 +11,7 @@ import (
type Tag struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Image string `json:"image,omitempty"`
Parents []string `json:"parents,omitempty"`

View file

@ -1,10 +1,14 @@
package models
import "time"
import (
"database/sql"
"time"
)
type Tag struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"` // TODO make schema not null
Description sql.NullString `db:"description" json:"description"`
IgnoreAutoTag bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
CreatedAt SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt SQLiteTimestamp `db:"updated_at" json:"updated_at"`
@ -13,6 +17,7 @@ type Tag struct {
type TagPartial struct {
ID int `db:"id" json:"id"`
Name *string `db:"name" json:"name"` // TODO make schema not null
Description *sql.NullString `db:"description" json:"description"`
IgnoreAutoTag *bool `db:"ignore_auto_tag" json:"ignore_auto_tag"`
CreatedAt *SQLiteTimestamp `db:"created_at" json:"created_at"`
UpdatedAt *SQLiteTimestamp `db:"updated_at" json:"updated_at"`

View file

@ -22,7 +22,7 @@ import (
"github.com/stashapp/stash/pkg/logger"
)
var appSchemaVersion uint = 35
var appSchemaVersion uint = 36
//go:embed migrations/*.sql
var migrationsBox embed.FS

View file

@ -0,0 +1 @@
ALTER TABLE `tags` ADD COLUMN `description` text;

View file

@ -20,6 +20,7 @@ type FinderAliasImageGetter interface {
func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag) (*jsonschema.Tag, error) {
newTagJSON := jsonschema.Tag{
Name: tag.Name,
Description: tag.Description.String,
IgnoreAutoTag: tag.IgnoreAutoTag,
CreatedAt: json.JSONTime{Time: tag.CreatedAt.Timestamp},
UpdatedAt: json.JSONTime{Time: tag.UpdatedAt.Timestamp},

View file

@ -2,6 +2,7 @@ package tag
import (
"context"
"database/sql"
"errors"
"github.com/stashapp/stash/pkg/models"
@ -23,7 +24,10 @@ const (
errParentsID = 6
)
const tagName = "testTag"
const (
tagName = "testTag"
description = "description"
)
var (
autoTagIgnored = true
@ -33,8 +37,12 @@ var (
func createTag(id int) models.Tag {
return models.Tag{
ID: id,
Name: tagName,
ID: id,
Name: tagName,
Description: sql.NullString{
String: description,
Valid: true,
},
IgnoreAutoTag: autoTagIgnored,
CreatedAt: models.SQLiteTimestamp{
Timestamp: createTime,
@ -48,6 +56,7 @@ func createTag(id int) models.Tag {
func createJSONTag(aliases []string, image string, parents []string) *jsonschema.Tag {
return &jsonschema.Tag{
Name: tagName,
Description: description,
Aliases: aliases,
IgnoreAutoTag: autoTagIgnored,
CreatedAt: json.JSONTime{

View file

@ -2,6 +2,7 @@ package tag
import (
"context"
"database/sql"
"fmt"
"github.com/stashapp/stash/pkg/models"
@ -42,6 +43,7 @@ type Importer struct {
func (i *Importer) PreImport(ctx context.Context) error {
i.tag = models.Tag{
Name: i.Input.Name,
Description: sql.NullString{String: i.Input.Description, Valid: true},
IgnoreAutoTag: i.Input.IgnoreAutoTag,
CreatedAt: models.SQLiteTimestamp{Timestamp: i.Input.CreatedAt.GetTime()},
UpdatedAt: models.SQLiteTimestamp{Timestamp: i.Input.UpdatedAt.GetTime()},

View file

@ -40,6 +40,7 @@ func TestImporterPreImport(t *testing.T) {
i := Importer{
Input: jsonschema.Tag{
Name: tagName,
Description: description,
Image: invalidImage,
IgnoreAutoTag: autoTagIgnored,
},

View file

@ -247,6 +247,13 @@ export const SettingsInterfacePanel: React.FC = () => {
/>
</SettingSection>
<SettingSection headingID="config.ui.tag_panel.heading">
<BooleanSetting
id="show-tag-card-on-hover"
headingID="config.ui.show_tag_card_on_hover.heading"
subHeadingID="config.ui.show_tag_card_on_hover.description"
checked={ui.showTagCardOnHover ?? true}
onChange={(v) => saveUI({ showTagCardOnHover: v })}
/>
<BooleanSetting
id="show-child-tagged-content"
headingID="config.ui.tag_panel.options.show_child_tagged_content.heading"

View file

@ -27,6 +27,7 @@ import { ConfigurationContext } from "src/hooks/Config";
import { useIntl } from "react-intl";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import { TagPopover } from "../Tags/TagPopover";
export type ValidTypes =
| GQL.SlimPerformerDataFragment
@ -659,7 +660,11 @@ export const TagSelect: React.FC<IFilterProps & { excludeIds?: string[] }> = (
};
}
return <reactSelectComponents.Option {...thisOptionProps} />;
return (
<TagPopover id={optionProps.data.value}>
<reactSelectComponents.Option {...thisOptionProps} />
</TagPopover>
);
};
const filterOption = (option: Option, rawInput: string): boolean => {

View file

@ -14,6 +14,7 @@ import TextUtils from "src/utils/text";
import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries";
import * as GQL from "src/core/generated-graphql";
import { TagPopover } from "../Tags/TagPopover";
interface IFile {
path: string;
@ -37,6 +38,7 @@ interface IProps {
}
export const TagLink: React.FC<IProps> = (props: IProps) => {
let id: string = "";
let link: string = "#";
let title: string = "";
if (props.tag) {
@ -55,6 +57,7 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
link = NavUtils.makeTagImagesUrl(props.tag);
break;
}
id = props.tag.id || "";
title = props.tag.name || "";
} else if (props.performer) {
link = NavUtils.makePerformerScenesUrl(props.performer);
@ -76,7 +79,9 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
}
return (
<Badge className={cx("tag-item", props.className)} variant="secondary">
<Link to={link}>{title}</Link>
<TagPopover id={id}>
<Link to={link}>{title}</Link>
</TagPopover>
</Badge>
);
};

View file

@ -4,7 +4,7 @@ import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { NavUtils } from "src/utils";
import { FormattedMessage } from "react-intl";
import { Icon } from "../Shared";
import { Icon, TruncatedText } from "../Shared";
import { GridCard } from "../Shared/GridCard";
import { PopoverCountButton } from "../Shared/PopoverCountButton";
import { faMapMarkerAlt, faUser } from "@fortawesome/free-solid-svg-icons";
@ -24,6 +24,18 @@ export const TagCard: React.FC<IProps> = ({
selected,
onSelectedChanged,
}) => {
function maybeRenderDescription() {
if (tag.description) {
return (
<TruncatedText
className="tag-description"
text={tag.description}
lineCount={3}
/>
);
}
}
function maybeRenderParents() {
if (tag.parents.length === 1) {
const parent = tag.parents[0];
@ -181,6 +193,7 @@ export const TagCard: React.FC<IProps> = ({
}
details={
<>
{maybeRenderDescription()}
{maybeRenderParents()}
{maybeRenderChildren()}
{maybeRenderPopoverButtonGroup()}

View file

@ -267,6 +267,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
renderImage()
)}
<h2>{tag.name}</h2>
<p>{tag.description}</p>
</div>
{!isEditing ? (
<>

View file

@ -47,6 +47,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
const schema = yup.object({
name: yup.string().required(),
description: yup.string().optional().nullable(),
aliases: yup
.array(yup.string().required())
.optional()
@ -65,6 +66,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
const initialValues = {
name: tag?.name,
description: tag?.description,
aliases: tag?.aliases,
parent_ids: (tag?.parents ?? []).map((t) => t.id),
child_ids: (tag?.children ?? []).map((t) => t.id),
@ -167,6 +169,20 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
</Col>
</Form.Group>
<Form.Group controlId="description" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "description" }),
})}
<Col xs={9}>
<Form.Control
as="textarea"
className="text-input"
placeholder={intl.formatMessage({ id: "description" })}
{...formik.getFieldProps("description")}
/>
</Col>
</Form.Group>
<Form.Group controlId="parent_tags" as={Row}>
{FormUtils.renderLabel({
title: intl.formatMessage({ id: "parent_tags" }),

View file

@ -0,0 +1,59 @@
import React from "react";
import { ErrorMessage, LoadingIndicator } from "../Shared";
import { HoverPopover } from "src/components/Shared";
import { useFindTag } from "../../core/StashService";
import { TagCard } from "./TagCard";
import { ConfigurationContext } from "../../hooks/Config";
import { IUIConfig } from "src/core/config";
interface ITagPopoverProps {
id?: string;
}
export const TagPopoverCard: React.FC<ITagPopoverCardProps> = ({ id }) => {
const { data, loading, error } = useFindTag(id ?? "");
if (loading)
return (
<div className="tag-popover-card-placeholder">
<LoadingIndicator card={true} message={""} />
</div>
);
if (error) return <ErrorMessage error={error.message} />;
if (!data?.findTag)
return <ErrorMessage error={`No tag found with id ${id}.`} />;
const tag = data.findTag;
return (
<div className="tag-popover-card">
<TagCard tag={tag} zoomIndex={0} />
</div>
);
};
export const TagPopover: React.FC<ITagPopoverProps> = ({ id, children }) => {
const { configuration: config } = React.useContext(ConfigurationContext);
const showTagCardOnHover =
(config?.ui as IUIConfig)?.showTagCardOnHover ?? true;
if (!id || !showTagCardOnHover) {
return <>{children}</>;
}
return (
<HoverPopover
placement={"top"}
enterDelay={500}
leaveDelay={100}
content={<TagPopoverCard id={id} />}
>
{children}
</HoverPopover>
);
};
interface ITagPopoverCardProps {
id?: string;
}

View file

@ -43,3 +43,28 @@
#tag-merge-menu .dropdown-item {
align-items: center;
}
.tag-card {
.tag-description + div {
margin-top: 1rem;
}
}
.tag-popover-card-placeholder {
display: flex;
max-width: 240px;
min-height: 314px;
width: calc(100vw - 2rem);
}
.tag-popover-card {
padding: 0.5rem;
text-align: left;
.card {
background: transparent;
box-shadow: none;
max-width: calc(100vw - 2rem);
padding: 0;
}
}

View file

@ -30,6 +30,7 @@ export interface IUIConfig {
lastNoteSeen?: number;
showChildTagContent?: boolean;
showChildStudioContent?: boolean;
showTagCardOnHover?: boolean;
}
function recentlyReleased(

View file

@ -5,6 +5,7 @@ After migrating, please run a scan on your entire library to populate missing da
* Import/export schema has changed and is incompatible with the previous version.
### ✨ New Features
* Added description field to Tags. ([#2708](https://github.com/stashapp/stash/pull/2708))
* Added option to include sub-studio/sub-tag content in Studio/Tag page. ([#2832](https://github.com/stashapp/stash/pull/2832))
* Added backup location configuration setting. ([#2953](https://github.com/stashapp/stash/pull/2953))
* Allow overriding UI localisation strings. ([#2837](https://github.com/stashapp/stash/pull/2837))
@ -14,6 +15,7 @@ After migrating, please run a scan on your entire library to populate missing da
* Added release notes dialog. ([#2726](https://github.com/stashapp/stash/pull/2726))
### 🎨 Improvements
* Optionally show Tag card when hovering over tag badge. ([#2708](https://github.com/stashapp/stash/pull/2708))
* Show default thumbnails for scenes and images where the actual image is not found. ([#2949](https://github.com/stashapp/stash/pull/2949))
* Added unix timestamp parsing in the `parseDate` scraper post processor. ([#2817](https://github.com/stashapp/stash/pull/2817))
* Improve matching scene order in the tagger to prioritise matching phashes and durations. ([#2840](https://github.com/stashapp/stash/pull/2840))

View file

@ -500,6 +500,10 @@
"description": "Show or hide different types of content on the navigation bar",
"heading": "Menu Items"
},
"show_tag_card_on_hover": {
"description": "Show tag card when hovering tag badges",
"heading": "Tag card tooltips"
},
"performers": {
"options": {
"image_location": {
@ -615,6 +619,7 @@
"death_date": "Death Date",
"death_year": "Death Year",
"descending": "Descending",
"description": "Description",
"detail": "Detail",
"details": "Details",
"developmentVersion": "Development Version",