Add support for favorite Studios (#4675)

* Backend changes
* Add favorite icon to studio cards
* Add favorite button to studio page
* Add studio favorite filtering
This commit is contained in:
WithoutPants 2024-03-14 11:17:44 +11:00 committed by GitHub
parent e5929389b4
commit 8c454582c7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 185 additions and 52 deletions

View file

@ -325,6 +325,8 @@ input StudioFilterType {
is_missing: String is_missing: String
# rating expressed as 1-100 # rating expressed as 1-100
rating100: IntCriterionInput rating100: IntCriterionInput
"Filter by favorite"
favorite: Boolean
"Filter by scene count" "Filter by scene count"
scene_count: IntCriterionInput scene_count: IntCriterionInput
"Filter by image count" "Filter by image count"

View file

@ -16,6 +16,7 @@ type Studio {
stash_ids: [StashID!]! stash_ids: [StashID!]!
# rating expressed as 1-100 # rating expressed as 1-100
rating100: Int rating100: Int
favorite: Boolean!
details: String details: String
created_at: Time! created_at: Time!
updated_at: Time! updated_at: Time!
@ -31,6 +32,7 @@ input StudioCreateInput {
stash_ids: [StashIDInput!] stash_ids: [StashIDInput!]
# rating expressed as 1-100 # rating expressed as 1-100
rating100: Int rating100: Int
favorite: Boolean
details: String details: String
aliases: [String!] aliases: [String!]
ignore_auto_tag: Boolean ignore_auto_tag: Boolean
@ -46,6 +48,7 @@ input StudioUpdateInput {
stash_ids: [StashIDInput!] stash_ids: [StashIDInput!]
# rating expressed as 1-100 # rating expressed as 1-100
rating100: Int rating100: Int
favorite: Boolean
details: String details: String
aliases: [String!] aliases: [String!]
ignore_auto_tag: Boolean ignore_auto_tag: Boolean

View file

@ -35,6 +35,7 @@ func (r *mutationResolver) StudioCreate(ctx context.Context, input models.Studio
newStudio.Name = input.Name newStudio.Name = input.Name
newStudio.URL = translator.string(input.URL) newStudio.URL = translator.string(input.URL)
newStudio.Rating = input.Rating100 newStudio.Rating = input.Rating100
newStudio.Favorite = translator.bool(input.Favorite)
newStudio.Details = translator.string(input.Details) newStudio.Details = translator.string(input.Details)
newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag) newStudio.IgnoreAutoTag = translator.bool(input.IgnoreAutoTag)
newStudio.Aliases = models.NewRelatedStrings(input.Aliases) newStudio.Aliases = models.NewRelatedStrings(input.Aliases)
@ -103,6 +104,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
updatedStudio.URL = translator.optionalString(input.URL, "url") updatedStudio.URL = translator.optionalString(input.URL, "url")
updatedStudio.Details = translator.optionalString(input.Details, "details") updatedStudio.Details = translator.optionalString(input.Details, "details")
updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100") updatedStudio.Rating = translator.optionalInt(input.Rating100, "rating100")
updatedStudio.Favorite = translator.optionalBool(input.Favorite, "favorite")
updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag") updatedStudio.IgnoreAutoTag = translator.optionalBool(input.IgnoreAutoTag, "ignore_auto_tag")
updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases") updatedStudio.Aliases = translator.updateStrings(input.Aliases, "aliases")
updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids") updatedStudio.StashIDs = translator.updateStashIDs(input.StashIds, "stash_ids")

View file

@ -18,6 +18,7 @@ type Studio struct {
CreatedAt json.JSONTime `json:"created_at,omitempty"` CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"` UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
Rating int `json:"rating,omitempty"` Rating int `json:"rating,omitempty"`
Favorite bool `json:"favorite,omitempty"`
Details string `json:"details,omitempty"` Details string `json:"details,omitempty"`
Aliases []string `json:"aliases,omitempty"` Aliases []string `json:"aliases,omitempty"`
StashIDs []models.StashID `json:"stash_ids,omitempty"` StashIDs []models.StashID `json:"stash_ids,omitempty"`

View file

@ -14,6 +14,7 @@ type Studio struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
// Rating expressed in 1-100 scale // Rating expressed in 1-100 scale
Rating *int `json:"rating"` Rating *int `json:"rating"`
Favorite bool `json:"favorite"`
Details string `json:"details"` Details string `json:"details"`
IgnoreAutoTag bool `json:"ignore_auto_tag"` IgnoreAutoTag bool `json:"ignore_auto_tag"`
@ -37,6 +38,7 @@ type StudioPartial struct {
ParentID OptionalInt ParentID OptionalInt
// Rating expressed in 1-100 scale // Rating expressed in 1-100 scale
Rating OptionalInt Rating OptionalInt
Favorite OptionalBool
Details OptionalString Details OptionalString
CreatedAt OptionalTime CreatedAt OptionalTime
UpdatedAt OptionalTime UpdatedAt OptionalTime

View file

@ -16,6 +16,8 @@ type StudioFilterType struct {
IsMissing *string `json:"is_missing"` IsMissing *string `json:"is_missing"`
// Filter by rating expressed as 1-100 // Filter by rating expressed as 1-100
Rating100 *IntCriterionInput `json:"rating100"` Rating100 *IntCriterionInput `json:"rating100"`
// Filter by favorite
Favorite *bool `json:"favorite"`
// Filter by scene count // Filter by scene count
SceneCount *IntCriterionInput `json:"scene_count"` SceneCount *IntCriterionInput `json:"scene_count"`
// Filter by image count // Filter by image count
@ -44,6 +46,7 @@ type StudioCreateInput struct {
Image *string `json:"image"` Image *string `json:"image"`
StashIds []StashID `json:"stash_ids"` StashIds []StashID `json:"stash_ids"`
Rating100 *int `json:"rating100"` Rating100 *int `json:"rating100"`
Favorite *bool `json:"favorite"`
Details *string `json:"details"` Details *string `json:"details"`
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
IgnoreAutoTag *bool `json:"ignore_auto_tag"` IgnoreAutoTag *bool `json:"ignore_auto_tag"`
@ -58,6 +61,7 @@ type StudioUpdateInput struct {
Image *string `json:"image"` Image *string `json:"image"`
StashIds []StashID `json:"stash_ids"` StashIds []StashID `json:"stash_ids"`
Rating100 *int `json:"rating100"` Rating100 *int `json:"rating100"`
Favorite *bool `json:"favorite"`
Details *string `json:"details"` Details *string `json:"details"`
Aliases []string `json:"aliases"` Aliases []string `json:"aliases"`
IgnoreAutoTag *bool `json:"ignore_auto_tag"` IgnoreAutoTag *bool `json:"ignore_auto_tag"`

View file

@ -30,7 +30,7 @@ const (
dbConnTimeout = 30 dbConnTimeout = 30
) )
var appSchemaVersion uint = 55 var appSchemaVersion uint = 56
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrationsBox embed.FS var migrationsBox embed.FS

View file

@ -0,0 +1 @@
ALTER TABLE `studios` ADD COLUMN `favorite` boolean not null default '0';

View file

@ -1610,6 +1610,11 @@ func createStudioFromModel(ctx context.Context, sqb *sqlite.StudioStore, studio
return nil 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 // 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 { func createStudios(ctx context.Context, n int, o int) error {
sqb := db.Studio sqb := db.Studio
@ -1630,6 +1635,7 @@ func createStudios(ctx context.Context, n int, o int) error {
studio := models.Studio{ studio := models.Studio{
Name: name, Name: name,
URL: getStudioStringValue(index, urlField), URL: getStudioStringValue(index, urlField),
Favorite: getStudioBoolValue(index),
IgnoreAutoTag: getIgnoreAutoTag(i), IgnoreAutoTag: getIgnoreAutoTag(i),
} }
// only add aliases for some scenes // only add aliases for some scenes

View file

@ -36,6 +36,7 @@ type studioRow struct {
UpdatedAt Timestamp `db:"updated_at"` UpdatedAt Timestamp `db:"updated_at"`
// expressed as 1-100 // expressed as 1-100
Rating null.Int `db:"rating"` Rating null.Int `db:"rating"`
Favorite bool `db:"favorite"`
Details zero.String `db:"details"` Details zero.String `db:"details"`
IgnoreAutoTag bool `db:"ignore_auto_tag"` IgnoreAutoTag bool `db:"ignore_auto_tag"`
@ -51,6 +52,7 @@ func (r *studioRow) fromStudio(o models.Studio) {
r.CreatedAt = Timestamp{Timestamp: o.CreatedAt} r.CreatedAt = Timestamp{Timestamp: o.CreatedAt}
r.UpdatedAt = Timestamp{Timestamp: o.UpdatedAt} r.UpdatedAt = Timestamp{Timestamp: o.UpdatedAt}
r.Rating = intFromPtr(o.Rating) r.Rating = intFromPtr(o.Rating)
r.Favorite = o.Favorite
r.Details = zero.StringFrom(o.Details) r.Details = zero.StringFrom(o.Details)
r.IgnoreAutoTag = o.IgnoreAutoTag r.IgnoreAutoTag = o.IgnoreAutoTag
} }
@ -64,6 +66,7 @@ func (r *studioRow) resolve() *models.Studio {
CreatedAt: r.CreatedAt.Timestamp, CreatedAt: r.CreatedAt.Timestamp,
UpdatedAt: r.UpdatedAt.Timestamp, UpdatedAt: r.UpdatedAt.Timestamp,
Rating: nullIntPtr(r.Rating), Rating: nullIntPtr(r.Rating),
Favorite: r.Favorite,
Details: r.Details.String, Details: r.Details.String,
IgnoreAutoTag: r.IgnoreAutoTag, IgnoreAutoTag: r.IgnoreAutoTag,
} }
@ -82,6 +85,7 @@ func (r *studioRowRecord) fromPartial(o models.StudioPartial) {
r.setTimestamp("created_at", o.CreatedAt) r.setTimestamp("created_at", o.CreatedAt)
r.setTimestamp("updated_at", o.UpdatedAt) r.setTimestamp("updated_at", o.UpdatedAt)
r.setNullInt("rating", o.Rating) r.setNullInt("rating", o.Rating)
r.setBool("favorite", o.Favorite)
r.setNullString("details", o.Details) r.setNullString("details", o.Details)
r.setBool("ignore_auto_tag", o.IgnoreAutoTag) r.setBool("ignore_auto_tag", o.IgnoreAutoTag)
} }
@ -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.Details, studioTable+".details"))
query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url")) query.handleCriterion(ctx, stringCriterionHandler(studioFilter.URL, studioTable+".url"))
query.handleCriterion(ctx, intCriterionHandler(studioFilter.Rating100, studioTable+".rating", nil)) 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, boolCriterionHandler(studioFilter.IgnoreAutoTag, studioTable+".ignore_auto_tag", nil))
query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) { query.handleCriterion(ctx, criterionHandlerFunc(func(ctx context.Context, f *filterBuilder) {

View file

@ -24,6 +24,7 @@ func ToJSON(ctx context.Context, reader FinderImageStashIDGetter, studio *models
Name: studio.Name, Name: studio.Name,
URL: studio.URL, URL: studio.URL,
Details: studio.Details, Details: studio.Details,
Favorite: studio.Favorite,
IgnoreAutoTag: studio.IgnoreAutoTag, IgnoreAutoTag: studio.IgnoreAutoTag,
CreatedAt: json.JSONTime{Time: studio.CreatedAt}, CreatedAt: json.JSONTime{Time: studio.CreatedAt},
UpdatedAt: json.JSONTime{Time: studio.UpdatedAt}, UpdatedAt: json.JSONTime{Time: studio.UpdatedAt},

View file

@ -62,6 +62,7 @@ func createFullStudio(id int, parentID int) models.Studio {
Name: studioName, Name: studioName,
URL: url, URL: url,
Details: details, Details: details,
Favorite: true,
CreatedAt: createTime, CreatedAt: createTime,
UpdatedAt: updateTime, UpdatedAt: updateTime,
Rating: &rating, Rating: &rating,
@ -92,6 +93,7 @@ func createFullJSONStudio(parentStudio, image string, aliases []string) *jsonsch
Name: studioName, Name: studioName,
URL: url, URL: url,
Details: details, Details: details,
Favorite: true,
CreatedAt: json.JSONTime{ CreatedAt: json.JSONTime{
Time: createTime, Time: createTime,
}, },

View file

@ -144,6 +144,7 @@ func studioJSONtoStudio(studioJSON jsonschema.Studio) models.Studio {
URL: studioJSON.URL, URL: studioJSON.URL,
Aliases: models.NewRelatedStrings(studioJSON.Aliases), Aliases: models.NewRelatedStrings(studioJSON.Aliases),
Details: studioJSON.Details, Details: studioJSON.Details,
Favorite: studioJSON.Favorite,
IgnoreAutoTag: studioJSON.IgnoreAutoTag, IgnoreAutoTag: studioJSON.IgnoreAutoTag,
CreatedAt: studioJSON.CreatedAt.GetTime(), CreatedAt: studioJSON.CreatedAt.GetTime(),
UpdatedAt: studioJSON.UpdatedAt.GetTime(), UpdatedAt: studioJSON.UpdatedAt.GetTime(),

View file

@ -31,6 +31,7 @@ fragment StudioData on Studio {
} }
details details
rating100 rating100
favorite
aliases aliases
} }

View file

@ -17,12 +17,12 @@ import {
} from "src/models/list-filter/criteria/criterion"; } from "src/models/list-filter/criteria/criterion";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
import GenderIcon from "./GenderIcon"; 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 { RatingBanner } from "../Shared/RatingBanner";
import cx from "classnames";
import { usePerformerUpdate } from "src/core/StashService"; import { usePerformerUpdate } from "src/core/StashService";
import { ILabeledId } from "src/models/list-filter/types"; import { ILabeledId } from "src/models/list-filter/types";
import ScreenUtils from "src/utils/screen"; import ScreenUtils from "src/utils/screen";
import { FavoriteIcon } from "../Shared/FavoriteIcon";
export interface IPerformerCardExtraCriteria { export interface IPerformerCardExtraCriteria {
scenes?: Criterion<CriterionValue>[]; scenes?: Criterion<CriterionValue>[];
@ -82,24 +82,6 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
setCardWidth(fittedCardWidth); setCardWidth(fittedCardWidth);
}, [containerWidth]); }, [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) { function onToggleFavorite(v: boolean) {
if (performer.id) { if (performer.id) {
updatePerformer({ updatePerformer({
@ -292,7 +274,10 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
} }
overlays={ overlays={
<> <>
{renderFavoriteIcon()} <FavoriteIcon
favorite={performer.favorite}
onToggleFavorite={onToggleFavorite}
/>
{maybeRenderRatingBanner()} {maybeRenderRatingBanner()}
{maybeRenderFlag()} {maybeRenderFlag()}
</> </>

View file

@ -80,36 +80,15 @@
} }
button.btn.favorite-button { button.btn.favorite-button {
opacity: 1;
padding: 0; padding: 0;
position: absolute; position: absolute;
right: 5px; right: 5px;
top: 10px; top: 10px;
transition: opacity 0.5s;
svg.fa-icon { svg.fa-icon {
margin-left: 0.4rem; margin-left: 0.4rem;
margin-right: 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 { &:hover button.btn.favorite-button.not-favorite {

View file

@ -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>
);
};

View file

@ -528,3 +528,27 @@ div.react-datepicker {
align-items: baseline; align-items: baseline;
display: flex; 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;
}
}

View file

@ -11,6 +11,8 @@ import { FormattedMessage } from "react-intl";
import { PopoverCountButton } from "../Shared/PopoverCountButton"; import { PopoverCountButton } from "../Shared/PopoverCountButton";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import ScreenUtils from "src/utils/screen"; import ScreenUtils from "src/utils/screen";
import { FavoriteIcon } from "../Shared/FavoriteIcon";
import { useStudioUpdate } from "src/core/StashService";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@ -70,6 +72,7 @@ export const StudioCard: React.FC<IProps> = ({
selected, selected,
onSelectedChanged, onSelectedChanged,
}) => { }) => {
const [updateStudio] = useStudioUpdate();
const [cardWidth, setCardWidth] = useState<number>(); const [cardWidth, setCardWidth] = useState<number>();
useEffect(() => { useEffect(() => {
@ -83,6 +86,19 @@ export const StudioCard: React.FC<IProps> = ({
setCardWidth(fittedCardWidth); setCardWidth(fittedCardWidth);
}, [containerWidth]); }, [containerWidth]);
function onToggleFavorite(v: boolean) {
if (studio.id) {
updateStudio({
variables: {
input: {
id: studio.id,
favorite: v,
},
},
});
}
}
function maybeRenderScenesPopoverButton() { function maybeRenderScenesPopoverButton() {
if (!studio.scene_count) return; if (!studio.scene_count) return;
@ -193,6 +209,12 @@ export const StudioCard: React.FC<IProps> = ({
<RatingBanner rating={studio.rating100} /> <RatingBanner rating={studio.rating100} />
</div> </div>
} }
overlays={
<FavoriteIcon
favorite={studio.favorite}
onToggleFavorite={(v) => onToggleFavorite(v)}
/>
}
popovers={maybeRenderPopoverButtonGroup()} popovers={maybeRenderPopoverButtonGroup()}
selected={selected} selected={selected}
selecting={selecting} selecting={selecting}

View file

@ -37,6 +37,7 @@ import {
faLink, faLink,
faChevronDown, faChevronDown,
faChevronUp, faChevronUp,
faHeart,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
@ -154,6 +155,19 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
} }
}, [setTabKey, populatedDefaultTab, tabKey]); }, [setTabKey, populatedDefaultTab, tabKey]);
function setFavorite(v: boolean) {
if (studio.id) {
updateStudio({
variables: {
input: {
id: studio.id,
favorite: v,
},
},
});
}
}
// set up hotkeys // set up hotkeys
useEffect(() => { useEffect(() => {
Mousetrap.bind("e", () => toggleEditing()); Mousetrap.bind("e", () => toggleEditing());
@ -161,6 +175,7 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
setIsDeleteAlertOpen(true); setIsDeleteAlertOpen(true);
}); });
Mousetrap.bind(",", () => setCollapsed(!collapsed)); Mousetrap.bind(",", () => setCollapsed(!collapsed));
Mousetrap.bind("f", () => setFavorite(!studio.favorite));
return () => { return () => {
Mousetrap.unbind("e"); Mousetrap.unbind("e");
@ -284,6 +299,12 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
const renderClickableIcons = () => ( const renderClickableIcons = () => (
<span className="name-icons"> <span className="name-icons">
<Button
className={cx("minimal", studio.favorite ? "favorite" : "not-favorite")}
onClick={() => setFavorite(!studio.favorite)}
>
<Icon icon={faHeart} />
</Button>
{studio.url && ( {studio.url && (
<Button <Button
as={ExternalLink} as={ExternalLink}

View file

@ -5,3 +5,35 @@
max-width: 100%; max-width: 100%;
} }
} }
.studio-card {
button.btn.favorite-button {
padding: 0;
position: absolute;
right: 5px;
top: 10px;
svg.fa-icon {
margin-left: 0.4rem;
margin-right: 0.4rem;
}
}
&:hover button.btn.favorite-button.not-favorite {
opacity: 1;
}
}
#studio-page {
.studio-head {
.name-icons {
.not-favorite {
color: rgba(191, 204, 214, 0.5);
}
.favorite {
color: #ff7373;
}
}
}
}

View file

@ -1,14 +1,26 @@
import { BooleanCriterion, BooleanCriterionOption } from "./criterion"; import { BooleanCriterion, BooleanCriterionOption } from "./criterion";
export const FavoriteCriterionOption = new BooleanCriterionOption( export const FavoritePerformerCriterionOption = new BooleanCriterionOption(
"favourite", "favourite",
"filter_favorites", "filter_favorites",
() => new FavoriteCriterion() () => new FavoritePerformerCriterion()
); );
export class FavoriteCriterion extends BooleanCriterion { export class FavoritePerformerCriterion extends BooleanCriterion {
constructor() { constructor() {
super(FavoriteCriterionOption); super(FavoritePerformerCriterionOption);
}
}
export const FavoriteStudioCriterionOption = new BooleanCriterionOption(
"favourite",
"favorite",
() => new FavoriteStudioCriterion()
);
export class FavoriteStudioCriterion extends BooleanCriterion {
constructor() {
super(FavoriteStudioCriterionOption);
} }
} }

View file

@ -6,7 +6,7 @@ import {
createDateCriterionOption, createDateCriterionOption,
createMandatoryTimestampCriterionOption, createMandatoryTimestampCriterionOption,
} from "./criteria/criterion"; } from "./criteria/criterion";
import { FavoriteCriterionOption } from "./criteria/favorite"; import { FavoritePerformerCriterionOption } from "./criteria/favorite";
import { GenderCriterionOption } from "./criteria/gender"; import { GenderCriterionOption } from "./criteria/gender";
import { CircumcisedCriterionOption } from "./criteria/circumcised"; import { CircumcisedCriterionOption } from "./criteria/circumcised";
import { PerformerIsMissingCriterionOption } from "./criteria/is-missing"; import { PerformerIsMissingCriterionOption } from "./criteria/is-missing";
@ -81,7 +81,7 @@ const stringCriteria: CriterionType[] = [
]; ];
const criterionOptions = [ const criterionOptions = [
FavoriteCriterionOption, FavoritePerformerCriterionOption,
GenderCriterionOption, GenderCriterionOption,
CircumcisedCriterionOption, CircumcisedCriterionOption,
PerformerIsMissingCriterionOption, PerformerIsMissingCriterionOption,

View file

@ -5,6 +5,7 @@ import {
createStringCriterionOption, createStringCriterionOption,
createMandatoryTimestampCriterionOption, createMandatoryTimestampCriterionOption,
} from "./criteria/criterion"; } from "./criteria/criterion";
import { FavoriteStudioCriterionOption } from "./criteria/favorite";
import { StudioIsMissingCriterionOption } from "./criteria/is-missing"; import { StudioIsMissingCriterionOption } from "./criteria/is-missing";
import { RatingCriterionOption } from "./criteria/rating"; import { RatingCriterionOption } from "./criteria/rating";
import { StashIDCriterionOption } from "./criteria/stash-ids"; import { StashIDCriterionOption } from "./criteria/stash-ids";
@ -36,6 +37,7 @@ const sortByOptions = ["name", "random", "rating"]
const displayModeOptions = [DisplayMode.Grid, DisplayMode.Tagger]; const displayModeOptions = [DisplayMode.Grid, DisplayMode.Tagger];
const criterionOptions = [ const criterionOptions = [
FavoriteStudioCriterionOption,
createMandatoryStringCriterionOption("name"), createMandatoryStringCriterionOption("name"),
createStringCriterionOption("details"), createStringCriterionOption("details"),
ParentStudiosCriterionOption, ParentStudiosCriterionOption,

View file

@ -135,6 +135,7 @@ export type CriterionType =
| "audio_codec" | "audio_codec"
| "duration" | "duration"
| "filter_favorites" | "filter_favorites"
| "favorite"
| "has_markers" | "has_markers"
| "is_missing" | "is_missing"
| "tags" | "tags"