mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Tag Favoriting (#4728)
* Add missing key unbind --------- Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -419,6 +419,9 @@ input TagFilterType {
|
|||||||
"Filter by tag aliases"
|
"Filter by tag aliases"
|
||||||
aliases: StringCriterionInput
|
aliases: StringCriterionInput
|
||||||
|
|
||||||
|
"Filter by favorite"
|
||||||
|
favorite: Boolean
|
||||||
|
|
||||||
"Filter by tag description"
|
"Filter by tag description"
|
||||||
description: StringCriterionInput
|
description: StringCriterionInput
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ type Tag {
|
|||||||
ignore_auto_tag: Boolean!
|
ignore_auto_tag: Boolean!
|
||||||
created_at: Time!
|
created_at: Time!
|
||||||
updated_at: Time!
|
updated_at: Time!
|
||||||
|
favorite: Boolean!
|
||||||
image_path: String # Resolver
|
image_path: String # Resolver
|
||||||
scene_count(depth: Int): Int! # Resolver
|
scene_count(depth: Int): Int! # Resolver
|
||||||
scene_marker_count(depth: Int): Int! # Resolver
|
scene_marker_count(depth: Int): Int! # Resolver
|
||||||
@@ -25,7 +25,7 @@ input TagCreateInput {
|
|||||||
description: String
|
description: String
|
||||||
aliases: [String!]
|
aliases: [String!]
|
||||||
ignore_auto_tag: Boolean
|
ignore_auto_tag: Boolean
|
||||||
|
favorite: Boolean
|
||||||
"This should be a URL or a base64 encoded data URL"
|
"This should be a URL or a base64 encoded data URL"
|
||||||
image: String
|
image: String
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ input TagUpdateInput {
|
|||||||
description: String
|
description: String
|
||||||
aliases: [String!]
|
aliases: [String!]
|
||||||
ignore_auto_tag: Boolean
|
ignore_auto_tag: Boolean
|
||||||
|
favorite: Boolean
|
||||||
"This should be a URL or a base64 encoded data URL"
|
"This should be a URL or a base64 encoded data URL"
|
||||||
image: String
|
image: String
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func (r *mutationResolver) TagCreate(ctx context.Context, input TagCreateInput)
|
|||||||
newTag := models.NewTag()
|
newTag := models.NewTag()
|
||||||
|
|
||||||
newTag.Name = input.Name
|
newTag.Name = input.Name
|
||||||
|
newTag.Favorite = translator.bool(input.Favorite)
|
||||||
newTag.Description = translator.string(input.Description)
|
newTag.Description = translator.string(input.Description)
|
||||||
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
newTag.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
|
||||||
|
|
||||||
@@ -136,6 +137,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input TagUpdateInput)
|
|||||||
// Populate tag from the input
|
// Populate tag from the input
|
||||||
updatedTag := models.NewTagPartial()
|
updatedTag := models.NewTagPartial()
|
||||||
|
|
||||||
|
updatedTag.Favorite = translator.optionalBool(input.Favorite, "favorite")
|
||||||
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
updatedTag.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
|
||||||
updatedTag.Description = translator.optionalString(input.Description, "description")
|
updatedTag.Description = translator.optionalString(input.Description, "description")
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
type Tag struct {
|
type Tag struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
|
Favorite bool `json:"favorite,omitempty"`
|
||||||
Aliases []string `json:"aliases,omitempty"`
|
Aliases []string `json:"aliases,omitempty"`
|
||||||
Image string `json:"image,omitempty"`
|
Image string `json:"image,omitempty"`
|
||||||
Parents []string `json:"parents,omitempty"`
|
Parents []string `json:"parents,omitempty"`
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
type Tag struct {
|
type Tag struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
Favorite bool `json:"favorite"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
IgnoreAutoTag bool `json:"ignore_auto_tag"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
@@ -24,6 +25,7 @@ func NewTag() Tag {
|
|||||||
type TagPartial struct {
|
type TagPartial struct {
|
||||||
Name OptionalString
|
Name OptionalString
|
||||||
Description OptionalString
|
Description OptionalString
|
||||||
|
Favorite OptionalBool
|
||||||
IgnoreAutoTag OptionalBool
|
IgnoreAutoTag OptionalBool
|
||||||
CreatedAt OptionalTime
|
CreatedAt OptionalTime
|
||||||
UpdatedAt OptionalTime
|
UpdatedAt OptionalTime
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ type TagFilterType struct {
|
|||||||
Name *StringCriterionInput `json:"name"`
|
Name *StringCriterionInput `json:"name"`
|
||||||
// Filter by tag aliases
|
// Filter by tag aliases
|
||||||
Aliases *StringCriterionInput `json:"aliases"`
|
Aliases *StringCriterionInput `json:"aliases"`
|
||||||
|
// Filter by tag favorites
|
||||||
|
Favorite *bool `json:"favorite"`
|
||||||
// Filter by tag description
|
// Filter by tag description
|
||||||
Description *StringCriterionInput `json:"description"`
|
Description *StringCriterionInput `json:"description"`
|
||||||
// Filter to only include tags missing this property
|
// Filter to only include tags missing this property
|
||||||
|
|||||||
@@ -1034,6 +1034,7 @@ type TagQueryInput struct {
|
|||||||
// Filter to search name - assumes like query unless quoted
|
// Filter to search name - assumes like query unless quoted
|
||||||
Name *string `json:"name,omitempty"`
|
Name *string `json:"name,omitempty"`
|
||||||
// Filter to category ID
|
// Filter to category ID
|
||||||
|
IsFavorite *bool `json:"is_favorite,omitempty"`
|
||||||
CategoryID *string `json:"category_id,omitempty"`
|
CategoryID *string `json:"category_id,omitempty"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PerPage int `json:"per_page"`
|
PerPage int `json:"per_page"`
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const (
|
|||||||
dbConnTimeout = 30
|
dbConnTimeout = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
var appSchemaVersion uint = 56
|
var appSchemaVersion uint = 57
|
||||||
|
|
||||||
//go:embed migrations/*.sql
|
//go:embed migrations/*.sql
|
||||||
var migrationsBox embed.FS
|
var migrationsBox embed.FS
|
||||||
|
|||||||
1
pkg/sqlite/migrations/57_tag_favorite.up.sql
Normal file
1
pkg/sqlite/migrations/57_tag_favorite.up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `tags` ADD COLUMN `favorite` boolean not null default '0';
|
||||||
@@ -1480,7 +1480,10 @@ func createPerformers(ctx context.Context, n int, o int) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
func getTagBoolValue(index int) bool {
|
||||||
|
index = index % 2
|
||||||
|
return index == 1
|
||||||
|
}
|
||||||
func getTagStringValue(index int, field string) string {
|
func getTagStringValue(index int, field string) string {
|
||||||
return "tag_" + strconv.FormatInt(int64(index), 10) + "_" + field
|
return "tag_" + strconv.FormatInt(int64(index), 10) + "_" + field
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const (
|
|||||||
type tagRow struct {
|
type tagRow struct {
|
||||||
ID int `db:"id" goqu:"skipinsert"`
|
ID int `db:"id" goqu:"skipinsert"`
|
||||||
Name null.String `db:"name"` // TODO: make schema non-nullable
|
Name null.String `db:"name"` // TODO: make schema non-nullable
|
||||||
|
Favorite bool `db:"favorite"`
|
||||||
Description zero.String `db:"description"`
|
Description zero.String `db:"description"`
|
||||||
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
IgnoreAutoTag bool `db:"ignore_auto_tag"`
|
||||||
CreatedAt Timestamp `db:"created_at"`
|
CreatedAt Timestamp `db:"created_at"`
|
||||||
@@ -41,6 +42,7 @@ type tagRow struct {
|
|||||||
func (r *tagRow) fromTag(o models.Tag) {
|
func (r *tagRow) fromTag(o models.Tag) {
|
||||||
r.ID = o.ID
|
r.ID = o.ID
|
||||||
r.Name = null.StringFrom(o.Name)
|
r.Name = null.StringFrom(o.Name)
|
||||||
|
r.Favorite = o.Favorite
|
||||||
r.Description = zero.StringFrom(o.Description)
|
r.Description = zero.StringFrom(o.Description)
|
||||||
r.IgnoreAutoTag = o.IgnoreAutoTag
|
r.IgnoreAutoTag = o.IgnoreAutoTag
|
||||||
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
|
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
|
||||||
@@ -51,6 +53,7 @@ func (r *tagRow) resolve() *models.Tag {
|
|||||||
ret := &models.Tag{
|
ret := &models.Tag{
|
||||||
ID: r.ID,
|
ID: r.ID,
|
||||||
Name: r.Name.String,
|
Name: r.Name.String,
|
||||||
|
Favorite: r.Favorite,
|
||||||
Description: r.Description.String,
|
Description: r.Description.String,
|
||||||
IgnoreAutoTag: r.IgnoreAutoTag,
|
IgnoreAutoTag: r.IgnoreAutoTag,
|
||||||
CreatedAt: r.CreatedAt.Timestamp,
|
CreatedAt: r.CreatedAt.Timestamp,
|
||||||
@@ -81,6 +84,7 @@ type tagRowRecord struct {
|
|||||||
func (r *tagRowRecord) fromPartial(o models.TagPartial) {
|
func (r *tagRowRecord) fromPartial(o models.TagPartial) {
|
||||||
r.setString("name", o.Name)
|
r.setString("name", o.Name)
|
||||||
r.setNullString("description", o.Description)
|
r.setNullString("description", o.Description)
|
||||||
|
r.setBool("favorite", o.Favorite)
|
||||||
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
|
||||||
r.setTimestamp("created_at", o.CreatedAt)
|
r.setTimestamp("created_at", o.CreatedAt)
|
||||||
r.setTimestamp("updated_at", o.UpdatedAt)
|
r.setTimestamp("updated_at", o.UpdatedAt)
|
||||||
@@ -498,6 +502,7 @@ func (qb *TagStore) makeFilter(ctx context.Context, tagFilter *models.TagFilterT
|
|||||||
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Name, tagTable+".name"))
|
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Name, tagTable+".name"))
|
||||||
query.handleCriterion(ctx, tagAliasCriterionHandler(qb, tagFilter.Aliases))
|
query.handleCriterion(ctx, tagAliasCriterionHandler(qb, tagFilter.Aliases))
|
||||||
|
|
||||||
|
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.Favorite, tagTable+".favorite", nil))
|
||||||
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Description, tagTable+".description"))
|
query.handleCriterion(ctx, stringCriterionHandler(tagFilter.Description, tagTable+".description"))
|
||||||
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil))
|
query.handleCriterion(ctx, boolCriterionHandler(tagFilter.IgnoreAutoTag, tagTable+".ignore_auto_tag", nil))
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ func ToJSON(ctx context.Context, reader FinderAliasImageGetter, tag *models.Tag)
|
|||||||
newTagJSON := jsonschema.Tag{
|
newTagJSON := jsonschema.Tag{
|
||||||
Name: tag.Name,
|
Name: tag.Name,
|
||||||
Description: tag.Description,
|
Description: tag.Description,
|
||||||
|
Favorite: tag.Favorite,
|
||||||
IgnoreAutoTag: tag.IgnoreAutoTag,
|
IgnoreAutoTag: tag.IgnoreAutoTag,
|
||||||
CreatedAt: json.JSONTime{Time: tag.CreatedAt},
|
CreatedAt: json.JSONTime{Time: tag.CreatedAt},
|
||||||
UpdatedAt: json.JSONTime{Time: tag.UpdatedAt},
|
UpdatedAt: json.JSONTime{Time: tag.UpdatedAt},
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func createTag(id int) models.Tag {
|
|||||||
return models.Tag{
|
return models.Tag{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: tagName,
|
Name: tagName,
|
||||||
|
Favorite: true,
|
||||||
Description: description,
|
Description: description,
|
||||||
IgnoreAutoTag: autoTagIgnored,
|
IgnoreAutoTag: autoTagIgnored,
|
||||||
CreatedAt: createTime,
|
CreatedAt: createTime,
|
||||||
@@ -47,6 +48,7 @@ func createTag(id int) models.Tag {
|
|||||||
func createJSONTag(aliases []string, image string, parents []string) *jsonschema.Tag {
|
func createJSONTag(aliases []string, image string, parents []string) *jsonschema.Tag {
|
||||||
return &jsonschema.Tag{
|
return &jsonschema.Tag{
|
||||||
Name: tagName,
|
Name: tagName,
|
||||||
|
Favorite: true,
|
||||||
Description: description,
|
Description: description,
|
||||||
Aliases: aliases,
|
Aliases: aliases,
|
||||||
IgnoreAutoTag: autoTagIgnored,
|
IgnoreAutoTag: autoTagIgnored,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func (i *Importer) PreImport(ctx context.Context) error {
|
|||||||
i.tag = models.Tag{
|
i.tag = models.Tag{
|
||||||
Name: i.Input.Name,
|
Name: i.Input.Name,
|
||||||
Description: i.Input.Description,
|
Description: i.Input.Description,
|
||||||
|
Favorite: i.Input.Favorite,
|
||||||
IgnoreAutoTag: i.Input.IgnoreAutoTag,
|
IgnoreAutoTag: i.Input.IgnoreAutoTag,
|
||||||
CreatedAt: i.Input.CreatedAt.GetTime(),
|
CreatedAt: i.Input.CreatedAt.GetTime(),
|
||||||
UpdatedAt: i.Input.UpdatedAt.GetTime(),
|
UpdatedAt: i.Input.UpdatedAt.GetTime(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ fragment TagData on Tag {
|
|||||||
description
|
description
|
||||||
aliases
|
aliases
|
||||||
ignore_auto_tag
|
ignore_auto_tag
|
||||||
|
favorite
|
||||||
image_path
|
image_path
|
||||||
scene_count
|
scene_count
|
||||||
scene_count_all: scene_count(depth: -1)
|
scene_count_all: scene_count(depth: -1)
|
||||||
@@ -28,6 +29,7 @@ fragment TagData on Tag {
|
|||||||
fragment SelectTagData on Tag {
|
fragment SelectTagData on Tag {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
favorite
|
||||||
description
|
description
|
||||||
aliases
|
aliases
|
||||||
image_path
|
image_path
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||||||
Mousetrap.unbind("e");
|
Mousetrap.unbind("e");
|
||||||
Mousetrap.unbind("d d");
|
Mousetrap.unbind("d d");
|
||||||
Mousetrap.unbind(",");
|
Mousetrap.unbind(",");
|
||||||
|
Mousetrap.unbind("f");
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ButtonGroup } from "react-bootstrap";
|
import { Button, ButtonGroup } from "react-bootstrap";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
@@ -8,7 +8,10 @@ import { TruncatedText } from "../Shared/TruncatedText";
|
|||||||
import { GridCard, calculateCardWidth } from "../Shared/GridCard/GridCard";
|
import { GridCard, calculateCardWidth } from "../Shared/GridCard/GridCard";
|
||||||
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
import { PopoverCountButton } from "../Shared/PopoverCountButton";
|
||||||
import ScreenUtils from "src/utils/screen";
|
import ScreenUtils from "src/utils/screen";
|
||||||
|
import { Icon } from "../Shared/Icon";
|
||||||
|
import { faHeart } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { useTagUpdate } from "src/core/StashService";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
tag: GQL.TagDataFragment;
|
tag: GQL.TagDataFragment;
|
||||||
containerWidth?: number;
|
containerWidth?: number;
|
||||||
@@ -27,7 +30,7 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
onSelectedChanged,
|
onSelectedChanged,
|
||||||
}) => {
|
}) => {
|
||||||
const [cardWidth, setCardWidth] = useState<number>();
|
const [cardWidth, setCardWidth] = useState<number>();
|
||||||
|
const [updateTag] = useTagUpdate();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerWidth || zoomIndex === undefined || ScreenUtils.isMobile())
|
if (!containerWidth || zoomIndex === undefined || ScreenUtils.isMobile())
|
||||||
return;
|
return;
|
||||||
@@ -65,7 +68,36 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function renderFavoriteIcon() {
|
||||||
|
return (
|
||||||
|
<Link to="" onClick={(e) => e.preventDefault()}>
|
||||||
|
<Button
|
||||||
|
className={cx(
|
||||||
|
"minimal",
|
||||||
|
"mousetrap",
|
||||||
|
"favorite-button",
|
||||||
|
tag.favorite ? "favorite" : "not-favorite"
|
||||||
|
)}
|
||||||
|
onClick={() => onToggleFavorite!(!tag.favorite)}
|
||||||
|
>
|
||||||
|
<Icon icon={faHeart} size="2x" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onToggleFavorite(v: boolean) {
|
||||||
|
if (tag.id) {
|
||||||
|
updateTag({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: tag.id,
|
||||||
|
favorite: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
function maybeRenderParents() {
|
function maybeRenderParents() {
|
||||||
if (tag.parents.length === 1) {
|
if (tag.parents.length === 1) {
|
||||||
const parent = tag.parents[0];
|
const parent = tag.parents[0];
|
||||||
@@ -230,6 +262,7 @@ export const TagCard: React.FC<IProps> = ({
|
|||||||
{maybeRenderChildren()}
|
{maybeRenderChildren()}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
overlays={<>{renderFavoriteIcon()}</>}
|
||||||
popovers={maybeRenderPopoverButtonGroup()}
|
popovers={maybeRenderPopoverButtonGroup()}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
selecting={selecting}
|
selecting={selecting}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { TagMergeModal } from "./TagMergeDialog";
|
|||||||
import {
|
import {
|
||||||
faChevronDown,
|
faChevronDown,
|
||||||
faChevronUp,
|
faChevronUp,
|
||||||
|
faHeart,
|
||||||
faSignInAlt,
|
faSignInAlt,
|
||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
@@ -140,6 +141,19 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
}
|
}
|
||||||
}, [setTabKey, populatedDefaultTab, tabKey]);
|
}, [setTabKey, populatedDefaultTab, tabKey]);
|
||||||
|
|
||||||
|
function setFavorite(v: boolean) {
|
||||||
|
if (tag.id) {
|
||||||
|
updateTag({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
id: tag.id,
|
||||||
|
favorite: v,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("e", () => toggleEditing());
|
Mousetrap.bind("e", () => toggleEditing());
|
||||||
@@ -147,6 +161,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
setIsDeleteAlertOpen(true);
|
setIsDeleteAlertOpen(true);
|
||||||
});
|
});
|
||||||
Mousetrap.bind(",", () => setCollapsed(!collapsed));
|
Mousetrap.bind(",", () => setCollapsed(!collapsed));
|
||||||
|
Mousetrap.bind("f", () => setFavorite(!tag.favorite));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -156,6 +171,7 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
Mousetrap.unbind("e");
|
Mousetrap.unbind("e");
|
||||||
Mousetrap.unbind("d d");
|
Mousetrap.unbind("d d");
|
||||||
Mousetrap.unbind(",");
|
Mousetrap.unbind(",");
|
||||||
|
Mousetrap.unbind("f");
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -298,6 +314,17 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const renderClickableIcons = () => (
|
||||||
|
<span className="name-icons">
|
||||||
|
<Button
|
||||||
|
className={cx("minimal", tag.favorite ? "favorite" : "not-favorite")}
|
||||||
|
onClick={() => setFavorite(!tag.favorite)}
|
||||||
|
>
|
||||||
|
<Icon icon={faHeart} />
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
function renderMergeButton() {
|
function renderMergeButton() {
|
||||||
return (
|
return (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
@@ -528,10 +555,11 @@ const TagPage: React.FC<IProps> = ({ tag, tabKey }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="studio-head col">
|
<div className="tag-head col">
|
||||||
<h2>
|
<h2>
|
||||||
<span className="tag-name">{tag.name}</span>
|
<span className="tag-name">{tag.name}</span>
|
||||||
{maybeRenderShowCollapseButton()}
|
{maybeRenderShowCollapseButton()}
|
||||||
|
{renderClickableIcons()}
|
||||||
</h2>
|
</h2>
|
||||||
{maybeRenderAliases()}
|
{maybeRenderAliases()}
|
||||||
{maybeRenderDetails()}
|
{maybeRenderDetails()}
|
||||||
|
|||||||
@@ -31,6 +31,57 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.btn.favorite-button {
|
||||||
|
opacity: 1;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
top: 10px;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
|
||||||
|
svg.fa-icon {
|
||||||
|
margin-left: 0.4rem;
|
||||||
|
margin-right: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.not-favorite {
|
||||||
|
color: rgba(191, 204, 214, 0.5);
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.favorite {
|
||||||
|
color: #ff7373;
|
||||||
|
filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.9));
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:active:focus {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover button.btn.favorite-button.not-favorite {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#tag-page {
|
||||||
|
.tag-head {
|
||||||
|
.name-icons {
|
||||||
|
.not-favorite {
|
||||||
|
color: rgba(191, 204, 214, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite {
|
||||||
|
color: #ff7373;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-details {
|
.tag-details {
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ export class FavoritePerformerCriterion extends BooleanCriterion {
|
|||||||
super(FavoritePerformerCriterionOption);
|
super(FavoritePerformerCriterionOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export const FavoriteTagCriterionOption = new BooleanCriterionOption(
|
||||||
|
"favourite",
|
||||||
|
"favorite",
|
||||||
|
() => new FavoriteTagCriterion()
|
||||||
|
);
|
||||||
|
|
||||||
|
export class FavoriteTagCriterion extends BooleanCriterion {
|
||||||
|
constructor() {
|
||||||
|
super(FavoriteTagCriterionOption);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const FavoriteStudioCriterionOption = new BooleanCriterionOption(
|
export const FavoriteStudioCriterionOption = new BooleanCriterionOption(
|
||||||
"favourite",
|
"favourite",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ChildTagsCriterionOption,
|
ChildTagsCriterionOption,
|
||||||
ParentTagsCriterionOption,
|
ParentTagsCriterionOption,
|
||||||
} from "./criteria/tags";
|
} from "./criteria/tags";
|
||||||
|
import { FavoriteTagCriterionOption } from "./criteria/favorite";
|
||||||
|
|
||||||
const defaultSortBy = "name";
|
const defaultSortBy = "name";
|
||||||
const sortByOptions = ["name", "random"]
|
const sortByOptions = ["name", "random"]
|
||||||
@@ -42,6 +43,7 @@ const sortByOptions = ["name", "random"]
|
|||||||
|
|
||||||
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
const displayModeOptions = [DisplayMode.Grid, DisplayMode.List];
|
||||||
const criterionOptions = [
|
const criterionOptions = [
|
||||||
|
FavoriteTagCriterionOption,
|
||||||
createMandatoryStringCriterionOption("name"),
|
createMandatoryStringCriterionOption("name"),
|
||||||
TagIsMissingCriterionOption,
|
TagIsMissingCriterionOption,
|
||||||
createStringCriterionOption("aliases"),
|
createStringCriterionOption("aliases"),
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export type CriterionType =
|
|||||||
| "parent_count"
|
| "parent_count"
|
||||||
| "child_count"
|
| "child_count"
|
||||||
| "performer_favorite"
|
| "performer_favorite"
|
||||||
|
| "favorite"
|
||||||
| "performer_age"
|
| "performer_age"
|
||||||
| "duplicated"
|
| "duplicated"
|
||||||
| "ignore_auto_tag"
|
| "ignore_auto_tag"
|
||||||
|
|||||||
Reference in New Issue
Block a user