Show O Counter in Studio card (#5982)

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
Slick Daddy 2025-11-25 02:06:36 +03:00 committed by GitHub
parent ca8ee6bc2a
commit ecd9c6ec5b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 138 additions and 6 deletions

View file

@ -25,6 +25,7 @@ type Studio {
updated_at: Time! updated_at: Time!
groups: [Group!]! groups: [Group!]!
movies: [Movie!]! @deprecated(reason: "use groups instead") movies: [Movie!]! @deprecated(reason: "use groups instead")
o_counter: Int
} }
input StudioCreateInput { input StudioCreateInput {

View file

@ -143,6 +143,24 @@ func (r *studioResolver) MovieCount(ctx context.Context, obj *models.Studio, dep
return r.GroupCount(ctx, obj, depth) return r.GroupCount(ctx, obj, depth)
} }
func (r *studioResolver) OCounter(ctx context.Context, obj *models.Studio) (ret *int, err error) {
var res_scene int
var res_image int
var res int
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
res_scene, err = r.repository.Scene.OCountByStudioID(ctx, obj.ID)
if err != nil {
return err
}
res_image, err = r.repository.Image.OCountByStudioID(ctx, obj.ID)
return err
}); err != nil {
return nil, err
}
res = res_scene + res_image
return &res, nil
}
func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) { func (r *studioResolver) ParentStudio(ctx context.Context, obj *models.Studio) (ret *models.Studio, err error) {
if obj.ParentID == nil { if obj.ParentID == nil {
return nil, nil return nil, nil

View file

@ -594,6 +594,27 @@ func (_m *ImageReaderWriter) OCountByPerformerID(ctx context.Context, performerI
return r0, r1 return r0, r1
} }
// OCountByStudioID provides a mock function with given fields: ctx, studioID
func (_m *ImageReaderWriter) OCountByStudioID(ctx context.Context, studioID int) (int, error) {
ret := _m.Called(ctx, studioID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, studioID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, studioID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Query provides a mock function with given fields: ctx, options // Query provides a mock function with given fields: ctx, options
func (_m *ImageReaderWriter) Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error) { func (_m *ImageReaderWriter) Query(ctx context.Context, options models.ImageQueryOptions) (*models.ImageQueryResult, error) {
ret := _m.Called(ctx, options) ret := _m.Called(ctx, options)

View file

@ -1183,6 +1183,27 @@ func (_m *SceneReaderWriter) OCountByPerformerID(ctx context.Context, performerI
return r0, r1 return r0, r1
} }
// OCountByStudioID provides a mock function with given fields: ctx, studioID
func (_m *SceneReaderWriter) OCountByStudioID(ctx context.Context, studioID int) (int, error) {
ret := _m.Called(ctx, studioID)
var r0 int
if rf, ok := ret.Get(0).(func(context.Context, int) int); ok {
r0 = rf(ctx, studioID)
} else {
r0 = ret.Get(0).(int)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, studioID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// PlayDuration provides a mock function with given fields: ctx // PlayDuration provides a mock function with given fields: ctx
func (_m *SceneReaderWriter) PlayDuration(ctx context.Context) (float64, error) { func (_m *SceneReaderWriter) PlayDuration(ctx context.Context) (float64, error) {
ret := _m.Called(ctx) ret := _m.Called(ctx)

View file

@ -38,6 +38,7 @@ type ImageCounter interface {
CountByGalleryID(ctx context.Context, galleryID int) (int, error) CountByGalleryID(ctx context.Context, galleryID int) (int, error)
OCount(ctx context.Context) (int, error) OCount(ctx context.Context) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error)
OCountByStudioID(ctx context.Context, studioID int) (int, error)
} }
// ImageCreator provides methods to create images. // ImageCreator provides methods to create images.

View file

@ -45,6 +45,7 @@ type SceneCounter interface {
CountMissingOSHash(ctx context.Context) (int, error) CountMissingOSHash(ctx context.Context) (int, error)
OCountByPerformerID(ctx context.Context, performerID int) (int, error) OCountByPerformerID(ctx context.Context, performerID int) (int, error)
OCountByGroupID(ctx context.Context, groupID int) (int, error) OCountByGroupID(ctx context.Context, groupID int) (int, error)
OCountByStudioID(ctx context.Context, studioID int) (int, error)
} }
// SceneCreator provides methods to create scenes. // SceneCreator provides methods to create scenes.

View file

@ -682,6 +682,20 @@ func (qb *ImageStore) OCountByPerformerID(ctx context.Context, performerID int)
return ret, nil return ret, nil
} }
func (qb *ImageStore) OCountByStudioID(ctx context.Context, studioID int) (int, error) {
table := qb.table()
q := dialect.Select(goqu.COALESCE(goqu.SUM("o_counter"), 0)).From(table).Where(
table.Col(studioIDColumn).Eq(studioID),
)
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
return ret, nil
}
func (qb *ImageStore) OCount(ctx context.Context) (int, error) { func (qb *ImageStore) OCount(ctx context.Context) (int, error) {
table := qb.table() table := qb.table()

View file

@ -818,6 +818,23 @@ func (qb *SceneStore) OCountByGroupID(ctx context.Context, groupID int) (int, er
return ret, nil return ret, nil
} }
func (qb *SceneStore) OCountByStudioID(ctx context.Context, studioID int) (int, error) {
table := qb.table()
oHistoryTable := goqu.T(scenesODatesTable)
q := dialect.Select(goqu.COUNT("*")).From(table).InnerJoin(
oHistoryTable,
goqu.On(table.Col(idColumn).Eq(oHistoryTable.Col(sceneIDColumn))),
).Where(table.Col(studioIDColumn).Eq(studioID))
var ret int
if err := querySimple(ctx, q, &ret); err != nil {
return 0, err
}
return ret, nil
}
func (qb *SceneStore) FindByGroupID(ctx context.Context, groupID int) ([]*models.Scene, error) { func (qb *SceneStore) FindByGroupID(ctx context.Context, groupID int) ([]*models.Scene, error) {
sq := dialect.From(scenesGroupsJoinTable).Select(scenesGroupsJoinTable.Col(sceneIDColumn)).Where( sq := dialect.From(scenesGroupsJoinTable).Select(scenesGroupsJoinTable.Col(sceneIDColumn)).Where(
scenesGroupsJoinTable.Col(groupIDColumn).Eq(groupID), scenesGroupsJoinTable.Col(groupIDColumn).Eq(groupID),

View file

@ -17,4 +17,5 @@ fragment SlimStudioData on Studio {
id id
name name
} }
o_counter
} }

View file

@ -39,6 +39,7 @@ fragment StudioData on Studio {
tags { tags {
...SlimTagData ...SlimTagData
} }
o_counter
} }
fragment SelectStudioData on Studio { fragment SelectStudioData on Studio {

View file

@ -13,6 +13,7 @@ import { RatingBanner } from "../Shared/RatingBanner";
import { FavoriteIcon } from "../Shared/FavoriteIcon"; import { FavoriteIcon } from "../Shared/FavoriteIcon";
import { useStudioUpdate } from "src/core/StashService"; import { useStudioUpdate } from "src/core/StashService";
import { faTag } from "@fortawesome/free-solid-svg-icons"; import { faTag } from "@fortawesome/free-solid-svg-icons";
import { OCounterButton } from "../Shared/CountButton";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@ -175,6 +176,12 @@ export const StudioCard: React.FC<IProps> = ({
); );
} }
function maybeRenderOCounter() {
if (!studio.o_counter) return;
return <OCounterButton value={studio.o_counter} />;
}
function maybeRenderPopoverButtonGroup() { function maybeRenderPopoverButtonGroup() {
if ( if (
studio.scene_count || studio.scene_count ||
@ -182,6 +189,7 @@ export const StudioCard: React.FC<IProps> = ({
studio.gallery_count || studio.gallery_count ||
studio.group_count || studio.group_count ||
studio.performer_count || studio.performer_count ||
studio.o_counter ||
studio.tags.length > 0 studio.tags.length > 0
) { ) {
return ( return (
@ -194,6 +202,7 @@ export const StudioCard: React.FC<IProps> = ({
{maybeRenderGalleriesPopoverButton()} {maybeRenderGalleriesPopoverButton()}
{maybeRenderPerformersPopoverButton()} {maybeRenderPerformersPopoverButton()}
{maybeRenderTagPopoverButton()} {maybeRenderTagPopoverButton()}
{maybeRenderOCounter()}
</ButtonGroup> </ButtonGroup>
</> </>
); );

View file

@ -48,6 +48,7 @@ import { ExternalLinkButtons } from "src/components/Shared/ExternalLinksButton";
import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { AliasList } from "src/components/Shared/DetailsPage/AliasList";
import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage";
import { goBackOrReplace } from "src/utils/history"; import { goBackOrReplace } from "src/utils/history";
import { OCounterButton } from "src/components/Shared/CountButton";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@ -471,12 +472,17 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
</DetailTitle> </DetailTitle>
<AliasList aliases={studio.aliases} /> <AliasList aliases={studio.aliases} />
<RatingSystem <div className="quality-group">
value={studio.rating100} <RatingSystem
onSetRating={(value) => setRating(value)} value={studio.rating100}
clickToRate onSetRating={(value) => setRating(value)}
withoutContext clickToRate
/> withoutContext
/>
{!!studio.o_counter && (
<OCounterButton value={studio.o_counter} />
)}
</div>
{!isEditing && ( {!isEditing && (
<StudioDetailsPanel <StudioDetailsPanel
studio={studio} studio={studio}

View file

@ -41,6 +41,27 @@
width: auto; width: auto;
} }
.quality-group {
display: inline-flex;
margin-top: 0.25rem;
}
// The following min-width declarations prevent
// the O-Count from moving around
// when hovering over rating stars
.rating-stars-precision-full .star-rating-number {
min-width: 0.75rem;
}
.rating-stars-precision-half .star-rating-number,
.rating-stars-precision-tenth .star-rating-number {
min-width: 1.45rem;
}
.rating-stars-precision-quarter .star-rating-number {
min-width: 2rem;
}
// the detail element ids are the same as field type name // the detail element ids are the same as field type name
// which don't follow the correct convention // which don't follow the correct convention
/* stylelint-disable selector-class-pattern */ /* stylelint-disable selector-class-pattern */