From ff495361d9cd8656f33e3050e617c9685c29b0a0 Mon Sep 17 00:00:00 2001
From: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
Date: Sat, 21 Mar 2020 08:21:49 +1100
Subject: [PATCH] Port Movies UI to v2.5 (#397)
* Ignore generated-graphql.tsx in 2.5
* Make movie name mandatory
* Port #395 fix to v2.5
* Differentiate front/back image browse buttons
* Move URL, Synopsis to separate rows
* Fix unknown query params crashing UI
---
.gitignore | 1 +
graphql/schema/types/movie.graphql | 2 +-
pkg/api/resolver_model_movie.go | 8 +-
ui/v2.5/src/App.tsx | 2 +
ui/v2.5/src/components/List/AddFilter.tsx | 3 +-
ui/v2.5/src/components/MainNavbar.tsx | 9 +-
ui/v2.5/src/components/Movies/MovieCard.tsx | 51 +
.../components/Movies/MovieDetails/Movie.tsx | 282 +
.../Movies/MovieDetails/MovieScenesPanel.tsx | 45 +
ui/v2.5/src/components/Movies/MovieList.tsx | 35 +
ui/v2.5/src/components/Movies/Movies.tsx | 13 +
ui/v2.5/src/components/Movies/styles.scss | 23 +
ui/v2.5/src/components/Scenes/SceneCard.tsx | 35 +
.../components/Scenes/SceneDetails/Scene.tsx | 8 +
.../Scenes/SceneDetails/SceneEditPanel.tsx | 104 +-
.../Scenes/SceneDetails/SceneMoviePanel.tsx | 25 +
.../Scenes/SceneDetails/SceneMovieTable.tsx | 76 +
.../src/components/Scenes/SceneListTable.tsx | 14 +
ui/v2.5/src/components/Scenes/styles.scss | 432 +-
.../components/Shared/DetailsEditNavbar.tsx | 26 +-
ui/v2.5/src/components/Shared/ImageInput.tsx | 4 +-
ui/v2.5/src/components/Shared/Select.tsx | 42 +-
ui/v2.5/src/components/Shared/TagLink.tsx | 7 +-
ui/v2.5/src/components/Stats.tsx | 8 +
.../Studios/StudioDetails/Studio.tsx | 2 +-
ui/v2.5/src/core/StashService.ts | 53 +
ui/v2.5/src/core/generated-graphql.tsx | 5192 -----------------
ui/v2.5/src/hooks/ListHook.tsx | 15 +-
ui/v2.5/src/index.scss | 916 +--
ui/v2.5/src/locale/de.json | 27 +-
ui/v2.5/src/locale/en.json | 27 +-
.../models/list-filter/criteria/criterion.ts | 3 +
.../models/list-filter/criteria/is-missing.ts | 1 +
.../src/models/list-filter/criteria/movies.ts | 32 +
.../src/models/list-filter/criteria/utils.ts | 3 +
ui/v2.5/src/models/list-filter/filter.ts | 27 +-
ui/v2.5/src/models/list-filter/types.ts | 5 +-
ui/v2.5/src/utils/navigation.ts | 15 +-
ui/v2.5/src/utils/table.tsx | 1 +
39 files changed, 1663 insertions(+), 5911 deletions(-)
create mode 100644 ui/v2.5/src/components/Movies/MovieCard.tsx
create mode 100644 ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx
create mode 100644 ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx
create mode 100644 ui/v2.5/src/components/Movies/MovieList.tsx
create mode 100644 ui/v2.5/src/components/Movies/Movies.tsx
create mode 100644 ui/v2.5/src/components/Movies/styles.scss
create mode 100644 ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx
create mode 100644 ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx
delete mode 100644 ui/v2.5/src/core/generated-graphql.tsx
create mode 100644 ui/v2.5/src/models/list-filter/criteria/movies.ts
diff --git a/.gitignore b/.gitignore
index a54db8fb0..4f92a344f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@
# GraphQL generated output
pkg/models/generated_*.go
ui/v2/src/core/generated-*.tsx
+ui/v2.5/src/core/generated-*.tsx
# packr generated files
*-packr.go
diff --git a/graphql/schema/types/movie.graphql b/graphql/schema/types/movie.graphql
index 6b8c47d79..28b828678 100644
--- a/graphql/schema/types/movie.graphql
+++ b/graphql/schema/types/movie.graphql
@@ -1,7 +1,7 @@
type Movie {
id: ID!
checksum: String!
- name: String
+ name: String!
aliases: String
duration: String
date: String
diff --git a/pkg/api/resolver_model_movie.go b/pkg/api/resolver_model_movie.go
index b30e113f6..3f1323cc8 100644
--- a/pkg/api/resolver_model_movie.go
+++ b/pkg/api/resolver_model_movie.go
@@ -8,11 +8,11 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
-func (r *movieResolver) Name(ctx context.Context, obj *models.Movie) (*string, error) {
+func (r *movieResolver) Name(ctx context.Context, obj *models.Movie) (string, error) {
if obj.Name.Valid {
- return &obj.Name.String, nil
+ return obj.Name.String, nil
}
- return nil, nil
+ return "", nil
}
func (r *movieResolver) URL(ctx context.Context, obj *models.Movie) (*string, error) {
@@ -81,4 +81,4 @@ func (r *movieResolver) SceneCount(ctx context.Context, obj *models.Movie) (*int
qb := models.NewSceneQueryBuilder()
res, err := qb.CountByMovieID(obj.ID)
return &res, err
-}
\ No newline at end of file
+}
diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx
index fb9b783b6..e8b1447b9 100755
--- a/ui/v2.5/src/App.tsx
+++ b/ui/v2.5/src/App.tsx
@@ -19,6 +19,7 @@ import { Stats } from "./components/Stats";
import Studios from "./components/Studios/Studios";
import { TagList } from "./components/Tags/TagList";
import { SceneFilenameParser } from "./components/SceneFilenameParser/SceneFilenameParser";
+import Movies from "./components/Movies/Movies";
// Set fontawesome/free-solid-svg as default fontawesome icons
library.add(fas);
@@ -43,6 +44,7 @@ export const App: React.FC = () => {
+
= (
if (
criterion.type !== "performers" &&
criterion.type !== "studios" &&
- criterion.type !== "tags"
+ criterion.type !== "tags" &&
+ criterion.type !== "movies"
)
return;
diff --git a/ui/v2.5/src/components/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx
index 556d88622..cef3716ab 100644
--- a/ui/v2.5/src/components/MainNavbar.tsx
+++ b/ui/v2.5/src/components/MainNavbar.tsx
@@ -19,6 +19,11 @@ const menuItems: IMenuItem[] = [
messageID: "scenes",
href: "/scenes"
},
+ {
+ href: "/movies",
+ icon: "film",
+ messageID: "movies"
+ },
{
href: "/scenes/markers",
icon: "map-marker-alt",
@@ -79,6 +84,8 @@ export const MainNavbar: React.FC = () => {
? "/performers/new"
: location.pathname === "/studios"
? "/studios/new"
+ : location.pathname === "/movies"
+ ? "/movies/new"
: null;
const newButton =
path === null ? (
@@ -98,7 +105,7 @@ export const MainNavbar: React.FC = () => {
variant="dark"
bg="dark"
className="top-nav"
- expand="md"
+ expand="lg"
expanded={expanded}
onToggle={setExpanded}
ref={navbarRef}
diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx
new file mode 100644
index 000000000..1cf6bc710
--- /dev/null
+++ b/ui/v2.5/src/components/Movies/MovieCard.tsx
@@ -0,0 +1,51 @@
+import { Card } from "react-bootstrap";
+import React, { FunctionComponent } from "react";
+import { Link } from "react-router-dom";
+import * as GQL from "src/core/generated-graphql";
+
+interface IProps {
+ movie: GQL.MovieDataFragment;
+ sceneIndex?: string;
+}
+
+export const MovieCard: FunctionComponent = (props: IProps) => {
+ function maybeRenderRatingBanner() {
+ if (!props.movie.rating) {
+ return;
+ }
+ return (
+
+ RATING: {props.movie.rating}
+
+ );
+ }
+
+ function maybeRenderSceneNumber() {
+ if (!props.sceneIndex) {
+ return {props.movie.scene_count} scenes.;
+ }
+
+ return Scene number: {props.sceneIndex};
+ }
+
+ return (
+
+
+
+ {maybeRenderRatingBanner()}
+
+
+
{props.movie.name}
+ {maybeRenderSceneNumber()}
+
+
+ );
+};
diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx
new file mode 100644
index 000000000..a684291ab
--- /dev/null
+++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx
@@ -0,0 +1,282 @@
+/* eslint-disable react/no-this-in-sfc */
+import React, { useEffect, useState, useCallback } from "react";
+import * as GQL from "src/core/generated-graphql";
+import { StashService } from "src/core/StashService";
+import { useParams, useHistory } from "react-router-dom";
+import cx from "classnames";
+import {
+ DetailsEditNavbar,
+ LoadingIndicator,
+ Modal
+} from "src/components/Shared";
+import { useToast } from "src/hooks";
+import { Table, Form } from "react-bootstrap";
+import { TableUtils, ImageUtils } from "src/utils";
+import { MovieScenesPanel } from "./MovieScenesPanel";
+
+export const Movie: React.FC = () => {
+ const history = useHistory();
+ const Toast = useToast();
+ const { id = "new" } = useParams();
+ const isNew = id === "new";
+
+ // Editing state
+ const [isEditing, setIsEditing] = useState(isNew);
+ const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false);
+
+ // Editing movie state
+ const [frontImage, setFrontImage] = useState(undefined);
+ const [backImage, setBackImage] = useState(undefined);
+ const [name, setName] = useState(undefined);
+ const [aliases, setAliases] = useState(undefined);
+ const [duration, setDuration] = useState(undefined);
+ const [date, setDate] = useState(undefined);
+ const [rating, setRating] = useState(undefined);
+ const [director, setDirector] = useState(undefined);
+ const [synopsis, setSynopsis] = useState(undefined);
+ const [url, setUrl] = useState(undefined);
+
+ // Movie state
+ const [movie, setMovie] = useState>({});
+ const [imagePreview, setImagePreview] = useState(
+ undefined
+ );
+ const [backimagePreview, setBackImagePreview] = useState(
+ undefined
+ );
+
+ // Network state
+ const { data, error, loading } = StashService.useFindMovie(id);
+ const [updateMovie] = StashService.useMovieUpdate(
+ getMovieInput() as GQL.MovieUpdateInput
+ );
+ const [createMovie] = StashService.useMovieCreate(
+ getMovieInput() as GQL.MovieCreateInput
+ );
+ const [deleteMovie] = StashService.useMovieDestroy(
+ getMovieInput() as GQL.MovieDestroyInput
+ );
+
+ function updateMovieEditState(state: Partial) {
+ setName(state.name ?? undefined);
+ setAliases(state.aliases ?? undefined);
+ setDuration(state.duration ?? undefined);
+ setDate(state.date ?? undefined);
+ setRating(state.rating ?? undefined);
+ setDirector(state.director ?? undefined);
+ setSynopsis(state.synopsis ?? undefined);
+ setUrl(state.url ?? undefined);
+ }
+
+ const updateMovieData = useCallback(
+ (movieData: Partial) => {
+ setFrontImage(undefined);
+ setBackImage(undefined);
+ updateMovieEditState(movieData);
+ setImagePreview(movieData.front_image_path ?? undefined);
+ setBackImagePreview(movieData.back_image_path ?? undefined);
+ setMovie(movieData);
+ },
+ []
+ );
+
+ useEffect(() => {
+ if (data && data.findMovie) {
+ updateMovieData(data.findMovie);
+ }
+ }, [data, updateMovieData]);
+
+ function onImageLoad(this: FileReader) {
+ setImagePreview(this.result as string);
+ setFrontImage(this.result as string);
+ }
+
+ function onBackImageLoad(this: FileReader) {
+ setBackImagePreview(this.result as string);
+ setBackImage(this.result as string);
+ }
+
+ ImageUtils.usePasteImage(onImageLoad);
+ ImageUtils.usePasteImage(onBackImageLoad);
+
+ if (!isNew && !isEditing) {
+ if (!data || !data.findMovie || loading) return ;
+ if (error) {
+ return <>{error!.message}>;
+ }
+ }
+
+ function getMovieInput() {
+ const input: Partial = {
+ name,
+ aliases,
+ duration,
+ date,
+ rating,
+ director,
+ synopsis,
+ url,
+ front_image: frontImage,
+ back_image: backImage
+ };
+
+ if (!isNew) {
+ (input as GQL.MovieUpdateInput).id = id;
+ }
+ return input;
+ }
+
+ async function onSave() {
+ try {
+ if (!isNew) {
+ const result = await updateMovie();
+ if (result.data?.movieUpdate) {
+ updateMovieData(result.data.movieUpdate);
+ setIsEditing(false);
+ }
+ } else {
+ const result = await createMovie();
+ if (result.data?.movieCreate?.id) {
+ history.push(`/movies/${result.data.movieCreate.id}`);
+ setIsEditing(false);
+ }
+ }
+ } catch (e) {
+ Toast.error(e);
+ }
+ }
+
+ async function onDelete() {
+ try {
+ await deleteMovie();
+ } catch (e) {
+ Toast.error(e);
+ }
+
+ // redirect to movies page
+ history.push(`/movies`);
+ }
+
+ function onImageChange(event: React.FormEvent) {
+ ImageUtils.onImageChange(event, onImageLoad);
+ }
+
+ function onBackImageChange(event: React.FormEvent) {
+ ImageUtils.onImageChange(event, onBackImageLoad);
+ }
+
+ function renderDeleteAlert() {
+ return (
+ setIsDeleteAlertOpen(false) }}
+ >
+ Are you sure you want to delete {movie.name ?? "movie"}?
+
+ );
+ }
+
+ // TODO: CSS class
+ return (
+
+
+ {isNew &&
Add Movie
}
+
+

+

+
+
+
+
+ {TableUtils.renderInputGroup({
+ title: "Name",
+ value: movie.name ?? "",
+ isEditing: !!isEditing,
+ onChange: setName
+ })}
+ {TableUtils.renderInputGroup({
+ title: "Aliases",
+ value: aliases,
+ isEditing,
+ onChange: setAliases
+ })}
+ {TableUtils.renderInputGroup({
+ title: "Duration",
+ value: duration,
+ isEditing,
+ onChange: setDuration
+ })}
+ {TableUtils.renderInputGroup({
+ title: "Date (YYYY-MM-DD)",
+ value: date,
+ isEditing,
+ onChange: setDate
+ })}
+ {TableUtils.renderInputGroup({
+ title: "Director",
+ value: director,
+ isEditing,
+ onChange: setDirector
+ })}
+ {TableUtils.renderHtmlSelect({
+ title: "Rating",
+ value: rating,
+ isEditing,
+ onChange: (value: string) => setRating(value),
+ selectOptions: ["", "1", "2", "3", "4", "5"]
+ })}
+
+
+
+
+ URL
+ ) =>
+ setUrl(newValue.currentTarget.value)
+ }
+ value={url}
+ />
+
+
+
+ Synopsis
+ ) =>
+ setSynopsis(newValue.currentTarget.value)
+ }
+ value={synopsis}
+ />
+
+
+
setIsEditing(!isEditing)}
+ onSave={onSave}
+ onImageChange={onImageChange}
+ onBackImageChange={onBackImageChange}
+ onDelete={onDelete}
+ />
+
+ {!isNew && (
+
+
+
+ )}
+ {renderDeleteAlert()}
+
+ );
+};
diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx
new file mode 100644
index 000000000..45ce35968
--- /dev/null
+++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieScenesPanel.tsx
@@ -0,0 +1,45 @@
+import React from "react";
+import * as GQL from "src/core/generated-graphql";
+import { MoviesCriterion } from "src/models/list-filter/criteria/movies";
+import { ListFilterModel } from "src/models/list-filter/filter";
+import { SceneList } from "src/components/Scenes/SceneList";
+
+interface IMovieScenesPanel {
+ movie: Partial;
+}
+
+export const MovieScenesPanel: React.FC = ({ movie }) => {
+ function filterHook(filter: ListFilterModel) {
+ const movieValue = { id: movie.id!, label: movie.name! };
+ // if movie is already present, then we modify it, otherwise add
+ let movieCriterion = filter.criteria.find(c => {
+ return c.type === "movies";
+ }) as MoviesCriterion;
+
+ if (
+ movieCriterion &&
+ (movieCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
+ movieCriterion.modifier === GQL.CriterionModifier.Includes)
+ ) {
+ // add the movie if not present
+ if (
+ !movieCriterion.value.find(p => {
+ return p.id === movie.id;
+ })
+ ) {
+ movieCriterion.value.push(movieValue);
+ }
+
+ movieCriterion.modifier = GQL.CriterionModifier.IncludesAll;
+ } else {
+ // overwrite
+ movieCriterion = new MoviesCriterion();
+ movieCriterion.value = [movieValue];
+ filter.criteria.push(movieCriterion);
+ }
+
+ return filter;
+ }
+
+ return ;
+};
diff --git a/ui/v2.5/src/components/Movies/MovieList.tsx b/ui/v2.5/src/components/Movies/MovieList.tsx
new file mode 100644
index 000000000..02292d098
--- /dev/null
+++ b/ui/v2.5/src/components/Movies/MovieList.tsx
@@ -0,0 +1,35 @@
+import React from "react";
+import { FindMoviesQueryResult } from "src/core/generated-graphql";
+import { ListFilterModel } from "src/models/list-filter/filter";
+import { DisplayMode } from "src/models/list-filter/types";
+import { useMoviesList } from "src/hooks/ListHook";
+import { MovieCard } from "./MovieCard";
+
+export const MovieList: React.FC = () => {
+ const listData = useMoviesList({
+ renderContent
+ });
+
+ function renderContent(
+ result: FindMoviesQueryResult,
+ filter: ListFilterModel
+ ) {
+ if (!result.data?.findMovies) {
+ return;
+ }
+ if (filter.displayMode === DisplayMode.Grid) {
+ return (
+
+ {result.data.findMovies.movies.map(p => (
+
+ ))}
+
+ );
+ }
+ if (filter.displayMode === DisplayMode.List) {
+ return TODO
;
+ }
+ }
+
+ return listData.template;
+};
diff --git a/ui/v2.5/src/components/Movies/Movies.tsx b/ui/v2.5/src/components/Movies/Movies.tsx
new file mode 100644
index 000000000..387f6391c
--- /dev/null
+++ b/ui/v2.5/src/components/Movies/Movies.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+import { Route, Switch } from "react-router-dom";
+import { Movie } from "./MovieDetails/Movie";
+import { MovieList } from "./MovieList";
+
+const Movies = () => (
+
+
+
+
+);
+
+export default Movies;
diff --git a/ui/v2.5/src/components/Movies/styles.scss b/ui/v2.5/src/components/Movies/styles.scss
new file mode 100644
index 000000000..19c487d47
--- /dev/null
+++ b/ui/v2.5/src/components/Movies/styles.scss
@@ -0,0 +1,23 @@
+.card.movie-card {
+ padding: 0 0 1rem 0;
+}
+
+.movie-card {
+ &-header {
+ height: 240px;
+ line-height: 240px;
+ text-align: center;
+ }
+
+ &-image {
+ max-height: 240px;
+ object-fit: contain;
+ vertical-align: middle;
+ width: 320px;
+
+ @media (max-width: 576px) {
+ width: 100%;
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx
index eb36bfeea..cb178e057 100644
--- a/ui/v2.5/src/components/Scenes/SceneCard.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx
@@ -126,6 +126,39 @@ export const SceneCard: React.FC = (
);
}
+ function maybeRenderMoviePopoverButton() {
+ if (props.scene.movies.length <= 0) return;
+
+ const popoverContent = props.scene.movies.map(sceneMovie => (
+
+
+

+
+
+
+ ));
+
+ return (
+
+
+
+ );
+ }
+
function maybeRenderSceneMarkerPopoverButton() {
if (props.scene.scene_markers.length <= 0) return;
@@ -161,6 +194,7 @@ export const SceneCard: React.FC = (
if (
props.scene.tags.length > 0 ||
props.scene.performers.length > 0 ||
+ props.scene.movies.length > 0 ||
props.scene.scene_markers.length > 0 ||
props.scene?.o_counter
) {
@@ -170,6 +204,7 @@ export const SceneCard: React.FC = (
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
+ {maybeRenderMoviePopoverButton()}
{maybeRenderSceneMarkerPopoverButton()}
{maybeRenderOCounter()}
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx
index 29b316364..e2fdadd24 100644
--- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx
@@ -15,6 +15,7 @@ import { SceneEditPanel } from "./SceneEditPanel";
import { SceneDetailPanel } from "./SceneDetailPanel";
import { OCounterButton } from "./OCounterButton";
import { SceneOperationsPanel } from "./SceneOperationsPanel";
+import { SceneMoviePanel } from "./SceneMoviePanel";
export const Scene: React.FC = () => {
const { id = "new" } = useParams();
@@ -124,6 +125,13 @@ export const Scene: React.FC = () => {
) : (
""
)}
+ {scene.movies.length > 0 ? (
+
+
+
+ ) : (
+ ""
+ )}
{scene.gallery ? (
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx
index 99b68fe14..91dc954ad 100644
--- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneEditPanel.tsx
@@ -16,6 +16,8 @@ import {
} from "src/components/Shared";
import { useToast } from "src/hooks";
import { ImageUtils, TableUtils } from "src/utils";
+import { MovieSelect } from "src/components/Shared/Select";
+import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
interface IProps {
scene: GQL.SceneDataFragment;
@@ -33,6 +35,10 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
const [galleryId, setGalleryId] = useState();
const [studioId, setStudioId] = useState();
const [performerIds, setPerformerIds] = useState();
+ const [movieIds, setMovieIds] = useState(undefined);
+ const [movieSceneIndexes, setMovieSceneIndexes] = useState<
+ MovieSceneIndexMap
+ >(new Map());
const [tagIds, setTagIds] = useState();
const [coverImage, setCoverImage] = useState();
@@ -59,9 +65,46 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
setQueryableScrapers(newQueryableScrapers);
}, [Scrapers]);
+ useEffect(() => {
+ let changed = false;
+ const newMap: MovieSceneIndexMap = new Map();
+ if (movieIds) {
+ movieIds.forEach(id => {
+ if (!movieSceneIndexes.has(id)) {
+ changed = true;
+ newMap.set(id, undefined);
+ } else {
+ newMap.set(id, movieSceneIndexes.get(id));
+ }
+ });
+
+ if (!changed) {
+ movieSceneIndexes.forEach((v, id) => {
+ if (!newMap.has(id)) {
+ // id was removed
+ changed = true;
+ }
+ });
+ }
+
+ if (changed) {
+ setMovieSceneIndexes(newMap);
+ }
+ }
+ }, [movieIds, movieSceneIndexes]);
+
function updateSceneEditState(state: Partial) {
const perfIds = state.performers?.map(performer => performer.id);
const tIds = state.tags ? state.tags.map(tag => tag.id) : undefined;
+ const moviIds = state.movies
+ ? state.movies.map(sceneMovie => sceneMovie.movie.id)
+ : undefined;
+ const movieSceneIdx: MovieSceneIndexMap = new Map();
+ if (state.movies) {
+ state.movies.forEach(m => {
+ movieSceneIdx.set(m.movie.id, m.scene_index ?? undefined);
+ });
+ }
setTitle(state.title ?? undefined);
setDetails(state.details ?? undefined);
@@ -70,6 +113,8 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
setRating(state.rating === null ? NaN : state.rating);
setGalleryId(state?.gallery?.id ?? undefined);
setStudioId(state?.studio?.id ?? undefined);
+ setMovieIds(moviIds);
+ setMovieSceneIndexes(movieSceneIdx);
setPerformerIds(perfIds);
setTagIds(tIds);
}
@@ -93,11 +138,31 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
gallery_id: galleryId,
studio_id: studioId,
performer_ids: performerIds,
+ movies: makeMovieInputs(),
tag_ids: tagIds,
cover_image: coverImage
};
}
+ function makeMovieInputs(): GQL.SceneMovieInput[] | undefined {
+ if (!movieIds) {
+ return undefined;
+ }
+
+ let ret = movieIds.map(id => {
+ const r: GQL.SceneMovieInput = {
+ movie_id: id
+ };
+ return r;
+ });
+
+ ret = ret.map(r => {
+ return { scene_index: movieSceneIndexes.get(r.movie_id), ...r };
+ });
+
+ return ret;
+ }
+
async function onSave() {
setIsLoading(true);
try {
@@ -133,6 +198,17 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
props.onDelete();
}
+ function renderTableMovies() {
+ return (
+ {
+ setMovieSceneIndexes(items);
+ }}
+ />
+ );
+ }
+
function renderDeleteAlert() {
return (
= (props: IProps) => {
return (
{queryableScrapers.map(s => (
- onScrapeClicked(s)}>
+ onScrapeClicked(s)}>
{s.name}
))}
@@ -247,6 +323,21 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
}
}
+ if (
+ (!movieIds || movieIds.length === 0) &&
+ scene.movies &&
+ scene.movies.length > 0
+ ) {
+ const idMovis = scene.movies.filter(p => {
+ return p.id !== undefined && p.id !== null;
+ });
+
+ if (idMovis.length > 0) {
+ const newIds = idMovis.map(p => p.id);
+ setMovieIds(newIds as string[]);
+ }
+ }
+
if (!tagIds?.length && scene?.tags?.length) {
const idTags = scene.tags.filter(p => {
return p.id !== undefined && p.id !== null;
@@ -369,6 +460,17 @@ export const SceneEditPanel: React.FC = (props: IProps) => {
/>
+
+ | Movies/Scenes |
+
+ setMovieIds(items.map(item => item.id))}
+ ids={movieIds}
+ />
+ {renderTableMovies()}
+ |
+
| Tags |
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx
new file mode 100644
index 000000000..a68e8cc53
--- /dev/null
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMoviePanel.tsx
@@ -0,0 +1,25 @@
+import React, { FunctionComponent } from "react";
+import * as GQL from "src/core/generated-graphql";
+import { MovieCard } from "src/components/Movies/MovieCard";
+
+interface ISceneMoviePanelProps {
+ scene: GQL.SceneDataFragment;
+}
+
+export const SceneMoviePanel: FunctionComponent = (
+ props: ISceneMoviePanelProps
+) => {
+ const cards = props.scene.movies.map(sceneMovie => (
+
+ ));
+
+ return (
+ <>
+ {cards}
+ >
+ );
+};
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx
new file mode 100644
index 000000000..2c4ba41c7
--- /dev/null
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx
@@ -0,0 +1,76 @@
+import * as React from "react";
+import * as GQL from "src/core/generated-graphql";
+import { StashService } from "src/core/StashService";
+import { Form } from "react-bootstrap";
+
+type ValidTypes = GQL.SlimMovieDataFragment;
+
+export type MovieSceneIndexMap = Map;
+
+export interface IProps {
+ movieSceneIndexes: MovieSceneIndexMap;
+ onUpdate: (value: MovieSceneIndexMap) => void;
+}
+
+export const SceneMovieTable: React.FunctionComponent = (
+ props: IProps
+) => {
+ const { data } = StashService.useAllMoviesForFilter();
+
+ const items = !!data && !!data.allMovies ? data.allMovies : [];
+ let itemsFilter: ValidTypes[] = [];
+
+ if (!!props.movieSceneIndexes && !!items) {
+ props.movieSceneIndexes.forEach((index, movieId) => {
+ itemsFilter = itemsFilter.concat(items.filter(x => x.id === movieId));
+ });
+ }
+
+ const storeIdx = itemsFilter.map(movie => {
+ return props.movieSceneIndexes.get(movie.id);
+ });
+
+ const updateFieldChanged = (movieId: string, value: string) => {
+ const newMap = new Map(props.movieSceneIndexes);
+ newMap.set(movieId, value);
+ props.onUpdate(newMap);
+ };
+
+ function renderTableData() {
+ return (
+
+ {itemsFilter!.map((item, index: number) => (
+
+ | {item.name} |
+ |
+ Scene number: |
+
+ ) =>
+ updateFieldChanged(item.id, e.currentTarget.value)
+ }
+ >
+ {["", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"].map(
+ opt => (
+
+ )
+ )}
+
+ |
+
+ ))}
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/ui/v2.5/src/components/Scenes/SceneListTable.tsx b/ui/v2.5/src/components/Scenes/SceneListTable.tsx
index 8dc3e66cc..63bf5119a 100644
--- a/ui/v2.5/src/components/Scenes/SceneListTable.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneListTable.tsx
@@ -26,6 +26,18 @@ export const SceneListTable: React.FC = (
));
+ const renderMovies = (movies: Partial[]) => {
+ return movies.map(sceneMovie =>
+ !sceneMovie.movie ? (
+ undefined
+ ) : (
+
+ {sceneMovie.movie.name}
+
+ )
+ );
+ };
+
const renderSceneRow = (scene: GQL.SlimSceneDataFragment) => (
|
@@ -58,6 +70,7 @@ export const SceneListTable: React.FC = (
)}
|
+ {renderMovies(scene.movies)} |
);
@@ -73,6 +86,7 @@ export const SceneListTable: React.FC = (
Tags |
Performers |
Studio |
+ Movies |
|
{props.scenes.map(renderSceneRow)}
diff --git a/ui/v2.5/src/components/Scenes/styles.scss b/ui/v2.5/src/components/Scenes/styles.scss
index 1a23390bd..40288ff3c 100644
--- a/ui/v2.5/src/components/Scenes/styles.scss
+++ b/ui/v2.5/src/components/Scenes/styles.scss
@@ -1,214 +1,218 @@
-.scene-popovers {
- display: flex;
- justify-content: center;
- margin-bottom: 10px;
-
- .btn {
- padding-bottom: 3px;
- padding-top: 3px;
- }
-
- .fa-icon {
- margin-right: 7px;
- }
-}
-
-.card-section {
- margin-bottom: 0;
- padding: .5rem 1rem 0 1rem;
-
- &-title {
- overflow: hidden;
- overflow-wrap: normal;
- text-overflow: ellipsis;
- }
-}
-
-.scene-card-check {
- left: .5rem;
- margin-top: -12px;
- opacity: .5;
- padding-left: 15px;
- position: absolute;
- top: .7rem;
- width: 1.2rem;
- z-index: 1;
-}
-
-.performer-tag-container {
- display: inline-block;
- margin: 5px;
-}
-
-.performer-tag.image {
- background-position: center;
- background-repeat: no-repeat;
- background-size: cover;
- height: 150px;
- margin: 0 auto;
- width: 100%;
-}
-
-.operation-container {
- .operation-item {
- min-width: 240px;
- }
-
- .rating-operation {
- min-width: 20px;
- }
-
- .apply-operation {
- margin-top: 2rem;
- }
-}
-
-.marker-container {
- display: "flex";
- flex-wrap: "nowrap";
- margin-bottom: "20px";
- overflow-x: "scroll";
- overflow-y: "hidden";
- white-space: "nowrap";
-}
-
-.studio-logo {
- margin-top: 1rem;
- max-width: 100%;
-}
-
-.scene-header {
- flex-basis: auto;
-}
-
-#scene-details-container {
- .tab-content {
- min-height: 15rem;
- }
-
- .scene-description {
- width: 100%;
- }
-}
-
-.file-info-panel {
- div {
- margin-bottom: .5rem;
- }
-}
-
-#details {
- min-height: 150px;
-}
-
-.primary-card {
- margin: 1rem 0;
-
- &-body {
- max-height: 15rem;
- overflow-y: auto;
- }
-}
-
-.studio-card {
- padding: .5rem;
-
- &-header {
- height: 150px;
- line-height: 150px;
- text-align: center;
- }
-
- &-image {
- max-height: 150px;
- object-fit: contain;
- vertical-align: middle;
- width: 320px;
-
- @media (max-width: 576px) {
- width: 100%;
- }
- }
-}
-
-.scene-specs-overlay {
- bottom: 1rem;
- color: $text-color;
- display: block;
- font-weight: 400;
- letter-spacing: -.03rem;
- position: absolute;
- right: .7rem;
- text-shadow: 0 0 3px #000;
-}
-
-.scene-studio-overlay {
- display: block;
- font-weight: 900;
- height: 10%;
- max-width: 40%;
- opacity: .75;
- position: absolute;
- right: .7rem;
- top: .7rem;
- z-index: 9;
-
- .image-thumbnail {
- height: auto;
- max-height: 50px;
- max-width: 100%;
- }
-
- a {
- color: $text-color;
- display: inline-block;
- letter-spacing: -.03rem;
- text-align: right;
- text-decoration: none;
- text-shadow: 0 0 3px #000;
- }
-}
-
-.overlay-resolution {
- font-weight: 900;
- margin-right: .3rem;
- text-transform: uppercase;
-}
-
-.scene-card {
- &.card {
- overflow: hidden;
- padding: 0;
- }
-
- &-link {
- position: relative;
- }
-
- .scene-specs-overlay,
- .rating-banner,
- .scene-studio-overlay {
- transition: opacity .5s;
- }
-
- &:hover {
- .scene-specs-overlay,
- .rating-banner,
- .scene-studio-overlay {
- opacity: 0;
- transition: opacity .5s;
- }
-
- .scene-studio-overlay:hover {
- opacity: .75;
- transition: opacity .5s;
- }
- }
-}
-
-.scene-cover {
- display: block;
- margin-bottom: 10px;
- margin-top: 10px;
- max-width: 100%;
-}
+.scene-popovers {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 10px;
+
+ .btn {
+ padding-bottom: 3px;
+ padding-top: 3px;
+ }
+
+ .fa-icon {
+ margin-right: 7px;
+ }
+}
+
+.card-section {
+ margin-bottom: 0;
+ padding: .5rem 1rem 0 1rem;
+
+ &-title {
+ overflow: hidden;
+ overflow-wrap: normal;
+ text-overflow: ellipsis;
+ }
+}
+
+.scene-card-check {
+ left: .5rem;
+ margin-top: -12px;
+ opacity: .5;
+ padding-left: 15px;
+ position: absolute;
+ top: .7rem;
+ width: 1.2rem;
+ z-index: 1;
+}
+
+.performer-tag-container, .movie-tag-container {
+ display: inline-block;
+ margin: 5px;
+}
+
+.performer-tag.image, .movie-tag.image {
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ height: 150px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.operation-container {
+ .operation-item {
+ min-width: 240px;
+ }
+
+ .rating-operation {
+ min-width: 20px;
+ }
+
+ .apply-operation {
+ margin-top: 2rem;
+ }
+}
+
+.marker-container {
+ display: "flex";
+ flex-wrap: "nowrap";
+ margin-bottom: "20px";
+ overflow-x: "scroll";
+ overflow-y: "hidden";
+ white-space: "nowrap";
+}
+
+.studio-logo {
+ margin-top: 1rem;
+ max-width: 100%;
+}
+
+.scene-header {
+ flex-basis: auto;
+}
+
+#scene-details-container {
+ .tab-content {
+ min-height: 15rem;
+ }
+
+ .scene-description {
+ width: 100%;
+ }
+}
+
+.file-info-panel {
+ div {
+ margin-bottom: .5rem;
+ }
+}
+
+#details {
+ min-height: 150px;
+}
+
+.primary-card {
+ margin: 1rem 0;
+
+ &-body {
+ max-height: 15rem;
+ overflow-y: auto;
+ }
+}
+
+.studio-card {
+ padding: .5rem;
+
+ &-header {
+ height: 150px;
+ line-height: 150px;
+ text-align: center;
+ }
+
+ &-image {
+ max-height: 150px;
+ object-fit: contain;
+ vertical-align: middle;
+ width: 320px;
+
+ @media (max-width: 576px) {
+ width: 100%;
+ }
+ }
+}
+
+.scene-specs-overlay {
+ bottom: 1rem;
+ color: $text-color;
+ display: block;
+ font-weight: 400;
+ letter-spacing: -.03rem;
+ position: absolute;
+ right: .7rem;
+ text-shadow: 0 0 3px #000;
+}
+
+.scene-studio-overlay {
+ display: block;
+ font-weight: 900;
+ height: 10%;
+ max-width: 40%;
+ opacity: .75;
+ position: absolute;
+ right: .7rem;
+ top: .7rem;
+ z-index: 9;
+
+ .image-thumbnail {
+ height: auto;
+ max-height: 50px;
+ max-width: 100%;
+ }
+
+ a {
+ color: $text-color;
+ display: inline-block;
+ letter-spacing: -.03rem;
+ text-align: right;
+ text-decoration: none;
+ text-shadow: 0 0 3px #000;
+ }
+}
+
+.overlay-resolution {
+ font-weight: 900;
+ margin-right: .3rem;
+ text-transform: uppercase;
+}
+
+.scene-card {
+ &.card {
+ overflow: hidden;
+ padding: 0;
+ }
+
+ &-link {
+ position: relative;
+ }
+
+ .scene-specs-overlay,
+ .rating-banner,
+ .scene-studio-overlay {
+ transition: opacity .5s;
+ }
+
+ &:hover {
+ .scene-specs-overlay,
+ .rating-banner,
+ .scene-studio-overlay {
+ opacity: 0;
+ transition: opacity .5s;
+ }
+
+ .scene-studio-overlay:hover {
+ opacity: .75;
+ transition: opacity .5s;
+ }
+ }
+}
+
+.scene-cover {
+ display: block;
+ margin-bottom: 10px;
+ margin-top: 10px;
+ max-width: 100%;
+}
+
+.movie-table td {
+ vertical-align: middle;
+}
\ No newline at end of file
diff --git a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx
index a4297ef88..2a092d0b0 100644
--- a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx
+++ b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx
@@ -1,11 +1,9 @@
import { Button, Modal } from "react-bootstrap";
import React, { useState } from "react";
-import * as GQL from "src/core/generated-graphql";
import { ImageInput } from "src/components/Shared";
interface IProps {
- performer?: Partial;
- studio?: Partial;
+ objectName?: string;
isNew: boolean;
isEditing: boolean;
onToggleEdit: () => void;
@@ -13,6 +11,7 @@ interface IProps {
onDelete: () => void;
onAutoTag?: () => void;
onImageChange: (event: React.FormEvent) => void;
+ onBackImageChange?: (event: React.FormEvent) => void;
}
export const DetailsEditNavbar: React.FC = (props: IProps) => {
@@ -54,6 +53,19 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => {
);
}
+ function renderBackImageInput() {
+ if (!props.isEditing || !props.onBackImageChange) {
+ return;
+ }
+ return (
+
+ );
+ }
+
function renderAutoTagButton() {
if (props.isNew || props.isEditing) return;
@@ -74,11 +86,11 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => {
}
function renderDeleteAlert() {
- const name = props?.studio?.name ?? props?.performer?.name;
-
return (
- Are you sure you want to delete {name}?
+
+ Are you sure you want to delete {props.objectName}?
+
+
diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
index f6a123934..4c512bde0 100644
--- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
+++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx
@@ -177,7 +177,7 @@ export const Studio: React.FC = () => {
setIsEditing(!isEditing)}
diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts
index e83e5a378..f8afb6f83 100644
--- a/ui/v2.5/src/core/StashService.ts
+++ b/ui/v2.5/src/core/StashService.ts
@@ -174,6 +174,14 @@ export class StashService {
});
}
+ public static useFindMovies(filter: ListFilterModel) {
+ return GQL.useFindMoviesQuery({
+ variables: {
+ filter: filter.makeFindFilter()
+ }
+ });
+ }
+
public static useFindPerformers(filter: ListFilterModel) {
let performerFilter = {};
// if (!!filter && filter.criteriaFilterOpen) {
@@ -220,6 +228,10 @@ export class StashService {
const skip = id === "new";
return GQL.useFindStudioQuery({ variables: { id }, skip });
}
+ public static useFindMovie(id: string) {
+ const skip = id === "new";
+ return GQL.useFindMovieQuery({ variables: { id }, skip });
+ }
// TODO - scene marker manipulation functions are handled differently
private static sceneMarkerMutationImpactedQueries = [
@@ -279,6 +291,9 @@ export class StashService {
public static useAllStudiosForFilter() {
return GQL.useAllStudiosForFilterQuery();
}
+ public static useAllMoviesForFilter() {
+ return GQL.useAllMoviesForFilterQuery();
+ }
public static useValidGalleriesForScene(sceneId: string) {
return GQL.useValidGalleriesForSceneQuery({
variables: { scene_id: sceneId }
@@ -341,6 +356,7 @@ export class StashService {
"findScenes",
"findSceneMarkers",
"findStudios",
+ "findMovies",
"allTags"
// TODO - add "findTags" when it is implemented
];
@@ -362,6 +378,7 @@ export class StashService {
"findPerformers",
"findSceneMarkers",
"findStudios",
+ "findMovies",
"allTags"
];
@@ -449,6 +466,42 @@ export class StashService {
});
}
+ private static movieMutationImpactedQueries = [
+ "findMovies",
+ "findScenes",
+ "allMovies"
+ ];
+
+ public static useMovieCreate(input: GQL.MovieCreateInput) {
+ return GQL.useMovieCreateMutation({
+ variables: input,
+ update: () =>
+ StashService.invalidateQueries(
+ StashService.movieMutationImpactedQueries
+ )
+ });
+ }
+
+ public static useMovieUpdate(input: GQL.MovieUpdateInput) {
+ return GQL.useMovieUpdateMutation({
+ variables: input,
+ update: () =>
+ StashService.invalidateQueries(
+ StashService.movieMutationImpactedQueries
+ )
+ });
+ }
+
+ public static useMovieDestroy(input: GQL.MovieDestroyInput) {
+ return GQL.useMovieDestroyMutation({
+ variables: input,
+ update: () =>
+ StashService.invalidateQueries(
+ StashService.movieMutationImpactedQueries
+ )
+ });
+ }
+
private static tagMutationImpactedQueries = [
"findScenes",
"findSceneMarkers",
diff --git a/ui/v2.5/src/core/generated-graphql.tsx b/ui/v2.5/src/core/generated-graphql.tsx
deleted file mode 100644
index 66aff1fd7..000000000
--- a/ui/v2.5/src/core/generated-graphql.tsx
+++ /dev/null
@@ -1,5192 +0,0 @@
-/* eslint-disable */
-import gql from 'graphql-tag';
-import * as ApolloReactCommon from '@apollo/react-common';
-import * as React from 'react';
-import * as ApolloReactComponents from '@apollo/react-components';
-import * as ApolloReactHooks from '@apollo/react-hooks';
-export type Maybe = T | null;
-export type Omit = Pick>;
-
-// Generated in 2020-03-05T11:18:15+11:00
-
-/** All built-in and custom scalars, mapped to their actual values */
-export type Scalars = {
- ID: string,
- String: string,
- Boolean: boolean,
- Int: number,
- Float: number,
- /** Log entries */
- Time: any,
-};
-
-export type AutoTagMetadataInput = {
- /** IDs of performers to tag files with, or "*" for all */
- performers?: Maybe>,
- /** IDs of studios to tag files with, or "*" for all */
- studios?: Maybe>,
- /** IDs of tags to tag files with, or "*" for all */
- tags?: Maybe>,
-};
-
-export type BulkSceneUpdateInput = {
- clientMutationId?: Maybe,
- ids?: Maybe>,
- title?: Maybe,
- details?: Maybe,
- url?: Maybe,
- date?: Maybe,
- rating?: Maybe,
- studio_id?: Maybe,
- gallery_id?: Maybe,
- performer_ids?: Maybe>,
- tag_ids?: Maybe>,
-};
-
-export type ConfigGeneralInput = {
- /** Array of file paths to content */
- stashes?: Maybe>,
- /** Path to the SQLite database */
- databasePath?: Maybe,
- /** Path to generated files */
- generatedPath?: Maybe,
- /** Max generated transcode size */
- maxTranscodeSize?: Maybe,
- /** Max streaming transcode size */
- maxStreamingTranscodeSize?: Maybe,
- /** Username */
- username?: Maybe,
- /** Password */
- password?: Maybe,
- /** Name of the log file */
- logFile?: Maybe,
- /** Whether to also output to stderr */
- logOut: Scalars['Boolean'],
- /** Minimum log level */
- logLevel: Scalars['String'],
- /** Whether to log http access */
- logAccess: Scalars['Boolean'],
- /** Array of file regexp to exclude from Scan */
- excludes?: Maybe>,
-};
-
-export type ConfigGeneralResult = {
- __typename?: 'ConfigGeneralResult',
- /** Array of file paths to content */
- stashes: Array,
- /** Path to the SQLite database */
- databasePath: Scalars['String'],
- /** Path to generated files */
- generatedPath: Scalars['String'],
- /** Max generated transcode size */
- maxTranscodeSize?: Maybe,
- /** Max streaming transcode size */
- maxStreamingTranscodeSize?: Maybe,
- /** Username */
- username: Scalars['String'],
- /** Password */
- password: Scalars['String'],
- /** Name of the log file */
- logFile?: Maybe,
- /** Whether to also output to stderr */
- logOut: Scalars['Boolean'],
- /** Minimum log level */
- logLevel: Scalars['String'],
- /** Whether to log http access */
- logAccess: Scalars['Boolean'],
- /** Array of file regexp to exclude from Scan */
- excludes: Array,
-};
-
-export type ConfigInterfaceInput = {
- /** Enable sound on mouseover previews */
- soundOnPreview?: Maybe,
- /** Show title and tags in wall view */
- wallShowTitle?: Maybe,
- /** Maximum duration (in seconds) in which a scene video will loop in the scene player */
- maximumLoopDuration?: Maybe,
- /** If true, video will autostart on load in the scene player */
- autostartVideo?: Maybe,
- /** If true, studio overlays will be shown as text instead of logo images */
- showStudioAsText?: Maybe,
- /** Custom CSS */
- css?: Maybe,
- cssEnabled?: Maybe,
- language?: Maybe,
-};
-
-export type ConfigInterfaceResult = {
- __typename?: 'ConfigInterfaceResult',
- /** Enable sound on mouseover previews */
- soundOnPreview?: Maybe,
- /** Show title and tags in wall view */
- wallShowTitle?: Maybe,
- /** Maximum duration (in seconds) in which a scene video will loop in the scene player */
- maximumLoopDuration?: Maybe,
- /** If true, video will autostart on load in the scene player */
- autostartVideo?: Maybe,
- /** If true, studio overlays will be shown as text instead of logo images */
- showStudioAsText?: Maybe,
- /** Custom CSS */
- css?: Maybe,
- cssEnabled?: Maybe,
- /** Interface language */
- language?: Maybe,
-};
-
-/** All configuration settings */
-export type ConfigResult = {
- __typename?: 'ConfigResult',
- general: ConfigGeneralResult,
- interface: ConfigInterfaceResult,
-};
-
-export enum CriterionModifier {
- /** = */
- Equals = 'EQUALS',
- /** != */
- NotEquals = 'NOT_EQUALS',
- /** > */
- GreaterThan = 'GREATER_THAN',
- /** < */
- LessThan = 'LESS_THAN',
- /** IS NULL */
- IsNull = 'IS_NULL',
- /** IS NOT NULL */
- NotNull = 'NOT_NULL',
- /** INCLUDES ALL */
- IncludesAll = 'INCLUDES_ALL',
- Includes = 'INCLUDES',
- Excludes = 'EXCLUDES'
-}
-
-export type FindFilterType = {
- q?: Maybe,
- page?: Maybe,
- per_page?: Maybe,
- sort?: Maybe,
- direction?: Maybe,
-};
-
-export type FindGalleriesResultType = {
- __typename?: 'FindGalleriesResultType',
- count: Scalars['Int'],
- galleries: Array,
-};
-
-export type FindPerformersResultType = {
- __typename?: 'FindPerformersResultType',
- count: Scalars['Int'],
- performers: Array,
-};
-
-export type FindSceneMarkersResultType = {
- __typename?: 'FindSceneMarkersResultType',
- count: Scalars['Int'],
- scene_markers: Array,
-};
-
-export type FindScenesResultType = {
- __typename?: 'FindScenesResultType',
- count: Scalars['Int'],
- scenes: Array,
-};
-
-export type FindStudiosResultType = {
- __typename?: 'FindStudiosResultType',
- count: Scalars['Int'],
- studios: Array,
-};
-
-/** Gallery type */
-export type Gallery = {
- __typename?: 'Gallery',
- id: Scalars['ID'],
- checksum: Scalars['String'],
- path: Scalars['String'],
- title?: Maybe,
- /** The files in the gallery */
- files: Array,
-};
-
-export type GalleryFilesType = {
- __typename?: 'GalleryFilesType',
- index: Scalars['Int'],
- name?: Maybe,
- path?: Maybe,
-};
-
-export type GenerateMetadataInput = {
- sprites: Scalars['Boolean'],
- previews: Scalars['Boolean'],
- markers: Scalars['Boolean'],
- transcodes: Scalars['Boolean'],
-};
-
-export type IntCriterionInput = {
- value: Scalars['Int'],
- modifier: CriterionModifier,
-};
-
-export type LogEntry = {
- __typename?: 'LogEntry',
- time: Scalars['Time'],
- level: LogLevel,
- message: Scalars['String'],
-};
-
-export enum LogLevel {
- Debug = 'Debug',
- Info = 'Info',
- Progress = 'Progress',
- Warning = 'Warning',
- Error = 'Error'
-}
-
-export type MarkerStringsResultType = {
- __typename?: 'MarkerStringsResultType',
- count: Scalars['Int'],
- id: Scalars['ID'],
- title: Scalars['String'],
-};
-
-export type MetadataUpdateStatus = {
- __typename?: 'MetadataUpdateStatus',
- progress: Scalars['Float'],
- status: Scalars['String'],
- message: Scalars['String'],
-};
-
-export type MultiCriterionInput = {
- value?: Maybe>,
- modifier: CriterionModifier,
-};
-
-export type Mutation = {
- __typename?: 'Mutation',
- sceneUpdate?: Maybe,
- bulkSceneUpdate?: Maybe>,
- sceneDestroy: Scalars['Boolean'],
- scenesUpdate?: Maybe>>,
- /** Increments the o-counter for a scene. Returns the new value */
- sceneIncrementO: Scalars['Int'],
- /** Decrements the o-counter for a scene. Returns the new value */
- sceneDecrementO: Scalars['Int'],
- /** Resets the o-counter for a scene to 0. Returns the new value */
- sceneResetO: Scalars['Int'],
- sceneMarkerCreate?: Maybe,
- sceneMarkerUpdate?: Maybe,
- sceneMarkerDestroy: Scalars['Boolean'],
- performerCreate?: Maybe,
- performerUpdate?: Maybe,
- performerDestroy: Scalars['Boolean'],
- studioCreate?: Maybe,
- studioUpdate?: Maybe,
- studioDestroy: Scalars['Boolean'],
- tagCreate?: Maybe,
- tagUpdate?: Maybe,
- tagDestroy: Scalars['Boolean'],
- /** Change general configuration options */
- configureGeneral: ConfigGeneralResult,
- configureInterface: ConfigInterfaceResult,
-};
-
-
-export type MutationSceneUpdateArgs = {
- input: SceneUpdateInput
-};
-
-
-export type MutationBulkSceneUpdateArgs = {
- input: BulkSceneUpdateInput
-};
-
-
-export type MutationSceneDestroyArgs = {
- input: SceneDestroyInput
-};
-
-
-export type MutationScenesUpdateArgs = {
- input: Array
-};
-
-
-export type MutationSceneIncrementOArgs = {
- id: Scalars['ID']
-};
-
-
-export type MutationSceneDecrementOArgs = {
- id: Scalars['ID']
-};
-
-
-export type MutationSceneResetOArgs = {
- id: Scalars['ID']
-};
-
-
-export type MutationSceneMarkerCreateArgs = {
- input: SceneMarkerCreateInput
-};
-
-
-export type MutationSceneMarkerUpdateArgs = {
- input: SceneMarkerUpdateInput
-};
-
-
-export type MutationSceneMarkerDestroyArgs = {
- id: Scalars['ID']
-};
-
-
-export type MutationPerformerCreateArgs = {
- input: PerformerCreateInput
-};
-
-
-export type MutationPerformerUpdateArgs = {
- input: PerformerUpdateInput
-};
-
-
-export type MutationPerformerDestroyArgs = {
- input: PerformerDestroyInput
-};
-
-
-export type MutationStudioCreateArgs = {
- input: StudioCreateInput
-};
-
-
-export type MutationStudioUpdateArgs = {
- input: StudioUpdateInput
-};
-
-
-export type MutationStudioDestroyArgs = {
- input: StudioDestroyInput
-};
-
-
-export type MutationTagCreateArgs = {
- input: TagCreateInput
-};
-
-
-export type MutationTagUpdateArgs = {
- input: TagUpdateInput
-};
-
-
-export type MutationTagDestroyArgs = {
- input: TagDestroyInput
-};
-
-
-export type MutationConfigureGeneralArgs = {
- input: ConfigGeneralInput
-};
-
-
-export type MutationConfigureInterfaceArgs = {
- input: ConfigInterfaceInput
-};
-
-export type Performer = {
- __typename?: 'Performer',
- id: Scalars['ID'],
- checksum: Scalars['String'],
- name?: Maybe,
- url?: Maybe,
- twitter?: Maybe,
- instagram?: Maybe,
- birthdate?: Maybe,
- ethnicity?: Maybe,
- country?: Maybe,
- eye_color?: Maybe,
- height?: Maybe,
- measurements?: Maybe,
- fake_tits?: Maybe,
- career_length?: Maybe,
- tattoos?: Maybe,
- piercings?: Maybe,
- aliases?: Maybe,
- favorite: Scalars['Boolean'],
- image_path?: Maybe,
- scene_count?: Maybe,
- scenes: Array,
-};
-
-export type PerformerCreateInput = {
- name?: Maybe,
- url?: Maybe,
- birthdate?: Maybe,
- ethnicity?: Maybe,
- country?: Maybe,
- eye_color?: Maybe,
- height?: Maybe,
- measurements?: Maybe,
- fake_tits?: Maybe,
- career_length?: Maybe,
- tattoos?: Maybe,
- piercings?: Maybe,
- aliases?: Maybe,
- twitter?: Maybe,
- instagram?: Maybe,
- favorite?: Maybe,
- /** This should be base64 encoded */
- image?: Maybe,
-};
-
-export type PerformerDestroyInput = {
- id: Scalars['ID'],
-};
-
-export type PerformerFilterType = {
- /** Filter by favorite */
- filter_favorites?: Maybe,
- /** Filter by birth year */
- birth_year?: Maybe,
- /** Filter by age */
- age?: Maybe,
- /** Filter by ethnicity */
- ethnicity?: Maybe,
- /** Filter by country */
- country?: Maybe,
- /** Filter by eye color */
- eye_color?: Maybe,
- /** Filter by height */
- height?: Maybe,
- /** Filter by measurements */
- measurements?: Maybe,
- /** Filter by fake tits value */
- fake_tits?: Maybe,
- /** Filter by career length */
- career_length?: Maybe,
- /** Filter by tattoos */
- tattoos?: Maybe,
- /** Filter by piercings */
- piercings?: Maybe,
- /** Filter by aliases */
- aliases?: Maybe,
-};
-
-export type PerformerUpdateInput = {
- id: Scalars['ID'],
- name?: Maybe,
- url?: Maybe,
- birthdate?: Maybe,
- ethnicity?: Maybe,
- country?: Maybe,
- eye_color?: Maybe,
- height?: Maybe,
- measurements?: Maybe,
- fake_tits?: Maybe,
- career_length?: Maybe,
- tattoos?: Maybe,
- piercings?: Maybe,
- aliases?: Maybe,
- twitter?: Maybe,
- instagram?: Maybe,
- favorite?: Maybe,
- /** This should be base64 encoded */
- image?: Maybe,
-};
-
-/** The query root for this schema */
-export type Query = {
- __typename?: 'Query',
- /** Find a scene by ID or Checksum */
- findScene?: Maybe,
- /** A function which queries Scene objects */
- findScenes: FindScenesResultType,
- findScenesByPathRegex: FindScenesResultType,
- parseSceneFilenames: SceneParserResultType,
- /** A function which queries SceneMarker objects */
- findSceneMarkers: FindSceneMarkersResultType,
- /** Find a performer by ID */
- findPerformer?: Maybe,
- /** A function which queries Performer objects */
- findPerformers: FindPerformersResultType,
- /** Find a studio by ID */
- findStudio?: Maybe,
- /** A function which queries Studio objects */
- findStudios: FindStudiosResultType,
- findGallery?: Maybe,
- findGalleries: FindGalleriesResultType,
- findTag?: Maybe,
- /** Retrieve random scene markers for the wall */
- markerWall: Array,
- /** Retrieve random scenes for the wall */
- sceneWall: Array,
- /** Get marker strings */
- markerStrings: Array>,
- /** Get the list of valid galleries for a given scene ID */
- validGalleriesForScene: Array,
- /** Get stats */
- stats: StatsResultType,
- /** Organize scene markers by tag for a given scene ID */
- sceneMarkerTags: Array,
- logs: Array,
- /** List available scrapers */
- listPerformerScrapers: Array,
- listSceneScrapers: Array,
- /** Scrape a list of performers based on name */
- scrapePerformerList: Array,
- /** Scrapes a complete performer record based on a scrapePerformerList result */
- scrapePerformer?: Maybe,
- /** Scrapes a complete performer record based on a URL */
- scrapePerformerURL?: Maybe,
- /** Scrapes a complete scene record based on an existing scene */
- scrapeScene?: Maybe,
- /** Scrapes a complete performer record based on a URL */
- scrapeSceneURL?: Maybe,
- /** Scrape a performer using Freeones */
- scrapeFreeones?: Maybe,
- /** Scrape a list of performers from a query */
- scrapeFreeonesPerformerList: Array,
- /** Returns the current, complete configuration */
- configuration: ConfigResult,
- /** Returns an array of paths for the given path */
- directories: Array,
- /** Start an import. Returns the job ID */
- metadataImport: Scalars['String'],
- /** Start an export. Returns the job ID */
- metadataExport: Scalars['String'],
- /** Start a scan. Returns the job ID */
- metadataScan: Scalars['String'],
- /** Start generating content. Returns the job ID */
- metadataGenerate: Scalars['String'],
- /** Start auto-tagging. Returns the job ID */
- metadataAutoTag: Scalars['String'],
- /** Clean metadata. Returns the job ID */
- metadataClean: Scalars['String'],
- jobStatus: MetadataUpdateStatus,
- stopJob: Scalars['Boolean'],
- allPerformers: Array,
- allStudios: Array,
- allTags: Array,
- /** Version */
- version: Version,
- /** LatestVersion */
- latestversion: ShortVersion,
-};
-
-
-/** The query root for this schema */
-export type QueryFindSceneArgs = {
- id?: Maybe,
- checksum?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryFindScenesArgs = {
- scene_filter?: Maybe,
- scene_ids?: Maybe>,
- filter?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryFindScenesByPathRegexArgs = {
- filter?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryParseSceneFilenamesArgs = {
- filter?: Maybe,
- config: SceneParserInput
-};
-
-
-/** The query root for this schema */
-export type QueryFindSceneMarkersArgs = {
- scene_marker_filter?: Maybe,
- filter?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryFindPerformerArgs = {
- id: Scalars['ID']
-};
-
-
-/** The query root for this schema */
-export type QueryFindPerformersArgs = {
- performer_filter?: Maybe,
- filter?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryFindStudioArgs = {
- id: Scalars['ID']
-};
-
-
-/** The query root for this schema */
-export type QueryFindStudiosArgs = {
- filter?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryFindGalleryArgs = {
- id: Scalars['ID']
-};
-
-
-/** The query root for this schema */
-export type QueryFindGalleriesArgs = {
- filter?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryFindTagArgs = {
- id: Scalars['ID']
-};
-
-
-/** The query root for this schema */
-export type QueryMarkerWallArgs = {
- q?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QuerySceneWallArgs = {
- q?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryMarkerStringsArgs = {
- q?: Maybe,
- sort?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryValidGalleriesForSceneArgs = {
- scene_id?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QuerySceneMarkerTagsArgs = {
- scene_id: Scalars['ID']
-};
-
-
-/** The query root for this schema */
-export type QueryScrapePerformerListArgs = {
- scraper_id: Scalars['ID'],
- query: Scalars['String']
-};
-
-
-/** The query root for this schema */
-export type QueryScrapePerformerArgs = {
- scraper_id: Scalars['ID'],
- scraped_performer: ScrapedPerformerInput
-};
-
-
-/** The query root for this schema */
-export type QueryScrapePerformerUrlArgs = {
- url: Scalars['String']
-};
-
-
-/** The query root for this schema */
-export type QueryScrapeSceneArgs = {
- scraper_id: Scalars['ID'],
- scene: SceneUpdateInput
-};
-
-
-/** The query root for this schema */
-export type QueryScrapeSceneUrlArgs = {
- url: Scalars['String']
-};
-
-
-/** The query root for this schema */
-export type QueryScrapeFreeonesArgs = {
- performer_name: Scalars['String']
-};
-
-
-/** The query root for this schema */
-export type QueryScrapeFreeonesPerformerListArgs = {
- query: Scalars['String']
-};
-
-
-/** The query root for this schema */
-export type QueryDirectoriesArgs = {
- path?: Maybe
-};
-
-
-/** The query root for this schema */
-export type QueryMetadataScanArgs = {
- input: ScanMetadataInput
-};
-
-
-/** The query root for this schema */
-export type QueryMetadataGenerateArgs = {
- input: GenerateMetadataInput
-};
-
-
-/** The query root for this schema */
-export type QueryMetadataAutoTagArgs = {
- input: AutoTagMetadataInput
-};
-
-export enum ResolutionEnum {
- /** 240p */
- Low = 'LOW',
- /** 480p */
- Standard = 'STANDARD',
- /** 720p */
- StandardHd = 'STANDARD_HD',
- /** 1080p */
- FullHd = 'FULL_HD',
- /** 4k */
- FourK = 'FOUR_K'
-}
-
-export type ScanMetadataInput = {
- useFileMetadata: Scalars['Boolean'],
-};
-
-export type Scene = {
- __typename?: 'Scene',
- id: Scalars['ID'],
- checksum: Scalars['String'],
- title?: Maybe,
- details?: Maybe,
- url?: Maybe,
- date?: Maybe,
- rating?: Maybe,
- o_counter?: Maybe,
- path: Scalars['String'],
- file: SceneFileType,
- paths: ScenePathsType,
- is_streamable: Scalars['Boolean'],
- scene_markers: Array,
- gallery?: Maybe,
- studio?: Maybe,
- tags: Array,
- performers: Array,
-};
-
-export type SceneDestroyInput = {
- id: Scalars['ID'],
- delete_file?: Maybe,
- delete_generated?: Maybe,
-};
-
-export type SceneFileType = {
- __typename?: 'SceneFileType',
- size?: Maybe,
- duration?: Maybe,
- video_codec?: Maybe,
- audio_codec?: Maybe,
- width?: Maybe,
- height?: Maybe,
- framerate?: Maybe,
- bitrate?: Maybe,
-};
-
-export type SceneFilterType = {
- /** Filter by rating */
- rating?: Maybe,
- /** Filter by o-counter */
- o_counter?: Maybe,
- /** Filter by resolution */
- resolution?: Maybe,
- /** Filter by duration (in seconds) */
- duration?: Maybe,
- /** Filter to only include scenes which have markers. `true` or `false` */
- has_markers?: Maybe,
- /** Filter to only include scenes missing this property */
- is_missing?: Maybe,
- /** Filter to only include scenes with this studio */
- studios?: Maybe,
- /** Filter to only include scenes with these tags */
- tags?: Maybe,
- /** Filter to only include scenes with these performers */
- performers?: Maybe,
-};
-
-export type SceneMarker = {
- __typename?: 'SceneMarker',
- id: Scalars['ID'],
- scene: Scene,
- title: Scalars['String'],
- seconds: Scalars['Float'],
- primary_tag: Tag,
- tags: Array,
- /** The path to stream this marker */
- stream: Scalars['String'],
- /** The path to the preview image for this marker */
- preview: Scalars['String'],
-};
-
-export type SceneMarkerCreateInput = {
- title: Scalars['String'],
- seconds: Scalars['Float'],
- scene_id: Scalars['ID'],
- primary_tag_id: Scalars['ID'],
- tag_ids?: Maybe>,
-};
-
-export type SceneMarkerFilterType = {
- /** Filter to only include scene markers with this tag */
- tag_id?: Maybe,
- /** Filter to only include scene markers with these tags */
- tags?: Maybe,
- /** Filter to only include scene markers attached to a scene with these tags */
- scene_tags?: Maybe,
- /** Filter to only include scene markers with these performers */
- performers?: Maybe,
-};
-
-export type SceneMarkerTag = {
- __typename?: 'SceneMarkerTag',
- tag: Tag,
- scene_markers: Array,
-};
-
-export type SceneMarkerUpdateInput = {
- id: Scalars['ID'],
- title: Scalars['String'],
- seconds: Scalars['Float'],
- scene_id: Scalars['ID'],
- primary_tag_id: Scalars['ID'],
- tag_ids?: Maybe>,
-};
-
-export type SceneParserInput = {
- ignoreWords?: Maybe>,
- whitespaceCharacters?: Maybe,
- capitalizeTitle?: Maybe,
-};
-
-export type SceneParserResult = {
- __typename?: 'SceneParserResult',
- scene: Scene,
- title?: Maybe,
- details?: Maybe,
- url?: Maybe,
- date?: Maybe,
- rating?: Maybe,
- studio_id?: Maybe,
- gallery_id?: Maybe,
- performer_ids?: Maybe>,
- tag_ids?: Maybe>,
-};
-
-export type SceneParserResultType = {
- __typename?: 'SceneParserResultType',
- count: Scalars['Int'],
- results: Array,
-};
-
-export type ScenePathsType = {
- __typename?: 'ScenePathsType',
- screenshot?: Maybe,
- preview?: Maybe,
- stream?: Maybe,
- webp?: Maybe,
- vtt?: Maybe,
- chapters_vtt?: Maybe,
-};
-
-export type SceneUpdateInput = {
- clientMutationId?: Maybe,
- id: Scalars['ID'],
- title?: Maybe,
- details?: Maybe,
- url?: Maybe,
- date?: Maybe,
- rating?: Maybe,
- studio_id?: Maybe,
- gallery_id?: Maybe,
- performer_ids?: Maybe>,
- tag_ids?: Maybe>,
- /** This should be base64 encoded */
- cover_image?: Maybe,
-};
-
-/** A performer from a scraping operation... */
-export type ScrapedPerformer = {
- __typename?: 'ScrapedPerformer',
- name?: Maybe