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
|
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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
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 { 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(
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 { 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" })}
|
||||||
|
|||||||
@@ -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 { 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(
|
||||||
|
|||||||
Reference in New Issue
Block a user