mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Clear image (#722)
* Allow clearing of tag images * Allow clearing of studio images * Allow clearing of performer images * Allow clearing of movie images * Add filtering for missing images
This commit is contained in:
@@ -89,11 +89,15 @@ input SceneFilterType {
|
|||||||
input MovieFilterType {
|
input MovieFilterType {
|
||||||
"""Filter to only include movies with this studio"""
|
"""Filter to only include movies with this studio"""
|
||||||
studios: MultiCriterionInput
|
studios: MultiCriterionInput
|
||||||
|
"""Filter to only include movies missing this property"""
|
||||||
|
is_missing: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input StudioFilterType {
|
input StudioFilterType {
|
||||||
"""Filter to only include studios with this parent studio"""
|
"""Filter to only include studios with this parent studio"""
|
||||||
parents: MultiCriterionInput
|
parents: MultiCriterionInput
|
||||||
|
"""Filter to only include studios missing this property"""
|
||||||
|
is_missing: String
|
||||||
}
|
}
|
||||||
|
|
||||||
input GalleryFilterType {
|
input GalleryFilterType {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gobuffalo/packr/v2"
|
"github.com/gobuffalo/packr/v2"
|
||||||
|
"github.com/stashapp/stash/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var performerBox *packr.Box
|
var performerBox *packr.Box
|
||||||
@@ -30,3 +31,19 @@ func getRandomPerformerImage(gender string) ([]byte, error) {
|
|||||||
index := rand.Intn(len(imageFiles))
|
index := rand.Intn(len(imageFiles))
|
||||||
return box.Find(imageFiles[index])
|
return box.Find(imageFiles[index])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getRandomPerformerImageUsingName(name, gender string) ([]byte, error) {
|
||||||
|
var box *packr.Box
|
||||||
|
switch strings.ToUpper(gender) {
|
||||||
|
case "FEMALE":
|
||||||
|
box = performerBox
|
||||||
|
case "MALE":
|
||||||
|
box = performerBoxMale
|
||||||
|
default:
|
||||||
|
box = performerBox
|
||||||
|
|
||||||
|
}
|
||||||
|
imageFiles := box.List()
|
||||||
|
index := utils.IntFromString(name) % uint64(len(imageFiles))
|
||||||
|
return box.Find(imageFiles[index])
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,22 +19,27 @@ func (r *mutationResolver) MovieCreate(ctx context.Context, input models.MovieCr
|
|||||||
var backimageData []byte
|
var backimageData []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if input.FrontImage == nil {
|
// HACK: if back image is being set, set the front image to the default.
|
||||||
|
// This is because we can't have a null front image with a non-null back image.
|
||||||
|
if input.FrontImage == nil && input.BackImage != nil {
|
||||||
input.FrontImage = &models.DefaultMovieImage
|
input.FrontImage = &models.DefaultMovieImage
|
||||||
}
|
}
|
||||||
if input.BackImage == nil {
|
|
||||||
input.BackImage = &models.DefaultMovieImage
|
|
||||||
}
|
|
||||||
// Process the base 64 encoded image string
|
// Process the base 64 encoded image string
|
||||||
|
if input.FrontImage != nil {
|
||||||
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process the base 64 encoded image string
|
// Process the base 64 encoded image string
|
||||||
|
if input.BackImage != nil {
|
||||||
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Populate a new movie from the input
|
// Populate a new movie from the input
|
||||||
currentTime := time.Now()
|
currentTime := time.Now()
|
||||||
@@ -114,12 +119,14 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
|||||||
}
|
}
|
||||||
var frontimageData []byte
|
var frontimageData []byte
|
||||||
var err error
|
var err error
|
||||||
|
frontImageIncluded := wasFieldIncluded(ctx, "front_image")
|
||||||
if input.FrontImage != nil {
|
if input.FrontImage != nil {
|
||||||
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
backImageIncluded := wasFieldIncluded(ctx, "back_image")
|
||||||
var backimageData []byte
|
var backimageData []byte
|
||||||
if input.BackImage != nil {
|
if input.BackImage != nil {
|
||||||
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
|
||||||
@@ -185,27 +192,41 @@ func (r *mutationResolver) MovieUpdate(ctx context.Context, input models.MovieUp
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update image table
|
// update image table
|
||||||
if len(frontimageData) > 0 || len(backimageData) > 0 {
|
if frontImageIncluded || backImageIncluded {
|
||||||
if len(frontimageData) == 0 {
|
if !frontImageIncluded {
|
||||||
frontimageData, err = qb.GetFrontImage(updatedMovie.ID, tx)
|
frontimageData, err = qb.GetFrontImage(updatedMovie.ID, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(backimageData) == 0 {
|
if !backImageIncluded {
|
||||||
backimageData, err = qb.GetBackImage(updatedMovie.ID, tx)
|
backimageData, err = qb.GetBackImage(updatedMovie.ID, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(frontimageData) == 0 && len(backimageData) == 0 {
|
||||||
|
// both images are being nulled. Destroy them.
|
||||||
|
if err := qb.DestroyMovieImages(movie.ID, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// HACK - if front image is null and back image is not null, then set the front image
|
||||||
|
// to the default image since we can't have a null front image and a non-null back image
|
||||||
|
if frontimageData == nil && backimageData != nil {
|
||||||
|
_, frontimageData, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||||
|
}
|
||||||
|
|
||||||
if err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil {
|
if err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil {
|
||||||
_ = tx.Rollback()
|
_ = tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Commit
|
// Commit
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
|
|||||||
@@ -18,13 +18,7 @@ func (r *mutationResolver) PerformerCreate(ctx context.Context, input models.Per
|
|||||||
var imageData []byte
|
var imageData []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if input.Image == nil {
|
if input.Image != nil {
|
||||||
gender := ""
|
|
||||||
if input.Gender != nil {
|
|
||||||
gender = input.Gender.String()
|
|
||||||
}
|
|
||||||
imageData, err = getRandomPerformerImage(gender)
|
|
||||||
} else {
|
|
||||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +121,7 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
|||||||
}
|
}
|
||||||
var imageData []byte
|
var imageData []byte
|
||||||
var err error
|
var err error
|
||||||
|
imageIncluded := wasFieldIncluded(ctx, "image")
|
||||||
if input.Image != nil {
|
if input.Image != nil {
|
||||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -196,14 +191,20 @@ func (r *mutationResolver) PerformerUpdate(ctx context.Context, input models.Per
|
|||||||
qb := models.NewPerformerQueryBuilder()
|
qb := models.NewPerformerQueryBuilder()
|
||||||
performer, err := qb.Update(updatedPerformer, tx)
|
performer, err := qb.Update(updatedPerformer, tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// update image table
|
// update image table
|
||||||
if len(imageData) > 0 {
|
if len(imageData) > 0 {
|
||||||
if err := qb.UpdatePerformerImage(performer.ID, imageData, tx); err != nil {
|
if err := qb.UpdatePerformerImage(performer.ID, imageData, tx); err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if imageIncluded {
|
||||||
|
// must be unsetting
|
||||||
|
if err := qb.DestroyPerformerImage(performer.ID, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
|||||||
}
|
}
|
||||||
|
|
||||||
var imageData []byte
|
var imageData []byte
|
||||||
|
imageIncluded := wasFieldIncluded(ctx, "image")
|
||||||
if input.Image != nil {
|
if input.Image != nil {
|
||||||
var err error
|
var err error
|
||||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||||
@@ -123,7 +124,13 @@ func (r *mutationResolver) StudioUpdate(ctx context.Context, input models.Studio
|
|||||||
// update image table
|
// update image table
|
||||||
if len(imageData) > 0 {
|
if len(imageData) > 0 {
|
||||||
if err := qb.UpdateStudioImage(studio.ID, imageData, tx); err != nil {
|
if err := qb.UpdateStudioImage(studio.ID, imageData, tx); err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if imageIncluded {
|
||||||
|
// must be unsetting
|
||||||
|
if err := qb.DestroyStudioImage(studio.ID, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
|||||||
var imageData []byte
|
var imageData []byte
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
imageIncluded := wasFieldIncluded(ctx, "image")
|
||||||
if input.Image != nil {
|
if input.Image != nil {
|
||||||
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
_, imageData, err = utils.ProcessBase64Image(*input.Image)
|
||||||
|
|
||||||
@@ -116,7 +117,13 @@ func (r *mutationResolver) TagUpdate(ctx context.Context, input models.TagUpdate
|
|||||||
// update image table
|
// update image table
|
||||||
if len(imageData) > 0 {
|
if len(imageData) > 0 {
|
||||||
if err := qb.UpdateTagImage(tag.ID, imageData, tx); err != nil {
|
if err := qb.UpdateTagImage(tag.ID, imageData, tx); err != nil {
|
||||||
_ = tx.Rollback()
|
tx.Rollback()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else if imageIncluded {
|
||||||
|
// must be unsetting
|
||||||
|
if err := qb.DestroyTagImage(tag.ID, tx); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ func (rs movieRoutes) FrontImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||||
qb := models.NewMovieQueryBuilder()
|
qb := models.NewMovieQueryBuilder()
|
||||||
image, _ := qb.GetFrontImage(movie.ID, nil)
|
image, _ := qb.GetFrontImage(movie.ID, nil)
|
||||||
|
|
||||||
|
defaultParam := r.URL.Query().Get("default")
|
||||||
|
if len(image) == 0 || defaultParam == "true" {
|
||||||
|
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||||
|
}
|
||||||
|
|
||||||
utils.ServeImage(image, w, r)
|
utils.ServeImage(image, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +41,12 @@ func (rs movieRoutes) BackImage(w http.ResponseWriter, r *http.Request) {
|
|||||||
movie := r.Context().Value(movieKey).(*models.Movie)
|
movie := r.Context().Value(movieKey).(*models.Movie)
|
||||||
qb := models.NewMovieQueryBuilder()
|
qb := models.NewMovieQueryBuilder()
|
||||||
image, _ := qb.GetBackImage(movie.ID, nil)
|
image, _ := qb.GetBackImage(movie.ID, nil)
|
||||||
|
|
||||||
|
defaultParam := r.URL.Query().Get("default")
|
||||||
|
if len(image) == 0 || defaultParam == "true" {
|
||||||
|
_, image, _ = utils.ProcessBase64Image(models.DefaultMovieImage)
|
||||||
|
}
|
||||||
|
|
||||||
utils.ServeImage(image, w, r)
|
utils.ServeImage(image, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ func (rs performerRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
|||||||
performer := r.Context().Value(performerKey).(*models.Performer)
|
performer := r.Context().Value(performerKey).(*models.Performer)
|
||||||
qb := models.NewPerformerQueryBuilder()
|
qb := models.NewPerformerQueryBuilder()
|
||||||
image, _ := qb.GetPerformerImage(performer.ID, nil)
|
image, _ := qb.GetPerformerImage(performer.ID, nil)
|
||||||
|
|
||||||
|
defaultParam := r.URL.Query().Get("default")
|
||||||
|
if len(image) == 0 || defaultParam == "true" {
|
||||||
|
image, _ = getRandomPerformerImageUsingName(performer.Name.String, performer.Gender.String)
|
||||||
|
}
|
||||||
|
|
||||||
utils.ServeImage(image, w, r)
|
utils.ServeImage(image, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ func (rs studioRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
|||||||
studio := r.Context().Value(studioKey).(*models.Studio)
|
studio := r.Context().Value(studioKey).(*models.Studio)
|
||||||
qb := models.NewStudioQueryBuilder()
|
qb := models.NewStudioQueryBuilder()
|
||||||
image, _ := qb.GetStudioImage(studio.ID, nil)
|
image, _ := qb.GetStudioImage(studio.ID, nil)
|
||||||
|
|
||||||
|
defaultParam := r.URL.Query().Get("default")
|
||||||
|
if len(image) == 0 || defaultParam == "true" {
|
||||||
|
_, image, _ = utils.ProcessBase64Image(models.DefaultStudioImage)
|
||||||
|
}
|
||||||
|
|
||||||
utils.ServeImage(image, w, r)
|
utils.ServeImage(image, w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ func (rs tagRoutes) Image(w http.ResponseWriter, r *http.Request) {
|
|||||||
image, _ := qb.GetTagImage(tag.ID, nil)
|
image, _ := qb.GetTagImage(tag.ID, nil)
|
||||||
|
|
||||||
// use default image if not present
|
// use default image if not present
|
||||||
if len(image) == 0 {
|
defaultParam := r.URL.Query().Get("default")
|
||||||
|
if len(image) == 0 || defaultParam == "true" {
|
||||||
image = models.DefaultTagImage
|
image = models.DefaultTagImage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,6 +152,21 @@ func (qb *MovieQueryBuilder) Query(movieFilter *MovieFilterType, findFilter *Fin
|
|||||||
havingClauses = appendClause(havingClauses, havingClause)
|
havingClauses = appendClause(havingClauses, havingClause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isMissingFilter := movieFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||||
|
switch *isMissingFilter {
|
||||||
|
case "front_image":
|
||||||
|
body += `left join movies_images on movies_images.movie_id = movies.id
|
||||||
|
`
|
||||||
|
whereClauses = appendClause(whereClauses, "movies_images.front_image IS NULL")
|
||||||
|
case "back_image":
|
||||||
|
body += `left join movies_images on movies_images.movie_id = movies.id
|
||||||
|
`
|
||||||
|
whereClauses = appendClause(whereClauses, "movies_images.back_image IS NULL")
|
||||||
|
default:
|
||||||
|
whereClauses = appendClause(whereClauses, "movies."+*isMissingFilter+" IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortAndPagination := qb.getMovieSort(findFilter) + getPagination(findFilter)
|
sortAndPagination := qb.getMovieSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult := executeFindQuery("movies", body, args, sortAndPagination, whereClauses, havingClauses)
|
idsResult, countResult := executeFindQuery("movies", body, args, sortAndPagination, whereClauses, havingClauses)
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,10 @@ func (qb *PerformerQueryBuilder) Query(performerFilter *PerformerFilterType, fin
|
|||||||
switch *isMissingFilter {
|
switch *isMissingFilter {
|
||||||
case "scenes":
|
case "scenes":
|
||||||
query.addWhere("scenes_join.scene_id IS NULL")
|
query.addWhere("scenes_join.scene_id IS NULL")
|
||||||
|
case "image":
|
||||||
|
query.body += `left join performers_image on performers_image.performer_id = performers.id
|
||||||
|
`
|
||||||
|
query.addWhere("performers_image.performer_id IS NULL")
|
||||||
default:
|
default:
|
||||||
query.addWhere("performers." + *isMissingFilter + " IS NULL")
|
query.addWhere("performers." + *isMissingFilter + " IS NULL")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,17 @@ func (qb *StudioQueryBuilder) Query(studioFilter *StudioFilterType, findFilter *
|
|||||||
havingClauses = appendClause(havingClauses, havingClause)
|
havingClauses = appendClause(havingClauses, havingClause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isMissingFilter := studioFilter.IsMissing; isMissingFilter != nil && *isMissingFilter != "" {
|
||||||
|
switch *isMissingFilter {
|
||||||
|
case "image":
|
||||||
|
body += `left join studios_image on studios_image.studio_id = studios.id
|
||||||
|
`
|
||||||
|
whereClauses = appendClause(whereClauses, "studios_image.studio_id IS NULL")
|
||||||
|
default:
|
||||||
|
whereClauses = appendClause(whereClauses, "studios."+*isMissingFilter+" IS NULL")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sortAndPagination := qb.getStudioSort(findFilter) + getPagination(findFilter)
|
sortAndPagination := qb.getStudioSort(findFilter) + getPagination(findFilter)
|
||||||
idsResult, countResult := executeFindQuery("studios", body, args, sortAndPagination, whereClauses, havingClauses)
|
idsResult, countResult := executeFindQuery("studios", body, args, sortAndPagination, whereClauses, havingClauses)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"crypto/md5"
|
"crypto/md5"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash/fnv"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
@@ -38,3 +39,9 @@ func GenerateRandomKey(l int) string {
|
|||||||
rand.Read(b)
|
rand.Read(b)
|
||||||
return fmt.Sprintf("%x", b)
|
return fmt.Sprintf("%x", b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IntFromString(str string) uint64 {
|
||||||
|
h := fnv.New64a()
|
||||||
|
h.Write([]byte(str))
|
||||||
|
return h.Sum64()
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,8 +42,12 @@ export const Movie: React.FC = () => {
|
|||||||
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing movie state
|
// Editing movie state
|
||||||
const [frontImage, setFrontImage] = useState<string | undefined>(undefined);
|
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
||||||
const [backImage, setBackImage] = useState<string | undefined>(undefined);
|
undefined
|
||||||
|
);
|
||||||
|
const [backImage, setBackImage] = useState<string | undefined | null>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
const [name, setName] = useState<string | undefined>(undefined);
|
const [name, setName] = useState<string | undefined>(undefined);
|
||||||
const [aliases, setAliases] = useState<string | undefined>(undefined);
|
const [aliases, setAliases] = useState<string | undefined>(undefined);
|
||||||
const [duration, setDuration] = useState<number | undefined>(undefined);
|
const [duration, setDuration] = useState<number | undefined>(undefined);
|
||||||
@@ -432,6 +436,24 @@ export const Movie: React.FC = () => {
|
|||||||
setScrapedMovie(undefined);
|
setScrapedMovie(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClearFrontImage() {
|
||||||
|
setFrontImage(null);
|
||||||
|
setImagePreview(
|
||||||
|
movie.front_image_path
|
||||||
|
? `${movie.front_image_path}?default=true`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClearBackImage() {
|
||||||
|
setBackImage(null);
|
||||||
|
setBackImagePreview(
|
||||||
|
movie.back_image_path
|
||||||
|
? `${movie.back_image_path}?default=true`
|
||||||
|
: undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
@@ -538,7 +560,9 @@ export const Movie: React.FC = () => {
|
|||||||
onToggleEdit={onToggleEdit}
|
onToggleEdit={onToggleEdit}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onImageChange={onFrontImageChange}
|
onImageChange={onFrontImageChange}
|
||||||
|
onClearImage={onClearFrontImage}
|
||||||
onBackImageChange={onBackImageChange}
|
onBackImageChange={onBackImageChange}
|
||||||
|
onClearBackImage={onClearBackImage}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,10 +27,17 @@ export const Performer: React.FC = () => {
|
|||||||
const [performer, setPerformer] = useState<
|
const [performer, setPerformer] = useState<
|
||||||
Partial<GQL.PerformerDataFragment>
|
Partial<GQL.PerformerDataFragment>
|
||||||
>({});
|
>({});
|
||||||
const [imagePreview, setImagePreview] = useState<string>();
|
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||||
const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
|
const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
|
||||||
const activeImage = imagePreview ?? performer.image_path ?? "";
|
|
||||||
|
// if undefined then get the existing image
|
||||||
|
// if null then get the default (no) image
|
||||||
|
// otherwise get the set image
|
||||||
|
const activeImage =
|
||||||
|
imagePreview === undefined
|
||||||
|
? performer.image_path ?? ""
|
||||||
|
: imagePreview ?? `${performer.image_path}?default=true`;
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -47,7 +54,7 @@ export const Performer: React.FC = () => {
|
|||||||
if (data?.findPerformer) setPerformer(data.findPerformer);
|
if (data?.findPerformer) setPerformer(data.findPerformer);
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const onImageChange = (image?: string) => setImagePreview(image);
|
const onImageChange = (image?: string | null) => setImagePreview(image);
|
||||||
|
|
||||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ interface IPerformerDetails {
|
|||||||
| Partial<GQL.PerformerUpdateInput>
|
| Partial<GQL.PerformerUpdateInput>
|
||||||
) => void;
|
) => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
onImageChange?: (image?: string) => void;
|
onImageChange?: (image?: string | null) => void;
|
||||||
onImageEncoding?: (loading?: boolean) => void;
|
onImageEncoding?: (loading?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing performer state
|
// Editing performer state
|
||||||
const [image, setImage] = useState<string>();
|
const [image, setImage] = useState<string | null>();
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
const [aliases, setAliases] = useState<string>();
|
const [aliases, setAliases] = useState<string>();
|
||||||
const [favorite, setFavorite] = useState<boolean>();
|
const [favorite, setFavorite] = useState<boolean>();
|
||||||
@@ -241,7 +241,6 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImage(undefined);
|
|
||||||
updatePerformerEditState(performer);
|
updatePerformerEditState(performer);
|
||||||
}, [performer]);
|
}, [performer]);
|
||||||
|
|
||||||
@@ -563,6 +562,17 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
isEditing={!!isEditing}
|
isEditing={!!isEditing}
|
||||||
onImageChange={onImageChangeHandler}
|
onImageChange={onImageChangeHandler}
|
||||||
/>
|
/>
|
||||||
|
{isEditing ? (
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => setImage(null)}
|
||||||
|
>
|
||||||
|
Clear image
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ interface IProps {
|
|||||||
onAutoTag?: () => void;
|
onAutoTag?: () => void;
|
||||||
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
onBackImageChange?: (event: React.FormEvent<HTMLInputElement>) => void;
|
onBackImageChange?: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
|
onClearImage?: () => void;
|
||||||
|
onClearBackImage?: () => void;
|
||||||
acceptSVG?: boolean;
|
acceptSVG?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +118,29 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
onImageChange={props.onImageChange}
|
onImageChange={props.onImageChange}
|
||||||
acceptSVG={props.acceptSVG ?? false}
|
acceptSVG={props.acceptSVG ?? false}
|
||||||
/>
|
/>
|
||||||
|
{props.isEditing && props.onClearImage ? (
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => props.onClearImage!()}
|
||||||
|
>
|
||||||
|
{props.onClearBackImage ? "Clear front image" : "Clear image"}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
{renderBackImageInput()}
|
{renderBackImageInput()}
|
||||||
|
{props.isEditing && props.onClearBackImage ? (
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => props.onClearBackImage!()}
|
||||||
|
>
|
||||||
|
Clear back image
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
""
|
||||||
|
)}
|
||||||
{renderAutoTagButton()}
|
{renderAutoTagButton()}
|
||||||
{renderSaveButton()}
|
{renderSaveButton()}
|
||||||
{renderDeleteButton()}
|
{renderDeleteButton()}
|
||||||
|
|||||||
@@ -35,14 +35,14 @@ export const Studio: React.FC = () => {
|
|||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing studio state
|
// Editing studio state
|
||||||
const [image, setImage] = useState<string>();
|
const [image, setImage] = useState<string | null>();
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
const [url, setUrl] = useState<string>();
|
const [url, setUrl] = useState<string>();
|
||||||
const [parentStudioId, setParentStudioId] = useState<string>();
|
const [parentStudioId, setParentStudioId] = useState<string>();
|
||||||
|
|
||||||
// Studio state
|
// Studio state
|
||||||
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
const [studio, setStudio] = useState<Partial<GQL.StudioDataFragment>>({});
|
||||||
const [imagePreview, setImagePreview] = useState<string>();
|
const [imagePreview, setImagePreview] = useState<string | null>();
|
||||||
|
|
||||||
const { data, error, loading } = useFindStudio(id);
|
const { data, error, loading } = useFindStudio(id);
|
||||||
const [updateStudio] = useStudioUpdate(
|
const [updateStudio] = useStudioUpdate(
|
||||||
@@ -185,6 +185,13 @@ export const Studio: React.FC = () => {
|
|||||||
updateStudioData(studio);
|
updateStudioData(studio);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClearImage() {
|
||||||
|
setImage(null);
|
||||||
|
setImagePreview(
|
||||||
|
studio.image_path ? `${studio.image_path}?default=true` : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div
|
<div
|
||||||
@@ -197,8 +204,10 @@ export const Studio: React.FC = () => {
|
|||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{imageEncoding ? (
|
{imageEncoding ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : imagePreview ? (
|
||||||
<img className="logo" alt={name} src={imagePreview} />
|
<img className="logo" alt={name} src={imagePreview} />
|
||||||
|
) : (
|
||||||
|
""
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Table>
|
<Table>
|
||||||
@@ -238,6 +247,9 @@ export const Studio: React.FC = () => {
|
|||||||
onToggleEdit={onToggleEdit}
|
onToggleEdit={onToggleEdit}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onImageChange={onImageChangeHandler}
|
onImageChange={onImageChangeHandler}
|
||||||
|
onClearImage={() => {
|
||||||
|
onClearImage();
|
||||||
|
}}
|
||||||
onAutoTag={onAutoTag}
|
onAutoTag={onAutoTag}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
acceptSVG
|
acceptSVG
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const Tag: React.FC = () => {
|
|||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing tag state
|
// Editing tag state
|
||||||
const [image, setImage] = useState<string>();
|
const [image, setImage] = useState<string | null>();
|
||||||
const [name, setName] = useState<string>();
|
const [name, setName] = useState<string>();
|
||||||
|
|
||||||
// Tag state
|
// Tag state
|
||||||
@@ -172,6 +172,13 @@ export const Tag: React.FC = () => {
|
|||||||
updateTagData(tag);
|
updateTagData(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onClearImage() {
|
||||||
|
setImage(null);
|
||||||
|
setImagePreview(
|
||||||
|
tag.image_path ? `${tag.image_path}?default=true` : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div
|
<div
|
||||||
@@ -205,6 +212,9 @@ export const Tag: React.FC = () => {
|
|||||||
onToggleEdit={onToggleEdit}
|
onToggleEdit={onToggleEdit}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onImageChange={onImageChangeHandler}
|
onImageChange={onImageChangeHandler}
|
||||||
|
onClearImage={() => {
|
||||||
|
onClearImage();
|
||||||
|
}}
|
||||||
onAutoTag={onAutoTag}
|
onAutoTag={onAutoTag}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
acceptSVG
|
acceptSVG
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export type CriterionType =
|
|||||||
| "performerIsMissing"
|
| "performerIsMissing"
|
||||||
| "galleryIsMissing"
|
| "galleryIsMissing"
|
||||||
| "tagIsMissing"
|
| "tagIsMissing"
|
||||||
|
| "studioIsMissing"
|
||||||
|
| "movieIsMissing"
|
||||||
| "tags"
|
| "tags"
|
||||||
| "sceneTags"
|
| "sceneTags"
|
||||||
| "performers"
|
| "performers"
|
||||||
@@ -62,6 +64,8 @@ export abstract class Criterion {
|
|||||||
case "performerIsMissing":
|
case "performerIsMissing":
|
||||||
case "galleryIsMissing":
|
case "galleryIsMissing":
|
||||||
case "tagIsMissing":
|
case "tagIsMissing":
|
||||||
|
case "studioIsMissing":
|
||||||
|
case "movieIsMissing":
|
||||||
return "Is Missing";
|
return "Is Missing";
|
||||||
case "tags":
|
case "tags":
|
||||||
return "Tags";
|
return "Tags";
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
|
|||||||
"aliases",
|
"aliases",
|
||||||
"gender",
|
"gender",
|
||||||
"scenes",
|
"scenes",
|
||||||
|
"image",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,3 +74,23 @@ export class TagIsMissingCriterionOption implements ICriterionOption {
|
|||||||
public label: string = Criterion.getLabel("tagIsMissing");
|
public label: string = Criterion.getLabel("tagIsMissing");
|
||||||
public value: CriterionType = "tagIsMissing";
|
public value: CriterionType = "tagIsMissing";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class StudioIsMissingCriterion extends IsMissingCriterion {
|
||||||
|
public type: CriterionType = "studioIsMissing";
|
||||||
|
public options: string[] = ["image"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StudioIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
public label: string = Criterion.getLabel("studioIsMissing");
|
||||||
|
public value: CriterionType = "studioIsMissing";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MovieIsMissingCriterion extends IsMissingCriterion {
|
||||||
|
public type: CriterionType = "movieIsMissing";
|
||||||
|
public options: string[] = ["front_image", "back_image"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MovieIsMissingCriterionOption implements ICriterionOption {
|
||||||
|
public label: string = Criterion.getLabel("movieIsMissing");
|
||||||
|
public value: CriterionType = "movieIsMissing";
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import {
|
|||||||
SceneIsMissingCriterion,
|
SceneIsMissingCriterion,
|
||||||
GalleryIsMissingCriterion,
|
GalleryIsMissingCriterion,
|
||||||
TagIsMissingCriterion,
|
TagIsMissingCriterion,
|
||||||
|
StudioIsMissingCriterion,
|
||||||
|
MovieIsMissingCriterion,
|
||||||
} from "./is-missing";
|
} from "./is-missing";
|
||||||
import { NoneCriterion } from "./none";
|
import { NoneCriterion } from "./none";
|
||||||
import { PerformersCriterion } from "./performers";
|
import { PerformersCriterion } from "./performers";
|
||||||
@@ -50,6 +52,10 @@ export function makeCriteria(type: CriterionType = "none") {
|
|||||||
return new GalleryIsMissingCriterion();
|
return new GalleryIsMissingCriterion();
|
||||||
case "tagIsMissing":
|
case "tagIsMissing":
|
||||||
return new TagIsMissingCriterion();
|
return new TagIsMissingCriterion();
|
||||||
|
case "studioIsMissing":
|
||||||
|
return new StudioIsMissingCriterion();
|
||||||
|
case "movieIsMissing":
|
||||||
|
return new MovieIsMissingCriterion();
|
||||||
case "tags":
|
case "tags":
|
||||||
return new TagsCriterion("tags");
|
return new TagsCriterion("tags");
|
||||||
case "sceneTags":
|
case "sceneTags":
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import {
|
|||||||
SceneIsMissingCriterionOption,
|
SceneIsMissingCriterionOption,
|
||||||
GalleryIsMissingCriterionOption,
|
GalleryIsMissingCriterionOption,
|
||||||
TagIsMissingCriterionOption,
|
TagIsMissingCriterionOption,
|
||||||
|
StudioIsMissingCriterionOption,
|
||||||
|
MovieIsMissingCriterionOption,
|
||||||
} from "./criteria/is-missing";
|
} from "./criteria/is-missing";
|
||||||
import { NoneCriterionOption } from "./criteria/none";
|
import { NoneCriterionOption } from "./criteria/none";
|
||||||
import {
|
import {
|
||||||
@@ -178,6 +180,7 @@ export class ListFilterModel {
|
|||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
new ParentStudiosCriterionOption(),
|
new ParentStudiosCriterionOption(),
|
||||||
|
new StudioIsMissingCriterionOption(),
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.Movies:
|
case FilterMode.Movies:
|
||||||
@@ -187,6 +190,7 @@ export class ListFilterModel {
|
|||||||
this.criterionOptions = [
|
this.criterionOptions = [
|
||||||
new NoneCriterionOption(),
|
new NoneCriterionOption(),
|
||||||
new StudiosCriterionOption(),
|
new StudiosCriterionOption(),
|
||||||
|
new MovieIsMissingCriterionOption(),
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
case FilterMode.Galleries:
|
case FilterMode.Galleries:
|
||||||
@@ -610,6 +614,8 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "movieIsMissing":
|
||||||
|
result.is_missing = (criterion as IsMissingCriterion).value;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -628,6 +634,8 @@ export class ListFilterModel {
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "studioIsMissing":
|
||||||
|
result.is_missing = (criterion as IsMissingCriterion).value;
|
||||||
// no default
|
// no default
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user