Skip to content

Commit

Permalink
Add support for favorite Studios (#4675)
Browse files Browse the repository at this point in the history
* Backend changes
* Add favorite icon to studio cards
* Add favorite button to studio page
* Add studio favorite filtering
  • Loading branch information
WithoutPants authored Mar 14, 2024
1 parent e592938 commit 8c45458
Show file tree
Hide file tree
Showing 25 changed files with 185 additions and 52 deletions.
2 changes: 2 additions & 0 deletions graphql/schema/types/filters.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ input StudioFilterType {
is_missing: String
# rating expressed as 1-100
rating100: IntCriterionInput
"Filter by favorite"
favorite: Boolean
"Filter by scene count"
scene_count: IntCriterionInput
"Filter by image count"
Expand Down
3 changes: 3 additions & 0 deletions graphql/schema/types/studio.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Studio {
stash_ids: [StashID!]!
# rating expressed as 1-100
rating100: Int
favorite: Boolean!
details: String
created_at: Time!
updated_at: Time!
Expand All @@ -31,6 +32,7 @@ input StudioCreateInput {
stash_ids: [StashIDInput!]
# rating expressed as 1-100
rating100: Int
favorite: Boolean
details: String
aliases: [String!]
ignore_auto_tag: Boolean
Expand All @@ -46,6 +48,7 @@ input StudioUpdateInput {
stash_ids: [StashIDInput!]
# rating expressed as 1-100
rating100: Int
favorite: Boolean
details: String
aliases: [String!]
ignore_auto_tag: Boolean
Expand Down
2 changes: 2 additions & 0 deletions internal/api/resolver_mutation_studio.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.Name = input.Name
newStudio.URL = translator.string(input.URL)
newStudio.Rating = input.Rating100
newStudio.Favorite = translator.bool(input.Favorite)
newStudio.Details = translator.string(input.Details)
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
Expand Down Expand Up @@ -103,6 +104,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.URL = translator.optionalString(input.URL, "url")
updatedStudio.Details = translator.optionalString(input.Details, "details")
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")
Expand Down
1 change: 1 addition & 0 deletions pkg/models/jsonschema/studio.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Studio struct {
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
Rating int `json:"rating,omitempty"`
Favorite bool `json:"favorite,omitempty"`
Details string `json:"details,omitempty"`
Aliases []string `json:"aliases,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"`
Expand Down
2 changes: 2 additions & 0 deletions pkg/models/model_studio.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Studio struct {
UpdatedAt time.Time `json:"updated_at"`
// Rating expressed in 1-100 scale
Rating *int `json:"rating"`
Favorite bool `json:"favorite"`
Details string `json:"details"`
IgnoreAutoTag bool `json:"ignore_auto_tag"`

Expand All @@ -37,6 +38,7 @@ type StudioPartial struct {
ParentID OptionalInt
// Rating expressed in 1-100 scale
Rating OptionalInt
Favorite OptionalBool
Details OptionalString
CreatedAt OptionalTime
UpdatedAt OptionalTime
Expand Down
4 changes: 4 additions & 0 deletions pkg/models/studio.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type StudioFilterType struct {
IsMissing *string `json:"is_missing"`
// Filter by rating expressed as 1-100
Rating100 *IntCriterionInput `json:"rating100"`
// Filter by favorite
Favorite *bool `json:"favorite"`
// Filter by scene count
SceneCount *IntCriterionInput `json:"scene_count"`
// Filter by image count
Expand Down Expand Up @@ -44,6 +46,7 @@ type StudioCreateInput struct {
Image *string `json:"image"`
StashIds []StashID `json:"stash_ids"`
Rating100 *int `json:"rating100"`
Favorite *bool `json:"favorite"`
Details *string `json:"details"`
Aliases []string `json:"aliases"`
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
Expand All @@ -58,6 +61,7 @@ type StudioUpdateInput struct {
Image *string `json:"image"`
StashIds []StashID `json:"stash_ids"`
Rating100 *int `json:"rating100"`
Favorite *bool `json:"favorite"`
Details *string `json:"details"`
Aliases []string `json:"aliases"`
IgnoreAutoTag *bool `json:"ignore_auto_tag"`
Expand Down
2 changes: 1 addition & 1 deletion pkg/sqlite/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const (
dbConnTimeout = 30
)

var appSchemaVersion uint = 55
var appSchemaVersion uint = 56

//go:embed migrations/*.sql
var migrationsBox embed.FS
Expand Down
1 change: 1 addition & 0 deletions pkg/sqlite/migrations/56_studio_favorite.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE `studios` ADD COLUMN `favorite` boolean not null default '0';
6 changes: 6 additions & 0 deletions pkg/sqlite/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,11 @@ func createStudioFromModel(ctx context.Context, sqb *sqlite.StudioStore, studio
return nil
}

func getStudioBoolValue(index int) bool {
index = index % 2
return index == 1
}

// createStudios creates n studios with plain Name and o studios with camel cased NaMe included
func createStudios(ctx context.Context, n int, o int) error {
sqb := db.Studio
Expand All @@ -1630,6 +1635,7 @@ func createStudios(ctx context.Context, n int, o int) error {
studio := models.Studio{
Name: name,
URL: getStudioStringValue(index, urlField),
Favorite: getStudioBoolValue(index),
IgnoreAutoTag: getIgnoreAutoTag(i),
}
// only add aliases for some scenes
Expand Down
5 changes: 5 additions & 0 deletions pkg/sqlite/studio.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type studioRow struct {
UpdatedAt Timestamp `db:"updated_at"`
// expressed as 1-100
Rating null.Int `db:"rating"`
Favorite bool `db:"favorite"`
Details zero.String `db:"details"`
IgnoreAutoTag bool `db:"ignore_auto_tag"`

Expand All @@ -51,6 +52,7 @@ func (r *studioRow) fromStudio(o models.Studio) {
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
r.UpdatedAt = Timestamp{Timestamp: o.UpdatedAt}
r.Rating = intFromPtr(o.Rating)
r.Favorite = o.Favorite
r.Details = zero.StringFrom(o.Details)
r.IgnoreAutoTag = o.IgnoreAutoTag
}
Expand All @@ -64,6 +66,7 @@ func (r *studioRow) resolve() *models.Studio {
CreatedAt: r.CreatedAt.Timestamp,
UpdatedAt: r.UpdatedAt.Timestamp,
Rating: nullIntPtr(r.Rating),
Favorite: r.Favorite,
Details: r.Details.String,
IgnoreAutoTag: r.IgnoreAutoTag,
}
Expand All @@ -82,6 +85,7 @@ func (r *studioRowRecord) fromPartial(o models.StudioPartial) {
r.setTimestamp("created_at", o.CreatedAt)
r.setTimestamp("updated_at", o.UpdatedAt)
r.setNullInt("rating", o.Rating)
r.setBool("favorite", o.Favorite)
r.setNullString("details", o.Details)
r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
}
Expand Down Expand Up @@ -496,6 +500,7 @@ func (qb *StudioStore) makeFilter(ctx context.Context, studioFilter *models.Stud
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.Details, studioTable+".details"))
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil))
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.Favorite, studioTable+".favorite", nil))
query.handleCriterion(ctx, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))

query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {
Expand Down
1 change: 1 addition & 0 deletions pkg/studio/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models
Name: studio.Name,
URL: studio.URL,
Details: studio.Details,
Favorite: studio.Favorite,
IgnoreAutoTag: studio.IgnoreAutoTag,
CreatedAt: json.JSONTime{Time: studio.CreatedAt},
UpdatedAt: json.JSONTime{Time: studio.UpdatedAt},
Expand Down
8 changes: 5 additions & 3 deletions pkg/studio/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func createFullStudio(id int, parentID int) models.Studio {
Name: studioName,
URL: url,
Details: details,
Favorite: true,
CreatedAt: createTime,
UpdatedAt: updateTime,
Rating: &rating,
Expand Down Expand Up @@ -89,9 +90,10 @@ func createEmptyStudio(id int) models.Studio {

func createFullJSONStudio(parentStudio, image string, aliases []string) *jsonschema.Studio {
return &jsonschema.Studio{
Name: studioName,
URL: url,
Details: details,
Name: studioName,
URL: url,
Details: details,
Favorite: true,
CreatedAt: json.JSONTime{
Time: createTime,
},
Expand Down
1 change: 1 addition & 0 deletions pkg/studio/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func studioJSONtoStudio(studioJSON jsonschema.Studio) models.Studio {
URL: studioJSON.URL,
Aliases: models.NewRelatedStrings(studioJSON.Aliases),
Details: studioJSON.Details,
Favorite: studioJSON.Favorite,
IgnoreAutoTag: studioJSON.IgnoreAutoTag,
CreatedAt: studioJSON.CreatedAt.GetTime(),
UpdatedAt: studioJSON.UpdatedAt.GetTime(),
Expand Down
1 change: 1 addition & 0 deletions ui/v2.5/graphql/data/studio.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ fragment StudioData on Studio {
}
details
rating100
favorite
aliases
}

Expand Down
27 changes: 6 additions & 21 deletions ui/v2.5/src/components/Performers/PerformerCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@ import {
} from "src/models/list-filter/criteria/criterion";
import { PopoverCountButton } from "../Shared/PopoverCountButton";
import GenderIcon from "./GenderIcon";
import { faHeart, faTag } from "@fortawesome/free-solid-svg-icons";
import { faTag } from "@fortawesome/free-solid-svg-icons";
import { RatingBanner } from "../Shared/RatingBanner";
import cx from "classnames";
import { usePerformerUpdate } from "src/core/StashService";
import { ILabeledId } from "src/models/list-filter/types";
import ScreenUtils from "src/utils/screen";
import { FavoriteIcon } from "../Shared/FavoriteIcon";

export interface IPerformerCardExtraCriteria {
scenes?: Criterion<CriterionValue>[];
Expand Down Expand Up @@ -82,24 +82,6 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
setCardWidth(fittedCardWidth);
}, [containerWidth]);

function renderFavoriteIcon() {
return (
<Link to="" onClick={(e) => e.preventDefault()}>
<Button
className={cx(
"minimal",
"mousetrap",
"favorite-button",
performer.favorite ? "favorite" : "not-favorite"
)}
onClick={() => onToggleFavorite!(!performer.favorite)}
>
<Icon icon={faHeart} size="2x" />
</Button>
</Link>
);
}

function onToggleFavorite(v: boolean) {
if (performer.id) {
updatePerformer({
Expand Down Expand Up @@ -292,7 +274,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
}
overlays={
<>
{renderFavoriteIcon()}
<FavoriteIcon
favorite={performer.favorite}
onToggleFavorite={onToggleFavorite}
/>
{maybeRenderRatingBanner()}
{maybeRenderFlag()}
</>
Expand Down
21 changes: 0 additions & 21 deletions ui/v2.5/src/components/Performers/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -80,36 +80,15 @@
}

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 {
Expand Down
24 changes: 24 additions & 0 deletions ui/v2.5/src/components/Shared/FavoriteIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from "react";
import { Icon } from "../Shared/Icon";
import { Button } from "react-bootstrap";
import { faHeart } from "@fortawesome/free-solid-svg-icons";
import cx from "classnames";

export const FavoriteIcon: React.FC<{
favorite: boolean;
onToggleFavorite: (v: boolean) => void;
}> = ({ favorite, onToggleFavorite }) => {
return (
<Button
className={cx(
"minimal",
"mousetrap",
"favorite-button",
favorite ? "favorite" : "not-favorite"
)}
onClick={() => onToggleFavorite!(!favorite)}
>
<Icon icon={faHeart} size="2x" />
</Button>
);
};
24 changes: 24 additions & 0 deletions ui/v2.5/src/components/Shared/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,27 @@ div.react-datepicker {
align-items: baseline;
display: flex;
}

button.btn.favorite-button {
opacity: 1;
transition: opacity 0.5s;

&.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;
}
}
Loading

0 comments on commit 8c45458

Please sign in to comment.