mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
3
ui/v2.5/src/components/Changelog/versions/v0100.md
Normal file
3
ui/v2.5/src/components/Changelog/versions/v0100.md
Normal 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))
|
||||
@@ -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<IMovieList> = ({ 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(
|
||||
|
||||
@@ -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 = () => {
|
||||
<Tab eventKey="images" title={intl.formatMessage({ id: "images" })}>
|
||||
<PerformerImagesPanel performer={performer} />
|
||||
</Tab>
|
||||
<Tab eventKey="movies" title={intl.formatMessage({ id: "movies" })}>
|
||||
<PerformerMoviesPanel performer={performer} />
|
||||
</Tab>
|
||||
<Tab eventKey="edit" title={intl.formatMessage({ id: "actions.edit" })}>
|
||||
<PerformerEditPanel
|
||||
performer={performer}
|
||||
|
||||
@@ -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)} />;
|
||||
};
|
||||
@@ -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 = () => {
|
||||
>
|
||||
<StudioPerformersPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab eventKey="movies" title={intl.formatMessage({ id: "movies" })}>
|
||||
<StudioMoviesPanel studio={studio} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="childstudios"
|
||||
title={intl.formatMessage({ id: "child_studios" })}
|
||||
|
||||
@@ -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)} />;
|
||||
};
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user