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:
WithoutPants
2020-08-12 09:19:27 +10:00
committed by GitHub
parent c0afd31b1c
commit e16118f551
24 changed files with 287 additions and 42 deletions

View File

@@ -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 {

View File

@@ -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])
}

View File

@@ -19,21 +19,26 @@ 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
_, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage) if input.FrontImage != nil {
if err != nil { _, frontimageData, err = utils.ProcessBase64Image(*input.FrontImage)
return nil, err if err != nil {
return nil, err
}
} }
// Process the base 64 encoded image string // Process the base 64 encoded image string
_, backimageData, err = utils.ProcessBase64Image(*input.BackImage) if input.BackImage != nil {
if err != nil { _, backimageData, err = utils.ProcessBase64Image(*input.BackImage)
return nil, err if err != nil {
return nil, err
}
} }
// Populate a new movie from the input // Populate a new movie from the input
@@ -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,25 +192,39 @@ 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 err := qb.UpdateMovieImages(movie.ID, frontimageData, backimageData, tx); err != nil { if len(frontimageData) == 0 && len(backimageData) == 0 {
_ = tx.Rollback() // both images are being nulled. Destroy them.
return nil, err 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 {
_ = tx.Rollback()
return nil, err
}
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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
} }
} }

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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)
} }

View File

@@ -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
} }

View File

@@ -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)

View File

@@ -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")
} }

View File

@@ -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)

View File

@@ -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()
}

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>
); );
} }

View File

@@ -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()}

View File

@@ -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

View File

@@ -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

View File

@@ -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";

View File

@@ -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";
}

View File

@@ -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":

View File

@@ -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
} }
}); });