From 7a468413da4a5e270e1f5d9e150a1d453078965e Mon Sep 17 00:00:00 2001 From: gitgiggety <79809426+gitgiggety@users.noreply.github.com> Date: Tue, 7 Sep 2021 04:16:33 +0200 Subject: [PATCH] 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 --- graphql/schema/types/filters.graphql | 2 ++ pkg/sqlite/movies.go | 30 +++++++++++++++++++ .../src/components/Changelog/Changelog.tsx | 10 +++++-- .../components/Changelog/versions/v0100.md | 3 ++ ui/v2.5/src/components/Movies/MovieList.tsx | 7 ++++- .../Performers/PerformerDetails/Performer.tsx | 6 ++++ .../PerformerDetails/PerformerMoviesPanel.tsx | 14 +++++++++ .../Studios/StudioDetails/Studio.tsx | 7 ++++- .../StudioDetails/StudioMoviesPanel.tsx | 12 ++++++++ ui/v2.5/src/models/list-filter/movies.ts | 2 ++ 10 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 ui/v2.5/src/components/Changelog/versions/v0100.md create mode 100644 ui/v2.5/src/components/Performers/PerformerDetails/PerformerMoviesPanel.tsx create mode 100644 ui/v2.5/src/components/Studios/StudioDetails/StudioMoviesPanel.tsx diff --git a/graphql/schema/types/filters.graphql b/graphql/schema/types/filters.graphql index 989b95863..76640bb24 100644 --- a/graphql/schema/types/filters.graphql +++ b/graphql/schema/types/filters.graphql @@ -176,6 +176,8 @@ input MovieFilterType { is_missing: String """Filter by url""" url: StringCriterionInput + """Filter to only include movies where performer appears in a scene""" + performers: MultiCriterionInput } input StudioFilterType { diff --git a/pkg/sqlite/movies.go b/pkg/sqlite/movies.go index 49d970459..9e69b8451 100644 --- a/pkg/sqlite/movies.go +++ b/pkg/sqlite/movies.go @@ -126,6 +126,7 @@ func (qb *movieQueryBuilder) makeFilter(movieFilter *models.MovieFilterType) *fi query.handleCriterion(movieIsMissingCriterionHandler(qb, movieFilter.IsMissing)) query.handleCriterion(stringCriterionHandler(movieFilter.URL, "movies.url")) query.handleCriterion(movieStudioCriterionHandler(qb, movieFilter.Studios)) + query.handleCriterion(moviePerformersCriterionHandler(qb, movieFilter.Performers)) return query } @@ -204,6 +205,35 @@ func movieStudioCriterionHandler(qb *movieQueryBuilder, studios *models.Hierarch 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 { var sort string var direction string diff --git a/ui/v2.5/src/components/Changelog/Changelog.tsx b/ui/v2.5/src/components/Changelog/Changelog.tsx index 87b8ff1db..f504f81de 100644 --- a/ui/v2.5/src/components/Changelog/Changelog.tsx +++ b/ui/v2.5/src/components/Changelog/Changelog.tsx @@ -12,6 +12,7 @@ import V060 from "./versions/v060.md"; import V070 from "./versions/v070.md"; import V080 from "./versions/v080.md"; import V090 from "./versions/v090.md"; +import V0100 from "./versions/v0100.md"; import { MarkdownPage } from "../Shared/MarkdownPage"; // to avoid use of explicit any @@ -50,9 +51,9 @@ const Changelog: React.FC = () => { // after new release: // add entry to releases, using the current* fields // then update the current fields. - const currentVersion = stashVersion || "v0.9.0"; + const currentVersion = stashVersion || "v0.10.0"; const currentDate = buildDate; - const currentPage = V090; + const currentPage = V0100; const releases: IStashRelease[] = [ { @@ -61,6 +62,11 @@ const Changelog: React.FC = () => { page: currentPage, defaultOpen: true, }, + { + version: "v0.9.0", + date: "2021-09-06", + page: V090, + }, { version: "v0.8.0", date: "2021-07-02", diff --git a/ui/v2.5/src/components/Changelog/versions/v0100.md b/ui/v2.5/src/components/Changelog/versions/v0100.md new file mode 100644 index 000000000..3a7b1e1d1 --- /dev/null +++ b/ui/v2.5/src/components/Changelog/versions/v0100.md @@ -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)) diff --git a/ui/v2.5/src/components/Movies/MovieList.tsx b/ui/v2.5/src/components/Movies/MovieList.tsx index 4329e9af3..ac5b3a5bc 100644 --- a/ui/v2.5/src/components/Movies/MovieList.tsx +++ b/ui/v2.5/src/components/Movies/MovieList.tsx @@ -18,7 +18,11 @@ import { import { ExportDialog, DeleteEntityDialog } from "src/components/Shared"; import { MovieCard } from "./MovieCard"; -export const MovieList: React.FC = () => { +interface IMovieList { + filterHook?: (filter: ListFilterModel) => ListFilterModel; +} + +export const MovieList: React.FC = ({ filterHook }) => { const intl = useIntl(); const history = useHistory(); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); @@ -73,6 +77,7 @@ export const MovieList: React.FC = () => { selectable: true, persistState: PersistanceLevel.ALL, renderDeleteDialog, + filterHook, }); async function viewRandom( diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 5297e74f9..92f602679 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -22,6 +22,7 @@ import { PerformerDetailsPanel } from "./PerformerDetailsPanel"; import { PerformerOperationsPanel } from "./PerformerOperationsPanel"; import { PerformerScenesPanel } from "./PerformerScenesPanel"; import { PerformerGalleriesPanel } from "./PerformerGalleriesPanel"; +import { PerformerMoviesPanel } from "./PerformerMoviesPanel"; import { PerformerImagesPanel } from "./PerformerImagesPanel"; import { PerformerEditPanel } from "./PerformerEditPanel"; @@ -70,6 +71,7 @@ export const Performer: React.FC = () => { tab === "scenes" || tab === "galleries" || tab === "images" || + tab === "movies" || tab === "edit" || tab === "operations" ? tab @@ -91,6 +93,7 @@ export const Performer: React.FC = () => { Mousetrap.bind("e", () => setActiveTabKey("edit")); Mousetrap.bind("c", () => setActiveTabKey("scenes")); Mousetrap.bind("g", () => setActiveTabKey("galleries")); + Mousetrap.bind("m", () => setActiveTabKey("movies")); Mousetrap.bind("o", () => setActiveTabKey("operations")); Mousetrap.bind("f", () => setFavorite(!performer.favorite)); @@ -140,6 +143,9 @@ export const Performer: React.FC = () => { + + + ; +} + +export const PerformerMoviesPanel: React.FC = ({ + performer, +}) => { + return ; +}; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index aaeb8ff4b..52323bc87 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -28,6 +28,7 @@ import { StudioChildrenPanel } from "./StudioChildrenPanel"; import { StudioPerformersPanel } from "./StudioPerformersPanel"; import { StudioEditPanel } from "./StudioEditPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel"; +import { StudioMoviesPanel } from "./StudioMoviesPanel"; interface IStudioParams { id?: string; @@ -186,7 +187,8 @@ export const Studio: React.FC = () => { tab === "childstudios" || tab === "images" || tab === "galleries" || - tab === "performers" + tab === "performers" || + tab === "movies" ? tab : "scenes"; const setActiveTabKey = (newTab: string | null) => { @@ -276,6 +278,9 @@ export const Studio: React.FC = () => { > + + + ; +} + +export const StudioMoviesPanel: React.FC = ({ studio }) => { + return ; +}; diff --git a/ui/v2.5/src/models/list-filter/movies.ts b/ui/v2.5/src/models/list-filter/movies.ts index 65b7af112..dcf495f76 100644 --- a/ui/v2.5/src/models/list-filter/movies.ts +++ b/ui/v2.5/src/models/list-filter/movies.ts @@ -5,6 +5,7 @@ import { import { MovieIsMissingCriterionOption } from "./criteria/is-missing"; import { RatingCriterionOption } from "./criteria/rating"; import { StudiosCriterionOption } from "./criteria/studios"; +import { PerformersCriterionOption } from "./criteria/performers"; import { ListFilterOptions } from "./filter-options"; import { DisplayMode } from "./types"; @@ -28,6 +29,7 @@ const criterionOptions = [ createStringCriterionOption("synopsis"), createMandatoryNumberCriterionOption("duration"), RatingCriterionOption, + PerformersCriterionOption, ]; export const MovieListFilterOptions = new ListFilterOptions(