mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Movie select overhaul (#4563)
* Add ids to findMovies input * Use ids for other find interfaces * Update client side * Fix gallery select function * Replace movie select * Re-add creatable * Overhaul movie table * Remove and deprecated unused code
This commit is contained in:
@@ -12,7 +12,8 @@ type Query {
|
|||||||
"A function which queries Scene objects"
|
"A function which queries Scene objects"
|
||||||
findScenes(
|
findScenes(
|
||||||
scene_filter: SceneFilterType
|
scene_filter: SceneFilterType
|
||||||
scene_ids: [Int!]
|
scene_ids: [Int!] @deprecated(reason: "use ids")
|
||||||
|
ids: [ID!]
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
): FindScenesResultType!
|
): FindScenesResultType!
|
||||||
|
|
||||||
@@ -50,7 +51,8 @@ type Query {
|
|||||||
"A function which queries Scene objects"
|
"A function which queries Scene objects"
|
||||||
findImages(
|
findImages(
|
||||||
image_filter: ImageFilterType
|
image_filter: ImageFilterType
|
||||||
image_ids: [Int!]
|
image_ids: [Int!] @deprecated(reason: "use ids")
|
||||||
|
ids: [ID!]
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
): FindImagesResultType!
|
): FindImagesResultType!
|
||||||
|
|
||||||
@@ -60,7 +62,8 @@ type Query {
|
|||||||
findPerformers(
|
findPerformers(
|
||||||
performer_filter: PerformerFilterType
|
performer_filter: PerformerFilterType
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
performer_ids: [Int!]
|
performer_ids: [Int!] @deprecated(reason: "use ids")
|
||||||
|
ids: [ID!]
|
||||||
): FindPerformersResultType!
|
): FindPerformersResultType!
|
||||||
|
|
||||||
"Find a studio by ID"
|
"Find a studio by ID"
|
||||||
@@ -78,6 +81,7 @@ type Query {
|
|||||||
findMovies(
|
findMovies(
|
||||||
movie_filter: MovieFilterType
|
movie_filter: MovieFilterType
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
|
ids: [ID!]
|
||||||
): FindMoviesResultType!
|
): FindMoviesResultType!
|
||||||
|
|
||||||
findGallery(id: ID!): Gallery
|
findGallery(id: ID!): Gallery
|
||||||
@@ -91,7 +95,7 @@ type Query {
|
|||||||
findTags(
|
findTags(
|
||||||
tag_filter: TagFilterType
|
tag_filter: TagFilterType
|
||||||
filter: FindFilterType
|
filter: FindFilterType
|
||||||
ids: [Int!]
|
ids: [ID!]
|
||||||
): FindTagsResultType!
|
): FindTagsResultType!
|
||||||
|
|
||||||
"Retrieve random scene markers for the wall"
|
"Retrieve random scene markers for the wall"
|
||||||
@@ -200,15 +204,16 @@ type Query {
|
|||||||
|
|
||||||
# Get everything
|
# Get everything
|
||||||
|
|
||||||
allScenes: [Scene!]!
|
allScenes: [Scene!]! @deprecated(reason: "Use findScenes instead")
|
||||||
allSceneMarkers: [SceneMarker!]!
|
allSceneMarkers: [SceneMarker!]!
|
||||||
allImages: [Image!]!
|
@deprecated(reason: "Use findSceneMarkers instead")
|
||||||
allGalleries: [Gallery!]!
|
allImages: [Image!]! @deprecated(reason: "Use findImages instead")
|
||||||
allMovies: [Movie!]!
|
allGalleries: [Gallery!]! @deprecated(reason: "Use findGalleries instead")
|
||||||
|
|
||||||
allPerformers: [Performer!]! @deprecated(reason: "Use findPerformers instead")
|
allPerformers: [Performer!]!
|
||||||
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
allTags: [Tag!]! @deprecated(reason: "Use findTags instead")
|
||||||
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
allStudios: [Studio!]! @deprecated(reason: "Use findStudios instead")
|
||||||
|
allMovies: [Movie!]! @deprecated(reason: "Use findMovies instead")
|
||||||
|
|
||||||
# Get everything with minimal metadata
|
# Get everything with minimal metadata
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/99designs/gqlgen/graphql"
|
"github.com/99designs/gqlgen/graphql"
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *string) (*models.Image, error) {
|
||||||
@@ -46,13 +47,52 @@ func (r *queryResolver) FindImage(ctx context.Context, id *string, checksum *str
|
|||||||
return image, nil
|
return image, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.ImageFilterType, imageIds []int, filter *models.FindFilterType) (ret *FindImagesResultType, err error) {
|
func (r *queryResolver) FindImages(
|
||||||
|
ctx context.Context,
|
||||||
|
imageFilter *models.ImageFilterType,
|
||||||
|
imageIds []int,
|
||||||
|
ids []string,
|
||||||
|
filter *models.FindFilterType,
|
||||||
|
) (ret *FindImagesResultType, err error) {
|
||||||
|
if len(ids) > 0 {
|
||||||
|
imageIds, err = stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
qb := r.repository.Image
|
qb := r.repository.Image
|
||||||
|
|
||||||
|
var images []*models.Image
|
||||||
fields := graphql.CollectAllFields(ctx)
|
fields := graphql.CollectAllFields(ctx)
|
||||||
|
result := &models.ImageQueryResult{}
|
||||||
|
|
||||||
result, err := qb.Query(ctx, models.ImageQueryOptions{
|
if len(imageIds) > 0 {
|
||||||
|
images, err = r.repository.Image.FindMany(ctx, imageIds)
|
||||||
|
if err == nil {
|
||||||
|
result.Count = len(images)
|
||||||
|
for _, s := range images {
|
||||||
|
if err = s.LoadPrimaryFile(ctx, r.repository.File); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f := s.Files.Primary()
|
||||||
|
if f == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
imageFile, ok := f.(*models.ImageFile)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Megapixels += float64(imageFile.Width*imageFile.Height) / float64(1000000)
|
||||||
|
result.TotalSize += float64(f.Base().Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result, err = qb.Query(ctx, models.ImageQueryOptions{
|
||||||
QueryOptions: models.QueryOptions{
|
QueryOptions: models.QueryOptions{
|
||||||
FindFilter: filter,
|
FindFilter: filter,
|
||||||
Count: sliceutil.Contains(fields, "count"),
|
Count: sliceutil.Contains(fields, "count"),
|
||||||
@@ -61,13 +101,9 @@ func (r *queryResolver) FindImages(ctx context.Context, imageFilter *models.Imag
|
|||||||
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
Megapixels: sliceutil.Contains(fields, "megapixels"),
|
||||||
TotalSize: sliceutil.Contains(fields, "filesize"),
|
TotalSize: sliceutil.Contains(fields, "filesize"),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err == nil {
|
||||||
return err
|
images, err = result.Resolve(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
images, err := result.Resolve(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = &FindImagesResultType{
|
ret = &FindImagesResultType{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
|
func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.Movie, err error) {
|
||||||
@@ -23,9 +24,24 @@ func (r *queryResolver) FindMovie(ctx context.Context, id string) (ret *models.M
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType) (ret *FindMoviesResultType, err error) {
|
func (r *queryResolver) FindMovies(ctx context.Context, movieFilter *models.MovieFilterType, filter *models.FindFilterType, ids []string) (ret *FindMoviesResultType, err error) {
|
||||||
|
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
movies, total, err := r.repository.Movie.Query(ctx, movieFilter, filter)
|
var movies []*models.Movie
|
||||||
|
var err error
|
||||||
|
var total int
|
||||||
|
|
||||||
|
if len(idInts) > 0 {
|
||||||
|
movies, err = r.repository.Movie.FindMany(ctx, idInts)
|
||||||
|
total = len(movies)
|
||||||
|
} else {
|
||||||
|
movies, total, err = r.repository.Movie.Query(ctx, movieFilter, filter)
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
|
func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *models.Performer, err error) {
|
||||||
@@ -23,7 +24,14 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (ret *mode
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int) (ret *FindPerformersResultType, err error) {
|
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType, performerIDs []int, ids []string) (ret *FindPerformersResultType, err error) {
|
||||||
|
if len(ids) > 0 {
|
||||||
|
performerIDs, err = stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
var performers []*models.Performer
|
var performers []*models.Performer
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
"github.com/stashapp/stash/pkg/scene"
|
"github.com/stashapp/stash/pkg/scene"
|
||||||
"github.com/stashapp/stash/pkg/sliceutil"
|
"github.com/stashapp/stash/pkg/sliceutil"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *string) (*models.Scene, error) {
|
||||||
@@ -74,7 +75,20 @@ func (r *queryResolver) FindSceneByHash(ctx context.Context, input SceneHashInpu
|
|||||||
return scene, nil
|
return scene, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIDs []int, filter *models.FindFilterType) (ret *FindScenesResultType, err error) {
|
func (r *queryResolver) FindScenes(
|
||||||
|
ctx context.Context,
|
||||||
|
sceneFilter *models.SceneFilterType,
|
||||||
|
sceneIDs []int,
|
||||||
|
ids []string,
|
||||||
|
filter *models.FindFilterType,
|
||||||
|
) (ret *FindScenesResultType, err error) {
|
||||||
|
if len(ids) > 0 {
|
||||||
|
sceneIDs, err = stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
var scenes []*models.Scene
|
var scenes []*models.Scene
|
||||||
var err error
|
var err error
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/stashapp/stash/pkg/models"
|
"github.com/stashapp/stash/pkg/models"
|
||||||
|
"github.com/stashapp/stash/pkg/sliceutil/stringslice"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
|
func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag, err error) {
|
||||||
@@ -23,14 +24,19 @@ func (r *queryResolver) FindTag(ctx context.Context, id string) (ret *models.Tag
|
|||||||
return ret, nil
|
return ret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []int) (ret *FindTagsResultType, err error) {
|
func (r *queryResolver) FindTags(ctx context.Context, tagFilter *models.TagFilterType, filter *models.FindFilterType, ids []string) (ret *FindTagsResultType, err error) {
|
||||||
|
idInts, err := stringslice.StringSliceToIntSlice(ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
if err := r.withReadTxn(ctx, func(ctx context.Context) error {
|
||||||
var tags []*models.Tag
|
var tags []*models.Tag
|
||||||
var err error
|
var err error
|
||||||
var total int
|
var total int
|
||||||
|
|
||||||
if len(ids) > 0 {
|
if len(idInts) > 0 {
|
||||||
tags, err = r.repository.Tag.FindMany(ctx, ids)
|
tags, err = r.repository.Tag.FindMany(ctx, idInts)
|
||||||
total = len(tags)
|
total = len(tags)
|
||||||
} else {
|
} else {
|
||||||
tags, total, err = r.repository.Tag.Query(ctx, tagFilter, filter)
|
tags, total, err = r.repository.Tag.Query(ctx, tagFilter, filter)
|
||||||
|
|||||||
@@ -4,3 +4,9 @@ fragment SlimMovieData on Movie {
|
|||||||
front_image_path
|
front_image_path
|
||||||
rating100
|
rating100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment SelectMovieData on Movie {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
front_image_path
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,13 +6,6 @@ query MarkerStrings($q: String, $sort: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query AllMoviesForFilter {
|
|
||||||
allMovies {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query Stats {
|
query Stats {
|
||||||
stats {
|
stats {
|
||||||
scene_count
|
scene_count
|
||||||
|
|||||||
@@ -12,3 +12,16 @@ query FindMovie($id: ID!) {
|
|||||||
...MovieData
|
...MovieData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query FindMoviesForSelect(
|
||||||
|
$filter: FindFilterType
|
||||||
|
$movie_filter: MovieFilterType
|
||||||
|
$ids: [ID!]
|
||||||
|
) {
|
||||||
|
findMovies(filter: $filter, movie_filter: $movie_filter, ids: $ids) {
|
||||||
|
count
|
||||||
|
movies {
|
||||||
|
...SelectMovieData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ query FindPerformer($id: ID!) {
|
|||||||
query FindPerformersForSelect(
|
query FindPerformersForSelect(
|
||||||
$filter: FindFilterType
|
$filter: FindFilterType
|
||||||
$performer_filter: PerformerFilterType
|
$performer_filter: PerformerFilterType
|
||||||
$performer_ids: [Int!]
|
$ids: [ID!]
|
||||||
) {
|
) {
|
||||||
findPerformers(
|
findPerformers(
|
||||||
filter: $filter
|
filter: $filter
|
||||||
performer_filter: $performer_filter
|
performer_filter: $performer_filter
|
||||||
performer_ids: $performer_ids
|
ids: $ids
|
||||||
) {
|
) {
|
||||||
count
|
count
|
||||||
performers {
|
performers {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ query FindTag($id: ID!) {
|
|||||||
query FindTagsForSelect(
|
query FindTagsForSelect(
|
||||||
$filter: FindFilterType
|
$filter: FindFilterType
|
||||||
$tag_filter: TagFilterType
|
$tag_filter: TagFilterType
|
||||||
$ids: [Int!]
|
$ids: [ID!]
|
||||||
) {
|
) {
|
||||||
findTags(filter: $filter, tag_filter: $tag_filter, ids: $ids) {
|
findTags(filter: $filter, tag_filter: $tag_filter, ids: $ids) {
|
||||||
count
|
count
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import cx from "classnames";
|
|||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import {
|
||||||
queryFindGalleries,
|
queryFindGalleriesForSelect,
|
||||||
queryFindGalleriesByIDForSelect,
|
queryFindGalleriesByIDForSelect,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
@@ -56,7 +56,7 @@ const _GallerySelect: React.FC<
|
|||||||
filter.itemsPerPage = maxOptionsShown;
|
filter.itemsPerPage = maxOptionsShown;
|
||||||
filter.sortBy = "title";
|
filter.sortBy = "title";
|
||||||
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||||
const query = await queryFindGalleries(filter);
|
const query = await queryFindGalleriesForSelect(filter);
|
||||||
let ret = query.data.findGalleries.galleries.filter((gallery) => {
|
let ret = query.data.findGalleries.galleries.filter((gallery) => {
|
||||||
// HACK - we should probably exclude these in the backend query, but
|
// HACK - we should probably exclude these in the backend query, but
|
||||||
// this will do in the short-term
|
// this will do in the short-term
|
||||||
@@ -190,8 +190,7 @@ const _GalleryIDSelect: React.FC<IFilterProps & IFilterIDProps<Gallery>> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Gallery[]> {
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Gallery[]> {
|
||||||
const galleryIDs = idsToLoad.map((id) => parseInt(id));
|
const query = await queryFindGalleriesByIDForSelect(idsToLoad);
|
||||||
const query = await queryFindGalleriesByIDForSelect(galleryIDs);
|
|
||||||
const { galleries: loadedGalleries } = query.data.findGalleries;
|
const { galleries: loadedGalleries } = query.data.findGalleries;
|
||||||
|
|
||||||
return loadedGalleries;
|
return loadedGalleries;
|
||||||
|
|||||||
238
ui/v2.5/src/components/Movies/MovieSelect.tsx
Normal file
238
ui/v2.5/src/components/Movies/MovieSelect.tsx
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
OptionProps,
|
||||||
|
components as reactSelectComponents,
|
||||||
|
MultiValueGenericProps,
|
||||||
|
SingleValueProps,
|
||||||
|
} from "react-select";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import {
|
||||||
|
queryFindMoviesForSelect,
|
||||||
|
queryFindMoviesByIDForSelect,
|
||||||
|
useMovieCreate,
|
||||||
|
} from "src/core/StashService";
|
||||||
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
|
import { defaultMaxOptionsShown } from "src/core/config";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import {
|
||||||
|
FilterSelectComponent,
|
||||||
|
IFilterIDProps,
|
||||||
|
IFilterProps,
|
||||||
|
IFilterValueProps,
|
||||||
|
Option as SelectOption,
|
||||||
|
} from "../Shared/FilterSelect";
|
||||||
|
import { useCompare } from "src/hooks/state";
|
||||||
|
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||||
|
import { sortByRelevance } from "src/utils/query";
|
||||||
|
import { PatchComponent } from "src/pluginApi";
|
||||||
|
|
||||||
|
export type Movie = Pick<GQL.Movie, "id" | "name">;
|
||||||
|
type Option = SelectOption<Movie>;
|
||||||
|
|
||||||
|
const _MovieSelect: React.FC<
|
||||||
|
IFilterProps &
|
||||||
|
IFilterValueProps<Movie> & {
|
||||||
|
hoverPlacement?: Placement;
|
||||||
|
excludeIds?: string[];
|
||||||
|
}
|
||||||
|
> = (props) => {
|
||||||
|
const [createMovie] = useMovieCreate();
|
||||||
|
|
||||||
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
const intl = useIntl();
|
||||||
|
const maxOptionsShown =
|
||||||
|
configuration?.ui.maxOptionsShown ?? defaultMaxOptionsShown;
|
||||||
|
const defaultCreatable =
|
||||||
|
!configuration?.interface.disableDropdownCreate.movie ?? true;
|
||||||
|
|
||||||
|
const exclude = useMemo(() => props.excludeIds ?? [], [props.excludeIds]);
|
||||||
|
|
||||||
|
async function loadMovies(input: string): Promise<Option[]> {
|
||||||
|
const filter = new ListFilterModel(GQL.FilterMode.Movies);
|
||||||
|
filter.searchTerm = input;
|
||||||
|
filter.currentPage = 1;
|
||||||
|
filter.itemsPerPage = maxOptionsShown;
|
||||||
|
filter.sortBy = "name";
|
||||||
|
filter.sortDirection = GQL.SortDirectionEnum.Asc;
|
||||||
|
const query = await queryFindMoviesForSelect(filter);
|
||||||
|
let ret = query.data.findMovies.movies.filter((movie) => {
|
||||||
|
// HACK - we should probably exclude these in the backend query, but
|
||||||
|
// this will do in the short-term
|
||||||
|
return !exclude.includes(movie.id.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortByRelevance(input, ret, (m) => m.name).map((movie) => ({
|
||||||
|
value: movie.id,
|
||||||
|
object: movie,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const MovieOption: React.FC<OptionProps<Option, boolean>> = (optionProps) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
const title = object.name;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: <span>{title}</span>,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.Option {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MovieMultiValueLabel: React.FC<
|
||||||
|
MultiValueGenericProps<Option, boolean>
|
||||||
|
> = (optionProps) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: object.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.MultiValueLabel {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MovieValueLabel: React.FC<SingleValueProps<Option, boolean>> = (
|
||||||
|
optionProps
|
||||||
|
) => {
|
||||||
|
let thisOptionProps = optionProps;
|
||||||
|
|
||||||
|
const { object } = optionProps.data;
|
||||||
|
|
||||||
|
thisOptionProps = {
|
||||||
|
...optionProps,
|
||||||
|
children: <>{object.name}</>,
|
||||||
|
};
|
||||||
|
|
||||||
|
return <reactSelectComponents.SingleValue {...thisOptionProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCreate = async (name: string) => {
|
||||||
|
const result = await createMovie({
|
||||||
|
variables: { input: { name } },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
value: result.data!.movieCreate!.id,
|
||||||
|
item: result.data!.movieCreate!,
|
||||||
|
message: "Created movie",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNamedObject = (id: string, name: string) => {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidNewOption = (inputValue: string, options: Movie[]) => {
|
||||||
|
if (!inputValue) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
options.some((o) => {
|
||||||
|
return o.name.toLowerCase() === inputValue.toLowerCase();
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterSelectComponent<Movie, boolean>
|
||||||
|
{...props}
|
||||||
|
className={cx(
|
||||||
|
"movie-select",
|
||||||
|
{
|
||||||
|
"movie-select-active": props.active,
|
||||||
|
},
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
loadOptions={loadMovies}
|
||||||
|
getNamedObject={getNamedObject}
|
||||||
|
isValidNewOption={isValidNewOption}
|
||||||
|
components={{
|
||||||
|
Option: MovieOption,
|
||||||
|
MultiValueLabel: MovieMultiValueLabel,
|
||||||
|
SingleValue: MovieValueLabel,
|
||||||
|
}}
|
||||||
|
isMulti={props.isMulti ?? false}
|
||||||
|
creatable={props.creatable ?? defaultCreatable}
|
||||||
|
onCreate={onCreate}
|
||||||
|
placeholder={
|
||||||
|
props.noSelectionString ??
|
||||||
|
intl.formatMessage(
|
||||||
|
{ id: "actions.select_entity" },
|
||||||
|
{
|
||||||
|
entityType: intl.formatMessage({
|
||||||
|
id: props.isMulti ? "movies" : "movie",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
closeMenuOnSelect={!props.isMulti}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MovieSelect = PatchComponent("MovieSelect", _MovieSelect);
|
||||||
|
|
||||||
|
const _MovieIDSelect: React.FC<IFilterProps & IFilterIDProps<Movie>> = (
|
||||||
|
props
|
||||||
|
) => {
|
||||||
|
const { ids, onSelect: onSelectValues } = props;
|
||||||
|
|
||||||
|
const [values, setValues] = useState<Movie[]>([]);
|
||||||
|
const idsChanged = useCompare(ids);
|
||||||
|
|
||||||
|
function onSelect(items: Movie[]) {
|
||||||
|
setValues(items);
|
||||||
|
onSelectValues?.(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Movie[]> {
|
||||||
|
const query = await queryFindMoviesByIDForSelect(idsToLoad);
|
||||||
|
const { movies: loadedMovies } = query.data.findMovies;
|
||||||
|
|
||||||
|
return loadedMovies;
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!idsChanged) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ids || ids?.length === 0) {
|
||||||
|
setValues([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the values if we have ids and they haven't been loaded yet
|
||||||
|
const filteredValues = values.filter((v) => ids.includes(v.id.toString()));
|
||||||
|
if (filteredValues.length === ids.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
const items = await loadObjectsByID(ids);
|
||||||
|
setValues(items);
|
||||||
|
};
|
||||||
|
|
||||||
|
load();
|
||||||
|
}, [ids, idsChanged, values]);
|
||||||
|
|
||||||
|
return <MovieSelect {...props} values={values} onSelect={onSelect} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MovieIDSelect = PatchComponent("MovieIDSelect", _MovieIDSelect);
|
||||||
@@ -256,8 +256,7 @@ const _PerformerIDSelect: React.FC<IFilterProps & IFilterIDProps<Performer>> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Performer[]> {
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Performer[]> {
|
||||||
const performerIDs = idsToLoad.map((id) => parseInt(id));
|
const query = await queryFindPerformersByIDForSelect(idsToLoad);
|
||||||
const query = await queryFindPerformersByIDForSelect(performerIDs);
|
|
||||||
const { performers: loadedPerformers } = query.data.findPerformers;
|
const { performers: loadedPerformers } = query.data.findPerformers;
|
||||||
|
|
||||||
return loadedPerformers;
|
return loadedPerformers;
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
queryScrapeSceneQueryFragment,
|
queryScrapeSceneQueryFragment,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { MovieSelect } from "src/components/Shared/Select";
|
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { ImageInput } from "src/components/Shared/ImageInput";
|
import { ImageInput } from "src/components/Shared/ImageInput";
|
||||||
@@ -30,7 +29,7 @@ import { useFormik } from "formik";
|
|||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
import { SceneMovieTable } from "./SceneMovieTable";
|
import { IMovieEntry, SceneMovieTable } from "./SceneMovieTable";
|
||||||
import { faSearch, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSearch, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
@@ -50,6 +49,7 @@ import { formikUtils } from "src/utils/form";
|
|||||||
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
import { Tag, TagSelect } from "src/components/Tags/TagSelect";
|
||||||
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
||||||
|
import { Movie } from "src/components/Movies/MovieSelect";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
@@ -76,6 +76,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const [galleries, setGalleries] = useState<Gallery[]>([]);
|
const [galleries, setGalleries] = useState<Gallery[]>([]);
|
||||||
const [performers, setPerformers] = useState<Performer[]>([]);
|
const [performers, setPerformers] = useState<Performer[]>([]);
|
||||||
|
const [movies, setMovies] = useState<Movie[]>([]);
|
||||||
const [tags, setTags] = useState<Tag[]>([]);
|
const [tags, setTags] = useState<Tag[]>([]);
|
||||||
const [studio, setStudio] = useState<Studio | null>(null);
|
const [studio, setStudio] = useState<Studio | null>(null);
|
||||||
|
|
||||||
@@ -104,6 +105,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setPerformers(scene.performers ?? []);
|
setPerformers(scene.performers ?? []);
|
||||||
}, [scene.performers]);
|
}, [scene.performers]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMovies(scene.movies?.map((m) => m.movie) ?? []);
|
||||||
|
}, [scene.movies]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTags(scene.tags ?? []);
|
setTags(scene.tags ?? []);
|
||||||
}, [scene.tags]);
|
}, [scene.tags]);
|
||||||
@@ -185,6 +190,17 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return sceneImage;
|
return sceneImage;
|
||||||
}, [formik.values.cover_image, scene.paths?.screenshot]);
|
}, [formik.values.cover_image, scene.paths?.screenshot]);
|
||||||
|
|
||||||
|
const movieEntries = useMemo(() => {
|
||||||
|
return formik.values.movies
|
||||||
|
.map((m) => {
|
||||||
|
return {
|
||||||
|
movie: movies.find((mm) => mm.id === m.movie_id),
|
||||||
|
scene_index: m.scene_index,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((m) => m.movie !== undefined) as IMovieEntry[];
|
||||||
|
}, [formik.values.movies, movies]);
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
@@ -258,17 +274,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setQueryableScrapers(newQueryableScrapers);
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
}, [Scrapers, stashConfig]);
|
}, [Scrapers, stashConfig]);
|
||||||
|
|
||||||
function setMovieIds(movieIds: string[]) {
|
function onSetMovies(items: Movie[]) {
|
||||||
|
setMovies(items);
|
||||||
|
|
||||||
const existingMovies = formik.values.movies;
|
const existingMovies = formik.values.movies;
|
||||||
|
|
||||||
const newMovies = movieIds.map((m) => {
|
const newMovies = items.map((m) => {
|
||||||
const existing = existingMovies.find((mm) => mm.movie_id === m);
|
const existing = existingMovies.find((mm) => mm.movie_id === m.id);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
movie_id: m,
|
movie_id: m.id,
|
||||||
scene_index: null,
|
scene_index: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -287,17 +305,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTableMovies() {
|
|
||||||
return (
|
|
||||||
<SceneMovieTable
|
|
||||||
movieScenes={formik.values.movies}
|
|
||||||
onUpdate={(items) => {
|
|
||||||
formik.setFieldValue("movies", items);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
function onImageLoad(imageData: string) {
|
||||||
@@ -400,6 +407,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
sceneStudio={studio}
|
sceneStudio={studio}
|
||||||
sceneTags={tags}
|
sceneTags={tags}
|
||||||
scenePerformers={performers}
|
scenePerformers={performers}
|
||||||
|
sceneMovies={movies}
|
||||||
scraped={scrapedScene}
|
scraped={scrapedScene}
|
||||||
endpoint={endpoint}
|
endpoint={endpoint}
|
||||||
onClose={(s) => onScrapeDialogClosed(s)}
|
onClose={(s) => onScrapeDialogClosed(s)}
|
||||||
@@ -589,8 +597,14 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (idMovis.length > 0) {
|
if (idMovis.length > 0) {
|
||||||
const newIds = idMovis.map((p) => p.stored_id);
|
onSetMovies(
|
||||||
setMovieIds(newIds as string[]);
|
idMovis.map((p) => {
|
||||||
|
return {
|
||||||
|
id: p.stored_id!,
|
||||||
|
name: p.name ?? "",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,17 +765,21 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return renderField("performer_ids", title, control, fullWidthProps);
|
return renderField("performer_ids", title, control, fullWidthProps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSetMovieEntries(input: IMovieEntry[]) {
|
||||||
|
setMovies(input.map((m) => m.movie));
|
||||||
|
|
||||||
|
const newMovies = input.map((m) => ({
|
||||||
|
movie_id: m.movie.id,
|
||||||
|
scene_index: m.scene_index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
formik.setFieldValue("movies", newMovies);
|
||||||
|
}
|
||||||
|
|
||||||
function renderMoviesField() {
|
function renderMoviesField() {
|
||||||
const title = intl.formatMessage({ id: "movies" });
|
const title = intl.formatMessage({ id: "movies" });
|
||||||
const control = (
|
const control = (
|
||||||
<>
|
<SceneMovieTable value={movieEntries} onUpdate={onSetMovieEntries} />
|
||||||
<MovieSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) => setMovieIds(items.map((item) => item.id))}
|
|
||||||
ids={formik.values.movies.map((m) => m.movie_id)}
|
|
||||||
/>
|
|
||||||
{renderTableMovies()}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return renderField("movies", title, control, fullWidthProps);
|
return renderField("movies", title, control, fullWidthProps);
|
||||||
|
|||||||
@@ -1,50 +1,96 @@
|
|||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useAllMoviesForFilter } from "src/core/StashService";
|
|
||||||
import { Form, Row, Col } from "react-bootstrap";
|
import { Form, Row, Col } from "react-bootstrap";
|
||||||
|
import { Movie, MovieSelect } from "src/components/Movies/MovieSelect";
|
||||||
|
import cx from "classnames";
|
||||||
|
|
||||||
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
||||||
|
|
||||||
|
export interface IMovieEntry {
|
||||||
|
movie: Movie;
|
||||||
|
scene_index?: GQL.InputMaybe<number> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
movieScenes: GQL.SceneMovieInput[];
|
value: IMovieEntry[];
|
||||||
onUpdate: (value: GQL.SceneMovieInput[]) => void;
|
onUpdate: (input: IMovieEntry[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneMovieTable: React.FC<IProps> = (props) => {
|
export const SceneMovieTable: React.FC<IProps> = (props) => {
|
||||||
|
const { value, onUpdate } = props;
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const { data } = useAllMoviesForFilter();
|
|
||||||
|
|
||||||
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
const movieIDs = useMemo(() => value.map((m) => m.movie.id), [value]);
|
||||||
|
|
||||||
const movieEntries = props.movieScenes.map((m) => {
|
const updateFieldChanged = (index: number, sceneIndex: number | null) => {
|
||||||
|
const newValues = value.map((existing, i) => {
|
||||||
|
if (i === index) {
|
||||||
return {
|
return {
|
||||||
movie: items.find((mm) => m.movie_id === mm.id),
|
...existing,
|
||||||
...m,
|
scene_index: sceneIndex,
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateFieldChanged = (movieId: string, value: number) => {
|
|
||||||
const newValues = props.movieScenes.map((ms) => {
|
|
||||||
if (ms.movie_id === movieId) {
|
|
||||||
return {
|
|
||||||
movie_id: movieId,
|
|
||||||
scene_index: value,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return ms;
|
return existing;
|
||||||
});
|
});
|
||||||
props.onUpdate(newValues);
|
|
||||||
|
onUpdate(newValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function onMovieSet(index: number, movies: Movie[]) {
|
||||||
|
if (!movies.length) {
|
||||||
|
// remove this entry
|
||||||
|
const newValues = value.filter((_, i) => i !== index);
|
||||||
|
onUpdate(newValues);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movie = movies[0];
|
||||||
|
|
||||||
|
const newValues = value.map((existing, i) => {
|
||||||
|
if (i === index) {
|
||||||
|
return {
|
||||||
|
...existing,
|
||||||
|
movie: movie,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return existing;
|
||||||
|
});
|
||||||
|
|
||||||
|
onUpdate(newValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onNewMovieSet(movies: Movie[]) {
|
||||||
|
if (!movies.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movie = movies[0];
|
||||||
|
|
||||||
|
const newValues = [
|
||||||
|
...value,
|
||||||
|
{
|
||||||
|
movie: movie,
|
||||||
|
scene_index: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
onUpdate(newValues);
|
||||||
|
}
|
||||||
|
|
||||||
function renderTableData() {
|
function renderTableData() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{movieEntries.map((m) => (
|
{value.map((m, i) => (
|
||||||
<Row key={m.movie_id}>
|
<Row key={m.movie.id} className="movie-row">
|
||||||
<Form.Label column xs={9}>
|
<Col xs={9}>
|
||||||
{m.movie?.name ?? ""}
|
<MovieSelect
|
||||||
</Form.Label>
|
onSelect={(items) => onMovieSet(i, items)}
|
||||||
|
values={[m.movie!]}
|
||||||
|
excludeIds={movieIDs}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
<Col xs={3}>
|
<Col xs={3}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
@@ -52,36 +98,38 @@ export const SceneMovieTable: React.FC<IProps> = (props) => {
|
|||||||
value={m.scene_index ?? ""}
|
value={m.scene_index ?? ""}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
updateFieldChanged(
|
updateFieldChanged(
|
||||||
m.movie_id,
|
i,
|
||||||
Number.parseInt(
|
e.currentTarget.value === ""
|
||||||
e.currentTarget.value ? e.currentTarget.value : "0",
|
? null
|
||||||
10
|
: Number.parseInt(e.currentTarget.value, 10)
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
))}
|
))}
|
||||||
|
<Row className="movie-row">
|
||||||
|
<Col xs={12}>
|
||||||
|
<MovieSelect
|
||||||
|
onSelect={(items) => onNewMovieSet(items)}
|
||||||
|
values={[]}
|
||||||
|
excludeIds={movieIDs}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.movieScenes.length > 0) {
|
|
||||||
return (
|
return (
|
||||||
<div className="movie-table">
|
<div className={cx("movie-table", { "no-movies": !value.length })}>
|
||||||
<Row>
|
<Row className="movie-table-header">
|
||||||
<Form.Label column xs={9}>
|
<Col xs={9}></Col>
|
||||||
{intl.formatMessage({ id: "movie" })}
|
<Form.Label column xs={3} className="movie-scene-number-header">
|
||||||
</Form.Label>
|
|
||||||
<Form.Label column xs={3}>
|
|
||||||
{intl.formatMessage({ id: "movie_scene_number" })}
|
{intl.formatMessage({ id: "movie_scene_number" })}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
</Row>
|
</Row>
|
||||||
{renderTableData()}
|
{renderTableData()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return <></>;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import {
|
|||||||
ScrapedImageRow,
|
ScrapedImageRow,
|
||||||
ScrapedStringListRow,
|
ScrapedStringListRow,
|
||||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import clone from "lodash-es/clone";
|
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { uniq } from "lodash-es";
|
import { uniq } from "lodash-es";
|
||||||
import { Performer } from "src/components/Performers/PerformerSelect";
|
import { Performer } from "src/components/Performers/PerformerSelect";
|
||||||
import { IHasStoredID, sortStoredIdObjects } from "src/utils/data";
|
import { sortStoredIdObjects } from "src/utils/data";
|
||||||
import {
|
import {
|
||||||
ObjectListScrapeResult,
|
ObjectListScrapeResult,
|
||||||
ObjectScrapeResult,
|
ObjectScrapeResult,
|
||||||
@@ -31,12 +30,14 @@ import {
|
|||||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||||
import { Tag } from "src/components/Tags/TagSelect";
|
import { Tag } from "src/components/Tags/TagSelect";
|
||||||
import { Studio } from "src/components/Studios/StudioSelect";
|
import { Studio } from "src/components/Studios/StudioSelect";
|
||||||
|
import { Movie } from "src/components/Movies/MovieSelect";
|
||||||
|
|
||||||
interface ISceneScrapeDialogProps {
|
interface ISceneScrapeDialogProps {
|
||||||
scene: Partial<GQL.SceneUpdateInput>;
|
scene: Partial<GQL.SceneUpdateInput>;
|
||||||
sceneStudio: Studio | null;
|
sceneStudio: Studio | null;
|
||||||
scenePerformers: Performer[];
|
scenePerformers: Performer[];
|
||||||
sceneTags: Tag[];
|
sceneTags: Tag[];
|
||||||
|
sceneMovies: Movie[];
|
||||||
scraped: GQL.ScrapedScene;
|
scraped: GQL.ScrapedScene;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
sceneStudio,
|
sceneStudio,
|
||||||
scenePerformers,
|
scenePerformers,
|
||||||
sceneTags,
|
sceneTags,
|
||||||
|
sceneMovies,
|
||||||
scraped,
|
scraped,
|
||||||
onClose,
|
onClose,
|
||||||
endpoint,
|
endpoint,
|
||||||
@@ -96,44 +98,6 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
function mapStoredIdObjects(
|
|
||||||
scrapedObjects?: IHasStoredID[]
|
|
||||||
): string[] | undefined {
|
|
||||||
if (!scrapedObjects) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const ret = scrapedObjects
|
|
||||||
.map((p) => p.stored_id)
|
|
||||||
.filter((p) => {
|
|
||||||
return p !== undefined && p !== null;
|
|
||||||
}) as string[];
|
|
||||||
|
|
||||||
if (ret.length === 0) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort by id numerically
|
|
||||||
ret.sort((a, b) => {
|
|
||||||
return parseInt(a, 10) - parseInt(b, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sortIdList(idList?: string[] | null) {
|
|
||||||
if (!idList) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ret = clone(idList);
|
|
||||||
// sort by id numerically
|
|
||||||
ret.sort((a, b) => {
|
|
||||||
return parseInt(a, 10) - parseInt(b, 10);
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [performers, setPerformers] = useState<
|
const [performers, setPerformers] = useState<
|
||||||
ObjectListScrapeResult<GQL.ScrapedPerformer>
|
ObjectListScrapeResult<GQL.ScrapedPerformer>
|
||||||
>(
|
>(
|
||||||
@@ -151,10 +115,17 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
scraped.performers?.filter((t) => !t.stored_id) ?? []
|
scraped.performers?.filter((t) => !t.stored_id) ?? []
|
||||||
);
|
);
|
||||||
|
|
||||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
const [movies, setMovies] = useState<
|
||||||
new ScrapeResult<string[]>(
|
ObjectListScrapeResult<GQL.ScrapedMovie>
|
||||||
sortIdList(scene.movies?.map((p) => p.movie_id)),
|
>(
|
||||||
mapStoredIdObjects(scraped.movies ?? undefined)
|
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
||||||
|
sortStoredIdObjects(
|
||||||
|
sceneMovies.map((p) => ({
|
||||||
|
stored_id: p.id,
|
||||||
|
name: p.name,
|
||||||
|
}))
|
||||||
|
),
|
||||||
|
sortStoredIdObjects(scraped.movies ?? undefined)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [newMovies, setNewMovies] = useState<GQL.ScrapedMovie[]>(
|
const [newMovies, setNewMovies] = useState<GQL.ScrapedMovie[]>(
|
||||||
@@ -249,12 +220,7 @@ export const SceneScrapeDialog: React.FC<ISceneScrapeDialogProps> = ({
|
|||||||
director: director.getNewValue(),
|
director: director.getNewValue(),
|
||||||
studio: newStudioValue,
|
studio: newStudioValue,
|
||||||
performers: performers.getNewValue(),
|
performers: performers.getNewValue(),
|
||||||
movies: movies.getNewValue()?.map((m) => {
|
movies: movies.getNewValue(),
|
||||||
return {
|
|
||||||
stored_id: m,
|
|
||||||
name: "",
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
tags: tags.getNewValue(),
|
tags: tags.getNewValue(),
|
||||||
details: details.getNewValue(),
|
details: details.getNewValue(),
|
||||||
image: image.getNewValue(),
|
image: image.getNewValue(),
|
||||||
|
|||||||
@@ -93,6 +93,13 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function movieToStoredID(o: { movie: { id: string; name: string } }) {
|
||||||
|
return {
|
||||||
|
stored_id: o.movie.id,
|
||||||
|
name: o.movie.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const [studio, setStudio] = useState<ScrapeResult<GQL.ScrapedStudio>>(
|
const [studio, setStudio] = useState<ScrapeResult<GQL.ScrapedStudio>>(
|
||||||
new ScrapeResult<GQL.ScrapedStudio>(
|
new ScrapeResult<GQL.ScrapedStudio>(
|
||||||
dest.studio ? idToStoredID(dest.studio) : undefined
|
dest.studio ? idToStoredID(dest.studio) : undefined
|
||||||
@@ -127,8 +134,12 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [movies, setMovies] = useState<ScrapeResult<string[]>>(
|
const [movies, setMovies] = useState<
|
||||||
new ScrapeResult<string[]>(sortIdList(dest.movies.map((p) => p.movie.id)))
|
ObjectListScrapeResult<GQL.ScrapedMovie>
|
||||||
|
>(
|
||||||
|
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
||||||
|
sortStoredIdObjects(dest.movies.map(movieToStoredID))
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [tags, setTags] = useState<ObjectListScrapeResult<GQL.ScrapedTag>>(
|
const [tags, setTags] = useState<ObjectListScrapeResult<GQL.ScrapedTag>>(
|
||||||
@@ -235,9 +246,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
setMovies(
|
setMovies(
|
||||||
new ScrapeResult(
|
new ObjectListScrapeResult<GQL.ScrapedMovie>(
|
||||||
dest.movies.map((m) => m.movie.id),
|
sortStoredIdObjects(dest.movies.map(movieToStoredID)),
|
||||||
uniq(all.map((s) => s.movies.map((m) => m.movie.id)).flat())
|
uniqIDStoredIDs(all.map((s) => s.movies.map(movieToStoredID)).flat())
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -571,9 +582,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
|
|||||||
const found = all
|
const found = all
|
||||||
.map((s) => s.movies)
|
.map((s) => s.movies)
|
||||||
.flat()
|
.flat()
|
||||||
.find((mm) => mm.movie.id === m);
|
.find((mm) => mm.movie.id === m.stored_id);
|
||||||
return {
|
return {
|
||||||
movie_id: m,
|
movie_id: m.stored_id!,
|
||||||
scene_index: found!.scene_index,
|
scene_index: found!.scene_index,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -237,9 +237,21 @@ textarea.scene-description {
|
|||||||
.movie-table {
|
.movie-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
td {
|
.movie-row {
|
||||||
vertical-align: middle;
|
align-items: center;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movie-scene-number-header {
|
||||||
|
color: $text-muted;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding-bottom: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.movie-table.no-movies .movie-table-header {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scene-tabs {
|
.scene-tabs {
|
||||||
|
|||||||
@@ -25,16 +25,11 @@ import { StringListInput } from "../StringListInput";
|
|||||||
import { ImageSelector } from "../ImageSelector";
|
import { ImageSelector } from "../ImageSelector";
|
||||||
import { ScrapeResult } from "./scrapeResult";
|
import { ScrapeResult } from "./scrapeResult";
|
||||||
|
|
||||||
export interface IHasName {
|
|
||||||
name: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IScrapedFieldProps<T> {
|
interface IScrapedFieldProps<T> {
|
||||||
result: ScrapeResult<T>;
|
result: ScrapeResult<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IScrapedRowProps<T, V extends IHasName>
|
interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> {
|
||||||
extends IScrapedFieldProps<T> {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
title: string;
|
title: string;
|
||||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||||
@@ -42,6 +37,7 @@ interface IScrapedRowProps<T, V extends IHasName>
|
|||||||
onChange: (value: ScrapeResult<T>) => void;
|
onChange: (value: ScrapeResult<T>) => void;
|
||||||
newValues?: V[];
|
newValues?: V[];
|
||||||
onCreateNew?: (index: number) => void;
|
onCreateNew?: (index: number) => void;
|
||||||
|
getName?: (value: V) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderButtonIcon(selected: boolean) {
|
function renderButtonIcon(selected: boolean) {
|
||||||
@@ -55,9 +51,9 @@ function renderButtonIcon(selected: boolean) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrapeDialogRow = <T, V extends IHasName>(
|
export const ScrapeDialogRow = <T, V>(props: IScrapedRowProps<T, V>) => {
|
||||||
props: IScrapedRowProps<T, V>
|
const { getName = () => "" } = props;
|
||||||
) => {
|
|
||||||
function handleSelectClick(isNew: boolean) {
|
function handleSelectClick(isNew: boolean) {
|
||||||
const ret = clone(props.result);
|
const ret = clone(props.result);
|
||||||
ret.useNewValue = isNew;
|
ret.useNewValue = isNew;
|
||||||
@@ -83,10 +79,10 @@ export const ScrapeDialogRow = <T, V extends IHasName>(
|
|||||||
<Badge
|
<Badge
|
||||||
className="tag-item"
|
className="tag-item"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
key={t.name}
|
key={getName(t)}
|
||||||
onClick={() => props.onCreateNew!(i)}
|
onClick={() => props.onCreateNew!(i)}
|
||||||
>
|
>
|
||||||
{t.name}
|
{getName(t)}
|
||||||
<Button className="minimal ml-2">
|
<Button className="minimal ml-2">
|
||||||
<Icon className="fa-fw" icon={faPlus} />
|
<Icon className="fa-fw" icon={faPlus} />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -173,6 +169,10 @@ const ScrapedInputGroup: React.FC<IScrapedInputGroupProps> = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getNameString(value: string) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
interface IScrapedInputGroupRowProps {
|
interface IScrapedInputGroupRowProps {
|
||||||
title: string;
|
title: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -206,6 +206,7 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
getName={getNameString}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -271,6 +272,7 @@ export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
getName={getNameString}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -316,6 +318,7 @@ export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
getName={getNameString}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -369,6 +372,7 @@ export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
getName={getNameString}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -412,6 +416,7 @@ export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
|
getName={getNameString}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -514,5 +519,6 @@ export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
getName={getNameString}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { MovieSelect } from "src/components/Shared/Select";
|
import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||||
import {
|
|
||||||
ScrapeDialogRow,
|
|
||||||
IHasName,
|
|
||||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
|
||||||
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
import { PerformerSelect } from "src/components/Performers/PerformerSelect";
|
||||||
import {
|
import {
|
||||||
ObjectScrapeResult,
|
ObjectScrapeResult,
|
||||||
@@ -12,6 +8,7 @@ import {
|
|||||||
} from "src/components/Shared/ScrapeDialog/scrapeResult";
|
} from "src/components/Shared/ScrapeDialog/scrapeResult";
|
||||||
import { TagSelect } from "src/components/Tags/TagSelect";
|
import { TagSelect } from "src/components/Tags/TagSelect";
|
||||||
import { StudioSelect } from "src/components/Studios/StudioSelect";
|
import { StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
|
import { MovieSelect } from "src/components/Movies/MovieSelect";
|
||||||
|
|
||||||
interface IScrapedStudioRow {
|
interface IScrapedStudioRow {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -21,6 +18,10 @@ interface IScrapedStudioRow {
|
|||||||
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
onCreateNew?: (value: GQL.ScrapedStudio) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getObjectName<T extends { name: string }>(value: T) {
|
||||||
|
return value.name;
|
||||||
|
}
|
||||||
|
|
||||||
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
||||||
title,
|
title,
|
||||||
result,
|
result,
|
||||||
@@ -76,28 +77,35 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
|
|||||||
onCreateNew={() => {
|
onCreateNew={() => {
|
||||||
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
if (onCreateNew && newStudio) onCreateNew(newStudio);
|
||||||
}}
|
}}
|
||||||
|
getName={getObjectName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IScrapedObjectsRow<T, R> {
|
interface IScrapedObjectsRow<T> {
|
||||||
title: string;
|
title: string;
|
||||||
result: ScrapeResult<R[]>;
|
result: ScrapeResult<T[]>;
|
||||||
onChange: (value: ScrapeResult<R[]>) => void;
|
onChange: (value: ScrapeResult<T[]>) => void;
|
||||||
newObjects?: T[];
|
newObjects?: T[];
|
||||||
onCreateNew?: (value: T) => void;
|
onCreateNew?: (value: T) => void;
|
||||||
renderObjects: (
|
renderObjects: (
|
||||||
result: ScrapeResult<R[]>,
|
result: ScrapeResult<T[]>,
|
||||||
isNew?: boolean,
|
isNew?: boolean,
|
||||||
onChange?: (value: R[]) => void
|
onChange?: (value: T[]) => void
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
|
getName: (value: T) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrapedObjectsRow = <T extends IHasName, R>(
|
export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
|
||||||
props: IScrapedObjectsRow<T, R>
|
const {
|
||||||
) => {
|
title,
|
||||||
const { title, result, onChange, newObjects, onCreateNew, renderObjects } =
|
result,
|
||||||
props;
|
onChange,
|
||||||
|
newObjects,
|
||||||
|
onCreateNew,
|
||||||
|
renderObjects,
|
||||||
|
getName,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapeDialogRow
|
<ScrapeDialogRow
|
||||||
@@ -114,17 +122,18 @@ export const ScrapedObjectsRow = <T extends IHasName, R>(
|
|||||||
onCreateNew={(i) => {
|
onCreateNew={(i) => {
|
||||||
if (onCreateNew) onCreateNew(newObjects![i]);
|
if (onCreateNew) onCreateNew(newObjects![i]);
|
||||||
}}
|
}}
|
||||||
|
getName={getName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type IScrapedObjectRowImpl<T, R> = Omit<
|
type IScrapedObjectRowImpl<T> = Omit<
|
||||||
IScrapedObjectsRow<T, R>,
|
IScrapedObjectsRow<T>,
|
||||||
"renderObjects"
|
"renderObjects" | "getName"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const ScrapedPerformersRow: React.FC<
|
export const ScrapedPerformersRow: React.FC<
|
||||||
IScrapedObjectRowImpl<GQL.ScrapedPerformer, GQL.ScrapedPerformer>
|
IScrapedObjectRowImpl<GQL.ScrapedPerformer>
|
||||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||||
const performersCopy = useMemo(() => {
|
const performersCopy = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -170,24 +179,21 @@ export const ScrapedPerformersRow: React.FC<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type PerformerType = GQL.ScrapedPerformer & {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapedObjectsRow<PerformerType, GQL.ScrapedPerformer>
|
<ScrapedObjectsRow<GQL.ScrapedPerformer>
|
||||||
title={title}
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderObjects={renderScrapedPerformers}
|
renderObjects={renderScrapedPerformers}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
newObjects={performersCopy}
|
newObjects={performersCopy}
|
||||||
onCreateNew={onCreateNew}
|
onCreateNew={onCreateNew}
|
||||||
|
getName={(value) => value.name ?? ""}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScrapedMoviesRow: React.FC<
|
export const ScrapedMoviesRow: React.FC<
|
||||||
IScrapedObjectRowImpl<GQL.ScrapedMovie, string>
|
IScrapedObjectRowImpl<GQL.ScrapedMovie>
|
||||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||||
const moviesCopy = useMemo(() => {
|
const moviesCopy = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
@@ -198,20 +204,25 @@ export const ScrapedMoviesRow: React.FC<
|
|||||||
);
|
);
|
||||||
}, [newObjects]);
|
}, [newObjects]);
|
||||||
|
|
||||||
type MovieType = GQL.ScrapedMovie & {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderScrapedMovies(
|
function renderScrapedMovies(
|
||||||
scrapeResult: ScrapeResult<string[]>,
|
scrapeResult: ScrapeResult<GQL.ScrapedMovie[]>,
|
||||||
isNew?: boolean,
|
isNew?: boolean,
|
||||||
onChangeFn?: (value: string[]) => void
|
onChangeFn?: (value: GQL.ScrapedMovie[]) => void
|
||||||
) {
|
) {
|
||||||
const resultValue = isNew
|
const resultValue = isNew
|
||||||
? scrapeResult.newValue
|
? scrapeResult.newValue
|
||||||
: scrapeResult.originalValue;
|
: scrapeResult.originalValue;
|
||||||
const value = resultValue ?? [];
|
const value = resultValue ?? [];
|
||||||
|
|
||||||
|
const selectValue = value.map((p) => {
|
||||||
|
const aliases: string[] = [];
|
||||||
|
return {
|
||||||
|
id: p.stored_id ?? "",
|
||||||
|
name: p.name ?? "",
|
||||||
|
aliases,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieSelect
|
<MovieSelect
|
||||||
isMulti
|
isMulti
|
||||||
@@ -219,28 +230,30 @@ export const ScrapedMoviesRow: React.FC<
|
|||||||
isDisabled={!isNew}
|
isDisabled={!isNew}
|
||||||
onSelect={(items) => {
|
onSelect={(items) => {
|
||||||
if (onChangeFn) {
|
if (onChangeFn) {
|
||||||
onChangeFn(items.map((i) => i.id));
|
// map the id back to stored_id
|
||||||
|
onChangeFn(items.map((p) => ({ ...p, stored_id: p.id })));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
ids={value}
|
values={selectValue}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapedObjectsRow<MovieType, string>
|
<ScrapedObjectsRow<GQL.ScrapedMovie>
|
||||||
title={title}
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderObjects={renderScrapedMovies}
|
renderObjects={renderScrapedMovies}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
newObjects={moviesCopy}
|
newObjects={moviesCopy}
|
||||||
onCreateNew={onCreateNew}
|
onCreateNew={onCreateNew}
|
||||||
|
getName={(value) => value.name ?? ""}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ScrapedTagsRow: React.FC<
|
export const ScrapedTagsRow: React.FC<
|
||||||
IScrapedObjectRowImpl<GQL.ScrapedTag, GQL.ScrapedTag>
|
IScrapedObjectRowImpl<GQL.ScrapedTag>
|
||||||
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
> = ({ title, result, onChange, newObjects, onCreateNew }) => {
|
||||||
function renderScrapedTags(
|
function renderScrapedTags(
|
||||||
scrapeResult: ScrapeResult<GQL.ScrapedTag[]>,
|
scrapeResult: ScrapeResult<GQL.ScrapedTag[]>,
|
||||||
@@ -278,13 +291,14 @@ export const ScrapedTagsRow: React.FC<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrapedObjectsRow<GQL.ScrapedTag, GQL.ScrapedTag>
|
<ScrapedObjectsRow<GQL.ScrapedTag>
|
||||||
title={title}
|
title={title}
|
||||||
result={result}
|
result={result}
|
||||||
renderObjects={renderScrapedTags}
|
renderObjects={renderScrapedTags}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
newObjects={newObjects}
|
newObjects={newObjects}
|
||||||
onCreateNew={onCreateNew}
|
onCreateNew={onCreateNew}
|
||||||
|
getName={getObjectName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -123,62 +123,41 @@ export function useCreateScrapedPerformer(
|
|||||||
return useCreateObject("performer", createNewPerformer);
|
return useCreateObject("performer", createNewPerformer);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUseCreateNewObjectIDListProps<
|
export function useCreateScrapedMovie(
|
||||||
T extends { name?: string | undefined | null }
|
props: IUseCreateNewObjectProps<GQL.ScrapedMovie>
|
||||||
> {
|
|
||||||
scrapeResult: ScrapeResult<string[]>;
|
|
||||||
setScrapeResult: (scrapeResult: ScrapeResult<string[]>) => void;
|
|
||||||
newObjects: T[];
|
|
||||||
setNewObjects: (newObject: T[]) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useCreateNewObjectIDList<
|
|
||||||
T extends { name?: string | undefined | null }
|
|
||||||
>(
|
|
||||||
entityTypeID: string,
|
|
||||||
props: IUseCreateNewObjectIDListProps<T>,
|
|
||||||
createObject: (toCreate: T) => Promise<string>
|
|
||||||
) {
|
) {
|
||||||
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
const { scrapeResult, setScrapeResult, newObjects, setNewObjects } = props;
|
||||||
|
const [createMovie] = useMovieCreate();
|
||||||
|
|
||||||
async function createNewObject(toCreate: T) {
|
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
||||||
const newID = await createObject(toCreate);
|
const input = scrapedMovieToCreateInput(toCreate);
|
||||||
|
|
||||||
// add the new object to the new objects value
|
const result = await createMovie({
|
||||||
const newResult = scrapeResult.cloneWithValue(scrapeResult.newValue);
|
variables: { input: input },
|
||||||
if (!newResult.newValue) {
|
});
|
||||||
newResult.newValue = [];
|
|
||||||
}
|
const newValue = [...(scrapeResult.newValue ?? [])];
|
||||||
newResult.newValue.push(newID);
|
if (result.data?.movieCreate)
|
||||||
setScrapeResult(newResult);
|
newValue.push({
|
||||||
|
stored_id: result.data.movieCreate.id,
|
||||||
|
name: result.data.movieCreate.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add the new object to the new object value
|
||||||
|
const resultClone = scrapeResult.cloneWithValue(newValue);
|
||||||
|
setScrapeResult(resultClone);
|
||||||
|
|
||||||
// remove the object from the list
|
// remove the object from the list
|
||||||
const newObjectsClone = newObjects.concat();
|
const newObjectsClone = newObjects.concat();
|
||||||
const pIndex = newObjectsClone.findIndex((p) => p.name === toCreate.name);
|
const pIndex = newObjectsClone.findIndex((p) => p.name === toCreate.name);
|
||||||
if (pIndex === -1) throw new Error("Could not find object to remove");
|
if (pIndex === -1) throw new Error("Could not find movie to remove");
|
||||||
|
|
||||||
newObjectsClone.splice(pIndex, 1);
|
newObjectsClone.splice(pIndex, 1);
|
||||||
|
|
||||||
setNewObjects(newObjectsClone);
|
setNewObjects(newObjectsClone);
|
||||||
}
|
}
|
||||||
|
|
||||||
return useCreateObject(entityTypeID, createNewObject);
|
return useCreateObject("movie", createNewMovie);
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateScrapedMovie(
|
|
||||||
props: IUseCreateNewObjectIDListProps<GQL.ScrapedMovie>
|
|
||||||
) {
|
|
||||||
const [createMovie] = useMovieCreate();
|
|
||||||
|
|
||||||
async function createNewMovie(toCreate: GQL.ScrapedMovie) {
|
|
||||||
const movieInput = scrapedMovieToCreateInput(toCreate);
|
|
||||||
const result = await createMovie({
|
|
||||||
variables: { input: movieInput },
|
|
||||||
});
|
|
||||||
|
|
||||||
return result.data?.movieCreate?.id ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return useCreateNewObjectIDList("movie", props, createNewMovie);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCreateScrapedTag(
|
export function useCreateScrapedTag(
|
||||||
|
|||||||
@@ -13,15 +13,9 @@ import Select, {
|
|||||||
import CreatableSelect from "react-select/creatable";
|
import CreatableSelect from "react-select/creatable";
|
||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import {
|
import { useMarkerStrings } from "src/core/StashService";
|
||||||
useAllMoviesForFilter,
|
|
||||||
useMarkerStrings,
|
|
||||||
useMovieCreate,
|
|
||||||
} from "src/core/StashService";
|
|
||||||
import { useToast } from "src/hooks/Toast";
|
|
||||||
import { SelectComponents } from "react-select/dist/declarations/src/components";
|
import { SelectComponents } from "react-select/dist/declarations/src/components";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { useIntl } from "react-intl";
|
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
import { defaultMaxOptionsShown } from "src/core/config";
|
import { defaultMaxOptionsShown } from "src/core/config";
|
||||||
import { useDebounce } from "src/hooks/debounce";
|
import { useDebounce } from "src/hooks/debounce";
|
||||||
@@ -32,6 +26,7 @@ import { faTableColumns } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { TagIDSelect } from "../Tags/TagSelect";
|
import { TagIDSelect } from "../Tags/TagSelect";
|
||||||
import { StudioIDSelect } from "../Studios/StudioSelect";
|
import { StudioIDSelect } from "../Studios/StudioSelect";
|
||||||
import { GalleryIDSelect } from "../Galleries/GallerySelect";
|
import { GalleryIDSelect } from "../Galleries/GallerySelect";
|
||||||
|
import { MovieIDSelect } from "../Movies/MovieSelect";
|
||||||
|
|
||||||
export type SelectObject = {
|
export type SelectObject = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -89,23 +84,6 @@ interface ISelectProps<T extends boolean> {
|
|||||||
closeMenuOnSelect?: boolean;
|
closeMenuOnSelect?: boolean;
|
||||||
noOptionsMessage?: string | null;
|
noOptionsMessage?: string | null;
|
||||||
}
|
}
|
||||||
interface IFilterComponentProps extends IFilterProps {
|
|
||||||
items: SelectObject[];
|
|
||||||
toOption?: (item: SelectObject) => Option;
|
|
||||||
onCreate?: (name: string) => Promise<{ item: SelectObject; message: string }>;
|
|
||||||
}
|
|
||||||
interface IFilterSelectProps<T extends boolean>
|
|
||||||
extends Pick<
|
|
||||||
ISelectProps<T>,
|
|
||||||
| "isLoading"
|
|
||||||
| "isMulti"
|
|
||||||
| "components"
|
|
||||||
| "filterOption"
|
|
||||||
| "isValidNewOption"
|
|
||||||
| "placeholder"
|
|
||||||
| "closeMenuOnSelect"
|
|
||||||
> {}
|
|
||||||
|
|
||||||
type TitledObject = { id: string; title: string };
|
type TitledObject = { id: string; title: string };
|
||||||
interface ITitledSelect {
|
interface ITitledSelect {
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -125,9 +103,6 @@ const getSelectedItems = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getSelectedValues = (selectedItems: OnChangeValue<Option, boolean>) =>
|
|
||||||
getSelectedItems(selectedItems).map((item) => item.value);
|
|
||||||
|
|
||||||
const LimitedSelectMenu = <T extends boolean>(
|
const LimitedSelectMenu = <T extends boolean>(
|
||||||
props: MenuListProps<Option, T, GroupBase<Option>>
|
props: MenuListProps<Option, T, GroupBase<Option>>
|
||||||
) => {
|
) => {
|
||||||
@@ -273,67 +248,6 @@ const SelectComponent = <T extends boolean>({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const FilterSelectComponent = <T extends boolean>(
|
|
||||||
props: IFilterComponentProps & ITypeProps & IFilterSelectProps<T>
|
|
||||||
) => {
|
|
||||||
const { items, ids, isMulti, onSelect } = props;
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const selectedIds = ids ?? [];
|
|
||||||
const Toast = useToast();
|
|
||||||
|
|
||||||
const options = items.map((i) => {
|
|
||||||
if (props.toOption) {
|
|
||||||
return props.toOption(i);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
value: i.id,
|
|
||||||
label: i.name ?? i.title ?? "",
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const selected = options.filter((option) =>
|
|
||||||
selectedIds.includes(option.value)
|
|
||||||
);
|
|
||||||
const selectedOptions = (
|
|
||||||
isMulti ? selected : selected[0] ?? null
|
|
||||||
) as OnChangeValue<Option, T>;
|
|
||||||
|
|
||||||
const onChange = (selectedItems: OnChangeValue<Option, boolean>) => {
|
|
||||||
const selectedValues = getSelectedValues(selectedItems);
|
|
||||||
onSelect?.(items.filter((item) => selectedValues.includes(item.id)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCreate = async (name: string) => {
|
|
||||||
try {
|
|
||||||
setLoading(true);
|
|
||||||
const { item: newItem, message } = await props.onCreate!(name);
|
|
||||||
props.onSelect?.([
|
|
||||||
...items.filter((item) => selectedIds.includes(item.id)),
|
|
||||||
newItem,
|
|
||||||
]);
|
|
||||||
setLoading(false);
|
|
||||||
Toast.success(
|
|
||||||
<span>
|
|
||||||
{message}: <b>{name}</b>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SelectComponent<T>
|
|
||||||
{...props}
|
|
||||||
isLoading={props.isLoading || loading}
|
|
||||||
onChange={onChange}
|
|
||||||
items={options}
|
|
||||||
selectedOptions={selectedOptions}
|
|
||||||
onCreateOption={props.creatable ? onCreate : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GallerySelect: React.FC<
|
export const GallerySelect: React.FC<
|
||||||
IFilterProps & { excludeIds?: string[] }
|
IFilterProps & { excludeIds?: string[] }
|
||||||
> = (props) => {
|
> = (props) => {
|
||||||
@@ -493,50 +407,7 @@ export const StudioSelect: React.FC<
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
export const MovieSelect: React.FC<IFilterProps> = (props) => {
|
||||||
const { data, loading } = useAllMoviesForFilter();
|
return <MovieIDSelect {...props} />;
|
||||||
const [createMovie] = useMovieCreate();
|
|
||||||
const items = data?.allMovies ?? [];
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
|
||||||
const defaultCreatable =
|
|
||||||
!configuration?.interface.disableDropdownCreate.movie ?? true;
|
|
||||||
|
|
||||||
const onCreate = async (name: string) => {
|
|
||||||
const result = await createMovie({
|
|
||||||
variables: { input: { name } },
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
item: result.data!.movieCreate!,
|
|
||||||
message: intl.formatMessage(
|
|
||||||
{ id: "toast.created_entity" },
|
|
||||||
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FilterSelectComponent
|
|
||||||
{...props}
|
|
||||||
isMulti={props.isMulti ?? false}
|
|
||||||
type="movies"
|
|
||||||
isLoading={loading}
|
|
||||||
items={items}
|
|
||||||
placeholder={
|
|
||||||
props.noSelectionString ??
|
|
||||||
intl.formatMessage(
|
|
||||||
{ id: "actions.select_entity" },
|
|
||||||
{
|
|
||||||
entityType: intl.formatMessage({
|
|
||||||
id: props.isMulti ? "movies" : "movie",
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
creatable={props.creatable ?? defaultCreatable}
|
|
||||||
onCreate={onCreate}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TagSelect: React.FC<
|
export const TagSelect: React.FC<
|
||||||
|
|||||||
@@ -233,8 +233,7 @@ const _StudioIDSelect: React.FC<IFilterProps & IFilterIDProps<Studio>> = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Studio[]> {
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Studio[]> {
|
||||||
const studioIDs = idsToLoad.map((id) => parseInt(id));
|
const query = await queryFindStudiosByIDForSelect(idsToLoad);
|
||||||
const query = await queryFindStudiosByIDForSelect(studioIDs);
|
|
||||||
const { studios: loadedStudios } = query.data.findStudios;
|
const { studios: loadedStudios } = query.data.findStudios;
|
||||||
|
|
||||||
return loadedStudios;
|
return loadedStudios;
|
||||||
|
|||||||
@@ -251,8 +251,7 @@ const _TagIDSelect: React.FC<IFilterProps & IFilterIDProps<Tag>> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectsByID(idsToLoad: string[]): Promise<Tag[]> {
|
async function loadObjectsByID(idsToLoad: string[]): Promise<Tag[]> {
|
||||||
const tagIDs = idsToLoad.map((id) => parseInt(id));
|
const query = await queryFindTagsByIDForSelect(idsToLoad);
|
||||||
const query = await queryFindTagsByIDForSelect(tagIDs);
|
|
||||||
const { tags: loadedTags } = query.data.findTags;
|
const { tags: loadedTags } = query.data.findTags;
|
||||||
|
|
||||||
return loadedTags;
|
return loadedTags;
|
||||||
|
|||||||
@@ -73,26 +73,6 @@ function evictTypeFields(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Appends obj to the cached result of the given query.
|
|
||||||
// Use to append objects to "All*" queries in "Create" mutations.
|
|
||||||
function appendObject(
|
|
||||||
cache: ApolloCache<unknown>,
|
|
||||||
obj: StoreObject,
|
|
||||||
query: DocumentNode
|
|
||||||
) {
|
|
||||||
const field = getQueryDefinition(query).selectionSet.selections[0];
|
|
||||||
if (!isField(field)) return;
|
|
||||||
const keyName = field.name.value;
|
|
||||||
|
|
||||||
cache.modify({
|
|
||||||
fields: {
|
|
||||||
[keyName]: (value, { toReference }) => {
|
|
||||||
return [...(value as unknown[]), toReference(obj)];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deletes obj from the cache, and sets the
|
// Deletes obj from the cache, and sets the
|
||||||
// cached result of the given query to null.
|
// cached result of the given query to null.
|
||||||
// Use with "Destroy" mutations.
|
// Use with "Destroy" mutations.
|
||||||
@@ -199,7 +179,22 @@ export const queryFindMovies = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAllMoviesForFilter = () => GQL.useAllMoviesForFilterQuery();
|
export const queryFindMoviesByIDForSelect = (movieIDs: string[]) =>
|
||||||
|
client.query<GQL.FindMoviesForSelectQuery>({
|
||||||
|
query: GQL.FindMoviesForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
ids: movieIDs,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryFindMoviesForSelect = (filter: ListFilterModel) =>
|
||||||
|
client.query<GQL.FindMoviesForSelectQuery>({
|
||||||
|
query: GQL.FindMoviesForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
filter: filter.makeFindFilter(),
|
||||||
|
movie_filter: filter.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const useFindSceneMarkers = (filter?: ListFilterModel) =>
|
export const useFindSceneMarkers = (filter?: ListFilterModel) =>
|
||||||
GQL.useFindSceneMarkersQuery({
|
GQL.useFindSceneMarkersQuery({
|
||||||
@@ -244,7 +239,16 @@ export const queryFindGalleries = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindGalleriesByIDForSelect = (galleryIDs: number[]) =>
|
export const queryFindGalleriesForSelect = (filter: ListFilterModel) =>
|
||||||
|
client.query<GQL.FindGalleriesForSelectQuery>({
|
||||||
|
query: GQL.FindGalleriesForSelectDocument,
|
||||||
|
variables: {
|
||||||
|
filter: filter.makeFindFilter(),
|
||||||
|
gallery_filter: filter.makeFilter(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const queryFindGalleriesByIDForSelect = (galleryIDs: string[]) =>
|
||||||
client.query<GQL.FindGalleriesForSelectQuery>({
|
client.query<GQL.FindGalleriesForSelectQuery>({
|
||||||
query: GQL.FindGalleriesForSelectDocument,
|
query: GQL.FindGalleriesForSelectDocument,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -281,11 +285,11 @@ export const queryFindPerformers = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindPerformersByIDForSelect = (performerIDs: number[]) =>
|
export const queryFindPerformersByIDForSelect = (performerIDs: string[]) =>
|
||||||
client.query<GQL.FindPerformersForSelectQuery>({
|
client.query<GQL.FindPerformersForSelectQuery>({
|
||||||
query: GQL.FindPerformersForSelectDocument,
|
query: GQL.FindPerformersForSelectDocument,
|
||||||
variables: {
|
variables: {
|
||||||
performer_ids: performerIDs,
|
ids: performerIDs,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -327,7 +331,7 @@ export const queryFindStudios = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindStudiosByIDForSelect = (studioIDs: number[]) =>
|
export const queryFindStudiosByIDForSelect = (studioIDs: string[]) =>
|
||||||
client.query<GQL.FindStudiosForSelectQuery>({
|
client.query<GQL.FindStudiosForSelectQuery>({
|
||||||
query: GQL.FindStudiosForSelectDocument,
|
query: GQL.FindStudiosForSelectDocument,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -367,7 +371,7 @@ export const queryFindTags = (filter: ListFilterModel) =>
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const queryFindTagsByIDForSelect = (tagIDs: number[]) =>
|
export const queryFindTagsByIDForSelect = (tagIDs: string[]) =>
|
||||||
client.query<GQL.FindTagsForSelectQuery>({
|
client.query<GQL.FindTagsForSelectQuery>({
|
||||||
query: GQL.FindTagsForSelectDocument,
|
query: GQL.FindTagsForSelectDocument,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -1072,6 +1076,7 @@ export const mutateImageSetPrimaryFile = (id: string, fileID: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
const movieMutationImpactedTypeFields = {
|
const movieMutationImpactedTypeFields = {
|
||||||
|
Performer: ["movie_count"],
|
||||||
Studio: ["movie_count"],
|
Studio: ["movie_count"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1085,10 +1090,8 @@ export const useMovieCreate = () =>
|
|||||||
const movie = result.data?.movieCreate;
|
const movie = result.data?.movieCreate;
|
||||||
if (!movie) return;
|
if (!movie) return;
|
||||||
|
|
||||||
appendObject(cache, movie, GQL.AllMoviesForFilterDocument);
|
|
||||||
|
|
||||||
// update stats
|
// update stats
|
||||||
updateStats(cache, "studio_count", 1);
|
updateStats(cache, "movie_count", 1);
|
||||||
|
|
||||||
evictTypeFields(cache, movieMutationImpactedTypeFields);
|
evictTypeFields(cache, movieMutationImpactedTypeFields);
|
||||||
evictQueries(cache, movieMutationImpactedQueries);
|
evictQueries(cache, movieMutationImpactedQueries);
|
||||||
|
|||||||
@@ -1073,7 +1073,7 @@
|
|||||||
"megabits_per_second": "{value} mbps",
|
"megabits_per_second": "{value} mbps",
|
||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"movie": "Movie",
|
"movie": "Movie",
|
||||||
"movie_scene_number": "Movie Scene Number",
|
"movie_scene_number": "Scene Number",
|
||||||
"movies": "Movies",
|
"movies": "Movies",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"new": "New",
|
"new": "New",
|
||||||
|
|||||||
Reference in New Issue
Block a user