Add movies tab to Studios and Performers page (#1675)

* Add movies tab to Studios page
* Add performers filter to movies
* Add movies tab to performers page
This commit is contained in:
gitgiggety
2021-09-07 04:16:33 +02:00
committed by GitHub
parent 4625e1f955
commit 7a468413da
10 changed files with 89 additions and 4 deletions

View File

@@ -176,6 +176,8 @@ input MovieFilterType {
is_missing: String is_missing: String
"""Filter by url""" """Filter by url"""
url: StringCriterionInput url: StringCriterionInput
"""Filter to only include movies where performer appears in a scene"""
performers: MultiCriterionInput
} }
input StudioFilterType { input StudioFilterType {

View File

@@ -126,6 +126,7 @@ func (qb *movieQueryBuilder) makeFilter(movieFilter *models.MovieFilterType) *fi
query.handleCriterion(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) query.handleCriterion(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing))
query.handleCriterion(stringCriterionHandler(movieFilter.URL, "movies.url")) query.handleCriterion(stringCriterionHandler(movieFilter.URL, "movies.url"))
query.handleCriterion(movieStudioCriterionHandler(qb, movieFilter.Studios)) query.handleCriterion(movieStudioCriterionHandler(qb, movieFilter.Studios))
query.handleCriterion(moviePerformersCriterionHandler(qb, movieFilter.Performers))
return query return query
} }
@@ -204,6 +205,35 @@ func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.Hierarch
return h.handler(studios) return h.handler(studios)
} }
func moviePerformersCriterionHandler(qb *movieQueryBuilder, performers *models.MultiCriterionInput) criterionHandlerFunc {
return func(f *filterBuilder) {
if performers != nil && len(performers.Value) > 0 {
var args []interface{}
for _, arg := range performers.Value {
args = append(args, arg)
}
// Hack, can't apply args to join, nor inner join on a left join, so use CTE instead
f.addWith(`movies_performers AS (
SELECT movies_scenes.movie_id, performers_scenes.performer_id
FROM movies_scenes
INNER JOIN performers_scenes ON movies_scenes.scene_id = performers_scenes.scene_id
WHERE performers_scenes.performer_id IN`+getInBinding(len(performers.Value))+`
)`, args...)
f.addJoin("movies_performers", "", "movies.id = movies_performers.movie_id")
if performers.Modifier == models.CriterionModifierIncludes {
f.addWhere("movies_performers.performer_id IS NOT NULL")
} else if performers.Modifier == models.CriterionModifierIncludesAll {
f.addWhere("movies_performers.performer_id IS NOT NULL")
f.addHaving("COUNT(DISTINCT movies_performers.performer_id) = ?", len(performers.Value))
} else if performers.Modifier == models.CriterionModifierExcludes {
f.addWhere("movies_performers.performer_id IS NULL")
}
}
}
}
func (qb *movieQueryBuilder) getMovieSort(findFilter *models.FindFilterType) string { func (qb *movieQueryBuilder) getMovieSort(findFilter *models.FindFilterType) string {
var sort string var sort string
var direction string var direction string

View File

@@ -12,6 +12,7 @@ import V060 from "./versions/v060.md";
import V070 from "./versions/v070.md"; import V070 from "./versions/v070.md";
import V080 from "./versions/v080.md"; import V080 from "./versions/v080.md";
import V090 from "./versions/v090.md"; import V090 from "./versions/v090.md";
import V0100 from "./versions/v0100.md";
import { MarkdownPage } from "../Shared/MarkdownPage"; import { MarkdownPage } from "../Shared/MarkdownPage";
// to avoid use of explicit any // to avoid use of explicit any
@@ -50,9 +51,9 @@ const Changelog: React.FC = () => {
// after new release: // after new release:
// add entry to releases, using the current* fields // add entry to releases, using the current* fields
// then update the current fields. // then update the current fields.
const currentVersion = stashVersion || "v0.9.0"; const currentVersion = stashVersion || "v0.10.0";
const currentDate = buildDate; const currentDate = buildDate;
const currentPage = V090; const currentPage = V0100;
const releases: IStashRelease[] = [ const releases: IStashRelease[] = [
{ {
@@ -61,6 +62,11 @@ const Changelog: React.FC = () => {
page: currentPage, page: currentPage,
defaultOpen: true, defaultOpen: true,
}, },
{
version: "v0.9.0",
date: "2021-09-06",
page: V090,
},
{ {
version: "v0.8.0", version: "v0.8.0",
date: "2021-07-02", date: "2021-07-02",

View File

@@ -0,0 +1,3 @@
### ✨ New Features
* Added Movies tab to Studio and Performer pages. ([#1675](https://github.com/stashapp/stash/pull/1675))
* Support filtering Movies by Performers. ([#1675](https://github.com/stashapp/stash/pull/1675))

View File

@@ -18,7 +18,11 @@ import {
import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; import { ExportDialog, DeleteEntityDialog } from "src/components/Shared";
import { MovieCard } from "./MovieCard"; import { MovieCard } from "./MovieCard";
export const MovieList: React.FC = () => { interface IMovieList {
filterHook?: (filter: ListFilterModel) => ListFilterModel;
}
export const MovieList: React.FC<IMovieList> = ({ filterHook }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false);
@@ -73,6 +77,7 @@ export const MovieList: React.FC = () => {
selectable: true, selectable: true,
persistState: PersistanceLevel.ALL, persistState: PersistanceLevel.ALL,
renderDeleteDialog, renderDeleteDialog,
filterHook,
}); });
async function viewRandom( async function viewRandom(

View File

@@ -22,6 +22,7 @@ import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
import { PerformerOperationsPanel } from "./PerformerOperationsPanel"; import { PerformerOperationsPanel } from "./PerformerOperationsPanel";
import { PerformerScenesPanel } from "./PerformerScenesPanel"; import { PerformerScenesPanel } from "./PerformerScenesPanel";
import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel"; import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel";
import { PerformerMoviesPanel } from "./PerformerMoviesPanel";
import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerImagesPanel } from "./PerformerImagesPanel";
import { PerformerEditPanel } from "./PerformerEditPanel"; import { PerformerEditPanel } from "./PerformerEditPanel";
@@ -70,6 +71,7 @@ export const Performer: React.FC = () => {
tab === "scenes" || tab === "scenes" ||
tab === "galleries" || tab === "galleries" ||
tab === "images" || tab === "images" ||
tab === "movies" ||
tab === "edit" || tab === "edit" ||
tab === "operations" tab === "operations"
? tab ? tab
@@ -91,6 +93,7 @@ export const Performer: React.FC = () => {
Mousetrap.bind("e", () => setActiveTabKey("edit")); Mousetrap.bind("e", () => setActiveTabKey("edit"));
Mousetrap.bind("c", () => setActiveTabKey("scenes")); Mousetrap.bind("c", () => setActiveTabKey("scenes"));
Mousetrap.bind("g", () => setActiveTabKey("galleries")); Mousetrap.bind("g", () => setActiveTabKey("galleries"));
Mousetrap.bind("m", () => setActiveTabKey("movies"));
Mousetrap.bind("o", () => setActiveTabKey("operations")); Mousetrap.bind("o", () => setActiveTabKey("operations"));
Mousetrap.bind("f", () => setFavorite(!performer.favorite)); Mousetrap.bind("f", () => setFavorite(!performer.favorite));
@@ -140,6 +143,9 @@ export const Performer: React.FC = () => {
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}> <Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
<PerformerImagesPanel performer={performer} /> <PerformerImagesPanel performer={performer} />
</Tab> </Tab>
<Tab eventKey="movies" title={intl.formatMessage({ id: "movies" })}>
<PerformerMoviesPanel performer={performer} />
</Tab>
<Tab eventKey="edit" title={intl.formatMessage({ id: "actions.edit" })}> <Tab eventKey="edit" title={intl.formatMessage({ id: "actions.edit" })}>
<PerformerEditPanel <PerformerEditPanel
performer={performer} performer={performer}

View File

@@ -0,0 +1,14 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieList } from "src/components/Movies/MovieList";
import { performerFilterHook } from "src/core/performers";
interface IPerformerDetailsProps {
performer: Partial<GQL.PerformerDataFragment>;
}
export const PerformerMoviesPanel: React.FC<IPerformerDetailsProps> = ({
performer,
}) => {
return <MovieList filterHook={performerFilterHook(performer)} />;
};

View File

@@ -28,6 +28,7 @@ import { StudioChildrenPanel } from "./StudioChildrenPanel";
import { StudioPerformersPanel } from "./StudioPerformersPanel"; import { StudioPerformersPanel } from "./StudioPerformersPanel";
import { StudioEditPanel } from "./StudioEditPanel"; import { StudioEditPanel } from "./StudioEditPanel";
import { StudioDetailsPanel } from "./StudioDetailsPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel";
import { StudioMoviesPanel } from "./StudioMoviesPanel";
interface IStudioParams { interface IStudioParams {
id?: string; id?: string;
@@ -186,7 +187,8 @@ export const Studio: React.FC = () => {
tab === "childstudios" || tab === "childstudios" ||
tab === "images" || tab === "images" ||
tab === "galleries" || tab === "galleries" ||
tab === "performers" tab === "performers" ||
tab === "movies"
? tab ? tab
: "scenes"; : "scenes";
const setActiveTabKey = (newTab: string | null) => { const setActiveTabKey = (newTab: string | null) => {
@@ -276,6 +278,9 @@ export const Studio: React.FC = () => {
> >
<StudioPerformersPanel studio={studio} /> <StudioPerformersPanel studio={studio} />
</Tab> </Tab>
<Tab eventKey="movies" title={intl.formatMessage({ id: "movies" })}>
<StudioMoviesPanel studio={studio} />
</Tab>
<Tab <Tab
eventKey="childstudios" eventKey="childstudios"
title={intl.formatMessage({ id: "child_studios" })} title={intl.formatMessage({ id: "child_studios" })}

View File

@@ -0,0 +1,12 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { MovieList } from "src/components/Movies/MovieList";
import { studioFilterHook } from "src/core/studios";
interface IStudioMoviesPanel {
studio: Partial<GQL.StudioDataFragment>;
}
export const StudioMoviesPanel: React.FC<IStudioMoviesPanel> = ({ studio }) => {
return <MovieList filterHook={studioFilterHook(studio)} />;
};

View File

@@ -5,6 +5,7 @@ import {
import { MovieIsMissingCriterionOption } from "./criteria/is-missing"; import { MovieIsMissingCriterionOption } from "./criteria/is-missing";
import { RatingCriterionOption } from "./criteria/rating"; import { RatingCriterionOption } from "./criteria/rating";
import { StudiosCriterionOption } from "./criteria/studios"; import { StudiosCriterionOption } from "./criteria/studios";
import { PerformersCriterionOption } from "./criteria/performers";
import { ListFilterOptions } from "./filter-options"; import { ListFilterOptions } from "./filter-options";
import { DisplayMode } from "./types"; import { DisplayMode } from "./types";
@@ -28,6 +29,7 @@ const criterionOptions = [
createStringCriterionOption("synopsis"), createStringCriterionOption("synopsis"),
createMandatoryNumberCriterionOption("duration"), createMandatoryNumberCriterionOption("duration"),
RatingCriterionOption, RatingCriterionOption,
PerformersCriterionOption,
]; ];
export const MovieListFilterOptions = new ListFilterOptions( export const MovieListFilterOptions = new ListFilterOptions(