[Feature] Images new fields : URL & Date (#3015)

Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
HijackHornet 2022-12-20 01:13:41 +01:00 committed by GitHub
parent cc4b0f7b11
commit 0b4b100ecc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 191 additions and 15 deletions

View file

@ -1,6 +1,8 @@
fragment SlimImageData on Image { fragment SlimImageData on Image {
id id
title title
date
url
rating100 rating100
organized organized
o_counter o_counter

View file

@ -2,6 +2,8 @@ fragment ImageData on Image {
id id
title title
rating100 rating100
date
url
organized organized
o_counter o_counter
created_at created_at

View file

@ -425,6 +425,10 @@ input ImageFilterType {
rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100") rating: IntCriterionInput @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100 # rating expressed as 1-100
rating100: IntCriterionInput rating100: IntCriterionInput
"""Filter by date"""
date: DateCriterionInput
"""Filter by url"""
url: StringCriterionInput
"""Filter by organized""" """Filter by organized"""
organized: Boolean organized: Boolean
"""Filter by o-counter""" """Filter by o-counter"""

View file

@ -6,6 +6,8 @@ type Image {
rating: Int @deprecated(reason: "Use 1-100 range with rating100") rating: Int @deprecated(reason: "Use 1-100 range with rating100")
# rating expressed as 1-100 # rating expressed as 1-100
rating100: Int rating100: Int
url: String
date: String
o_counter: Int o_counter: Int
organized: Boolean! organized: Boolean!
path: String! @deprecated(reason: "Use files.path") path: String! @deprecated(reason: "Use files.path")
@ -45,6 +47,8 @@ input ImageUpdateInput {
# rating expressed as 1-100 # rating expressed as 1-100
rating100: Int rating100: Int
organized: Boolean organized: Boolean
url: String
date: String
studio_id: ID studio_id: ID
performer_ids: [ID!] performer_ids: [ID!]
@ -63,6 +67,8 @@ input BulkImageUpdateInput {
# rating expressed as 1-100 # rating expressed as 1-100
rating100: Int rating100: Int
organized: Boolean organized: Boolean
url: String
date: String
studio_id: ID studio_id: ID
performer_ids: BulkUpdateIds performer_ids: BulkUpdateIds

View file

@ -75,6 +75,14 @@ func (r *imageResolver) File(ctx context.Context, obj *models.Image) (*ImageFile
}, nil }, nil
} }
func (r *imageResolver) Date(ctx context.Context, obj *models.Image) (*string, error) {
if obj.Date != nil {
result := obj.Date.String()
return &result, nil
}
return nil, nil
}
func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) { func (r *imageResolver) Files(ctx context.Context, obj *models.Image) ([]*ImageFile, error) {
files, err := r.getFiles(ctx, obj) files, err := r.getFiles(ctx, obj)
if err != nil { if err != nil {

View file

@ -104,6 +104,8 @@ func (r *mutationResolver) imageUpdate(ctx context.Context, input ImageUpdateInp
updatedImage := models.NewImagePartial() updatedImage := models.NewImagePartial()
updatedImage.Title = translator.optionalString(input.Title, "title") updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedImage.URL = translator.optionalString(input.URL, "url")
updatedImage.Date = translator.optionalDate(input.Date, "date")
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil { if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err) return nil, fmt.Errorf("converting studio id: %w", err)
@ -190,6 +192,8 @@ func (r *mutationResolver) BulkImageUpdate(ctx context.Context, input BulkImageU
updatedImage.Title = translator.optionalString(input.Title, "title") updatedImage.Title = translator.optionalString(input.Title, "title")
updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100) updatedImage.Rating = translator.ratingConversionOptional(input.Rating, input.Rating100)
updatedImage.URL = translator.optionalString(input.URL, "url")
updatedImage.Date = translator.optionalDate(input.Date, "date")
updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id") updatedImage.StudioID, err = translator.optionalIntFromString(input.StudioID, "studio_id")
if err != nil { if err != nil {
return nil, fmt.Errorf("converting studio id: %w", err) return nil, fmt.Errorf("converting studio id: %w", err)

View file

@ -15,6 +15,7 @@ import (
func ToBasicJSON(image *models.Image) *jsonschema.Image { func ToBasicJSON(image *models.Image) *jsonschema.Image {
newImageJSON := jsonschema.Image{ newImageJSON := jsonschema.Image{
Title: image.Title, Title: image.Title,
URL: image.URL,
CreatedAt: json.JSONTime{Time: image.CreatedAt}, CreatedAt: json.JSONTime{Time: image.CreatedAt},
UpdatedAt: json.JSONTime{Time: image.UpdatedAt}, UpdatedAt: json.JSONTime{Time: image.UpdatedAt},
} }
@ -23,6 +24,10 @@ func ToBasicJSON(image *models.Image) *jsonschema.Image {
newImageJSON.Rating = *image.Rating newImageJSON.Rating = *image.Rating
} }
if image.Date != nil {
newImageJSON.Date = image.Date.String()
}
newImageJSON.Organized = image.Organized newImageJSON.Organized = image.Organized
newImageJSON.OCounter = image.OCounter newImageJSON.OCounter = image.OCounter

View file

@ -25,6 +25,9 @@ const (
var ( var (
title = "title" title = "title"
rating = 5 rating = 5
url = "http://a.com"
date = "2001-01-01"
dateObj = models.NewDate(date)
organized = true organized = true
ocounter = 2 ocounter = 2
) )
@ -52,6 +55,8 @@ func createFullImage(id int) models.Image {
Title: title, Title: title,
OCounter: ocounter, OCounter: ocounter,
Rating: &rating, Rating: &rating,
Date: &dateObj,
URL: url,
Organized: organized, Organized: organized,
CreatedAt: createTime, CreatedAt: createTime,
UpdatedAt: updateTime, UpdatedAt: updateTime,
@ -63,6 +68,8 @@ func createFullJSONImage() *jsonschema.Image {
Title: title, Title: title,
OCounter: ocounter, OCounter: ocounter,
Rating: rating, Rating: rating,
Date: date,
URL: url,
Organized: organized, Organized: organized,
Files: []string{path}, Files: []string{path},
CreatedAt: json.JSONTime{ CreatedAt: json.JSONTime{

View file

@ -85,6 +85,13 @@ func (i *Importer) imageJSONToImage(imageJSON jsonschema.Image) models.Image {
if imageJSON.Rating != 0 { if imageJSON.Rating != 0 {
newImage.Rating = &imageJSON.Rating newImage.Rating = &imageJSON.Rating
} }
if imageJSON.URL != "" {
newImage.URL = imageJSON.URL
}
if imageJSON.Date != "" {
d := models.NewDate(imageJSON.Date)
newImage.Date = &d
}
return newImage return newImage
} }

View file

@ -18,6 +18,10 @@ type ImageFilterType struct {
Rating *IntCriterionInput `json:"rating"` Rating *IntCriterionInput `json:"rating"`
// Filter by rating expressed as 1-100 // Filter by rating expressed as 1-100
Rating100 *IntCriterionInput `json:"rating100"` Rating100 *IntCriterionInput `json:"rating100"`
// Filter by date
Date *DateCriterionInput `json:"date"`
// Filter by url
URL *StringCriterionInput `json:"url"`
// Filter by organized // Filter by organized
Organized *bool `json:"organized"` Organized *bool `json:"organized"`
// Filter by o-counter // Filter by o-counter

View file

@ -13,6 +13,8 @@ type Image struct {
Title string `json:"title,omitempty"` Title string `json:"title,omitempty"`
Studio string `json:"studio,omitempty"` Studio string `json:"studio,omitempty"`
Rating int `json:"rating,omitempty"` Rating int `json:"rating,omitempty"`
URL string `json:"url,omitempty"`
Date string `json:"date,omitempty"`
Organized bool `json:"organized,omitempty"` Organized bool `json:"organized,omitempty"`
OCounter int `json:"o_counter,omitempty"` OCounter int `json:"o_counter,omitempty"`
Galleries []GalleryRef `json:"galleries,omitempty"` Galleries []GalleryRef `json:"galleries,omitempty"`

View file

@ -20,6 +20,8 @@ type Image struct {
Organized bool `json:"organized"` Organized bool `json:"organized"`
OCounter int `json:"o_counter"` OCounter int `json:"o_counter"`
StudioID *int `json:"studio_id"` StudioID *int `json:"studio_id"`
URL string `json:"url"`
Date *Date `json:"date"`
// transient - not persisted // transient - not persisted
Files RelatedImageFiles Files RelatedImageFiles
@ -117,6 +119,8 @@ type ImagePartial struct {
Title OptionalString Title OptionalString
// Rating expressed in 1-100 scale // Rating expressed in 1-100 scale
Rating OptionalInt Rating OptionalInt
URL OptionalString
Date OptionalDate
Organized OptionalBool Organized OptionalBool
OCounter OptionalInt OCounter OptionalInt
StudioID OptionalInt StudioID OptionalInt

View file

@ -21,7 +21,7 @@ import (
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
) )
var appSchemaVersion uint = 42 var appSchemaVersion uint = 43
//go:embed migrations/*.sql //go:embed migrations/*.sql
var migrationsBox embed.FS var migrationsBox embed.FS

View file

@ -31,6 +31,8 @@ type imageRow struct {
Title zero.String `db:"title"` Title zero.String `db:"title"`
// expressed as 1-100 // expressed as 1-100
Rating null.Int `db:"rating"` Rating null.Int `db:"rating"`
URL zero.String `db:"url"`
Date models.SQLiteDate `db:"date"`
Organized bool `db:"organized"` Organized bool `db:"organized"`
OCounter int `db:"o_counter"` OCounter int `db:"o_counter"`
StudioID null.Int `db:"studio_id,omitempty"` StudioID null.Int `db:"studio_id,omitempty"`
@ -42,6 +44,10 @@ func (r *imageRow) fromImage(i models.Image) {
r.ID = i.ID r.ID = i.ID
r.Title = zero.StringFrom(i.Title) r.Title = zero.StringFrom(i.Title)
r.Rating = intFromPtr(i.Rating) r.Rating = intFromPtr(i.Rating)
r.URL = zero.StringFrom(i.URL)
if i.Date != nil {
_ = r.Date.Scan(i.Date.Time)
}
r.Organized = i.Organized r.Organized = i.Organized
r.OCounter = i.OCounter r.OCounter = i.OCounter
r.StudioID = intFromPtr(i.StudioID) r.StudioID = intFromPtr(i.StudioID)
@ -62,6 +68,8 @@ func (r *imageQueryRow) resolve() *models.Image {
ID: r.ID, ID: r.ID,
Title: r.Title.String, Title: r.Title.String,
Rating: nullIntPtr(r.Rating), Rating: nullIntPtr(r.Rating),
URL: r.URL.String,
Date: r.Date.DatePtr(),
Organized: r.Organized, Organized: r.Organized,
OCounter: r.OCounter, OCounter: r.OCounter,
StudioID: nullIntPtr(r.StudioID), StudioID: nullIntPtr(r.StudioID),
@ -87,6 +95,8 @@ type imageRowRecord struct {
func (r *imageRowRecord) fromPartial(i models.ImagePartial) { func (r *imageRowRecord) fromPartial(i models.ImagePartial) {
r.setNullString("title", i.Title) r.setNullString("title", i.Title)
r.setNullInt("rating", i.Rating) r.setNullInt("rating", i.Rating)
r.setNullString("url", i.URL)
r.setSQLiteDate("date", i.Date)
r.setBool("organized", i.Organized) r.setBool("organized", i.Organized)
r.setInt("o_counter", i.OCounter) r.setInt("o_counter", i.OCounter)
r.setNullInt("studio_id", i.StudioID) r.setNullInt("studio_id", i.StudioID)
@ -638,6 +648,8 @@ func (qb *ImageStore) makeFilter(ctx context.Context, imageFilter *models.ImageF
query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil)) query.handleCriterion(ctx, rating5CriterionHandler(imageFilter.Rating, "images.rating", nil))
query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil)) query.handleCriterion(ctx, intCriterionHandler(imageFilter.OCounter, "images.o_counter", nil))
query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil)) query.handleCriterion(ctx, boolCriterionHandler(imageFilter.Organized, "images.organized", nil))
query.handleCriterion(ctx, dateCriterionHandler(imageFilter.Date, "images.date"))
query.handleCriterion(ctx, stringCriterionHandler(imageFilter.URL, "images.url"))
query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable)) query.handleCriterion(ctx, resolutionCriterionHandler(imageFilter.Resolution, "image_files.height", "image_files.width", qb.addImageFilesTable))
query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing)) query.handleCriterion(ctx, imageIsMissingCriterionHandler(qb, imageFilter.IsMissing))

View file

@ -56,6 +56,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
title = "title" title = "title"
rating = 60 rating = 60
ocounter = 5 ocounter = 5
url = "url"
date = models.NewDate("2003-02-01")
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
@ -72,6 +74,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
models.Image{ models.Image{
Title: title, Title: title,
Rating: &rating, Rating: &rating,
Date: &date,
URL: url,
Organized: true, Organized: true,
OCounter: ocounter, OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithImage], StudioID: &studioIDs[studioIdxWithImage],
@ -88,6 +92,8 @@ func Test_imageQueryBuilder_Create(t *testing.T) {
models.Image{ models.Image{
Title: title, Title: title,
Rating: &rating, Rating: &rating,
Date: &date,
URL: url,
Organized: true, Organized: true,
OCounter: ocounter, OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithImage], StudioID: &studioIDs[studioIdxWithImage],
@ -209,6 +215,8 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
var ( var (
title = "title" title = "title"
rating = 60 rating = 60
url = "url"
date = models.NewDate("2003-02-01")
ocounter = 5 ocounter = 5
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
@ -225,6 +233,8 @@ func Test_imageQueryBuilder_Update(t *testing.T) {
ID: imageIDs[imageIdxWithGallery], ID: imageIDs[imageIdxWithGallery],
Title: title, Title: title,
Rating: &rating, Rating: &rating,
URL: url,
Date: &date,
Organized: true, Organized: true,
OCounter: ocounter, OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithImage], StudioID: &studioIDs[studioIdxWithImage],
@ -372,6 +382,8 @@ func clearImagePartial() models.ImagePartial {
return models.ImagePartial{ return models.ImagePartial{
Title: models.OptionalString{Set: true, Null: true}, Title: models.OptionalString{Set: true, Null: true},
Rating: models.OptionalInt{Set: true, Null: true}, Rating: models.OptionalInt{Set: true, Null: true},
URL: models.OptionalString{Set: true, Null: true},
Date: models.OptionalDate{Set: true, Null: true},
StudioID: models.OptionalInt{Set: true, Null: true}, StudioID: models.OptionalInt{Set: true, Null: true},
GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, GalleryIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet}, TagIDs: &models.UpdateIDs{Mode: models.RelationshipUpdateModeSet},
@ -383,6 +395,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
var ( var (
title = "title" title = "title"
rating = 60 rating = 60
url = "url"
date = models.NewDate("2003-02-01")
ocounter = 5 ocounter = 5
createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) createdAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC) updatedAt = time.Date(2001, 1, 1, 0, 0, 0, 0, time.UTC)
@ -401,6 +415,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
models.ImagePartial{ models.ImagePartial{
Title: models.NewOptionalString(title), Title: models.NewOptionalString(title),
Rating: models.NewOptionalInt(rating), Rating: models.NewOptionalInt(rating),
URL: models.NewOptionalString(url),
Date: models.NewOptionalDate(date),
Organized: models.NewOptionalBool(true), Organized: models.NewOptionalBool(true),
OCounter: models.NewOptionalInt(ocounter), OCounter: models.NewOptionalInt(ocounter),
StudioID: models.NewOptionalInt(studioIDs[studioIdxWithImage]), StudioID: models.NewOptionalInt(studioIDs[studioIdxWithImage]),
@ -423,6 +439,8 @@ func Test_imageQueryBuilder_UpdatePartial(t *testing.T) {
ID: imageIDs[imageIdx1WithGallery], ID: imageIDs[imageIdx1WithGallery],
Title: title, Title: title,
Rating: &rating, Rating: &rating,
URL: url,
Date: &date,
Organized: true, Organized: true,
OCounter: ocounter, OCounter: ocounter,
StudioID: &studioIDs[studioIdxWithImage], StudioID: &studioIDs[studioIdxWithImage],
@ -943,7 +961,8 @@ func Test_imageQueryBuilder_Destroy(t *testing.T) {
} }
func makeImageWithID(index int) *models.Image { func makeImageWithID(index int) *models.Image {
ret := makeImage(index) const fromDB = true
ret := makeImage(index, true)
ret.ID = imageIDs[index] ret.ID = imageIDs[index]
ret.Files = models.NewRelatedImageFiles([]*file.ImageFile{makeImageFile(index)}) ret.Files = models.NewRelatedImageFiles([]*file.ImageFile{makeImageFile(index)})
@ -2560,6 +2579,13 @@ func TestImageQuerySorting(t *testing.T) {
-1, -1,
-1, -1,
}, },
{
"date",
"date",
models.SortDirectionEnumDesc,
imageIdxWithTwoGalleries,
imageIdxWithGrandChildStudio,
},
} }
qb := db.Image qb := db.Image

View file

@ -0,0 +1,2 @@
ALTER TABLE `images` ADD COLUMN `url` varchar(255);
ALTER TABLE `images` ADD COLUMN `date` date;

View file

@ -885,9 +885,9 @@ func getObjectDate(index int) models.SQLiteDate {
} }
} }
func getObjectDateObject(index int) *models.Date { func getObjectDateObject(index int, fromDB bool) *models.Date {
d := getObjectDate(index) d := getObjectDate(index)
if !d.Valid { if !d.Valid || (fromDB && (d.String == "" || d.String == "0001-01-01")) {
return nil return nil
} }
@ -998,7 +998,7 @@ func makeScene(i int) *models.Scene {
URL: getSceneEmptyString(i, urlField), URL: getSceneEmptyString(i, urlField),
Rating: getIntPtr(rating), Rating: getIntPtr(rating),
OCounter: getOCounter(i), OCounter: getOCounter(i),
Date: getObjectDateObject(i), Date: getObjectDateObject(i, false),
StudioID: studioID, StudioID: studioID,
GalleryIDs: models.NewRelatedIDs(gids), GalleryIDs: models.NewRelatedIDs(gids),
PerformerIDs: models.NewRelatedIDs(pids), PerformerIDs: models.NewRelatedIDs(pids),
@ -1063,7 +1063,7 @@ func makeImageFile(i int) *file.ImageFile {
} }
} }
func makeImage(i int) *models.Image { func makeImage(i int, fromDB bool) *models.Image {
title := getImageStringValue(i, titleField) title := getImageStringValue(i, titleField)
var studioID *int var studioID *int
if _, ok := imageStudios[i]; ok { if _, ok := imageStudios[i]; ok {
@ -1078,6 +1078,8 @@ func makeImage(i int) *models.Image {
return &models.Image{ return &models.Image{
Title: title, Title: title,
Rating: getIntPtr(getRating(i)), Rating: getIntPtr(getRating(i)),
Date: getObjectDateObject(i, fromDB),
URL: getImageStringValue(i, urlField),
OCounter: getOCounter(i), OCounter: getOCounter(i),
StudioID: studioID, StudioID: studioID,
GalleryIDs: models.NewRelatedIDs(gids), GalleryIDs: models.NewRelatedIDs(gids),
@ -1101,7 +1103,7 @@ func createImages(ctx context.Context, n int) error {
} }
imageFileIDs = append(imageFileIDs, f.ID) imageFileIDs = append(imageFileIDs, f.ID)
image := makeImage(i) image := makeImage(i, false)
err := qb.Create(ctx, &models.ImageCreateInput{ err := qb.Create(ctx, &models.ImageCreateInput{
Image: image, Image: image,
@ -1162,7 +1164,7 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
Title: getGalleryStringValue(i, titleField), Title: getGalleryStringValue(i, titleField),
URL: getGalleryNullStringValue(i, urlField).String, URL: getGalleryNullStringValue(i, urlField).String,
Rating: getIntPtr(getRating(i)), Rating: getIntPtr(getRating(i)),
Date: getObjectDateObject(i), Date: getObjectDateObject(i, false),
StudioID: studioID, StudioID: studioID,
PerformerIDs: models.NewRelatedIDs(pids), PerformerIDs: models.NewRelatedIDs(pids),
TagIDs: models.NewRelatedIDs(tids), TagIDs: models.NewRelatedIDs(tids),

View file

@ -6,9 +6,8 @@ import { TagLink, TruncatedText } from "src/components/Shared";
import { PerformerCard } from "src/components/Performers/PerformerCard"; import { PerformerCard } from "src/components/Performers/PerformerCard";
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
import { sortPerformers } from "src/core/performers"; import { sortPerformers } from "src/core/performers";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedDate, FormattedMessage, useIntl } from "react-intl";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
interface IImageDetailProps { interface IImageDetailProps {
image: GQL.ImageDataFragment; image: GQL.ImageDataFragment;
} }
@ -91,6 +90,15 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
<TruncatedText text={objectTitle(props.image)} /> <TruncatedText text={objectTitle(props.image)} />
</h3> </h3>
</div> </div>
{props.image.date ? (
<h5>
<FormattedDate
value={props.image.date}
format="long"
timeZone="utc"
/>
</h5>
) : undefined}
{props.image.rating100 ? ( {props.image.rating100 ? (
<h6> <h6>
<FormattedMessage id="rating" />:{" "} <FormattedMessage id="rating" />:{" "}
@ -99,6 +107,7 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
) : ( ) : (
"" ""
)} )}
{renderGalleries()} {renderGalleries()}
{file?.width && file?.height ? ( {file?.width && file?.height ? (
<h6> <h6>

View file

@ -10,6 +10,7 @@ import {
TagSelect, TagSelect,
StudioSelect, StudioSelect,
LoadingIndicator, LoadingIndicator,
URLField,
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { FormUtils } from "src/utils"; import { FormUtils } from "src/utils";
@ -39,6 +40,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
const schema = yup.object({ const schema = yup.object({
title: yup.string().optional().nullable(), title: yup.string().optional().nullable(),
rating100: yup.number().optional().nullable(), rating100: yup.number().optional().nullable(),
url: yup.string().optional().nullable(),
date: yup.string().optional().nullable(),
studio_id: yup.string().optional().nullable(), studio_id: yup.string().optional().nullable(),
performer_ids: yup.array(yup.string().required()).optional().nullable(), performer_ids: yup.array(yup.string().required()).optional().nullable(),
tag_ids: yup.array(yup.string().required()).optional().nullable(), tag_ids: yup.array(yup.string().required()).optional().nullable(),
@ -47,6 +50,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
const initialValues = { const initialValues = {
title: image.title ?? "", title: image.title ?? "",
rating100: image.rating100 ?? null, rating100: image.rating100 ?? null,
url: image?.url ?? "",
date: image?.date ?? "",
studio_id: image.studio?.id, studio_id: image.studio?.id,
performer_ids: (image.performers ?? []).map((p) => p.id), performer_ids: (image.performers ?? []).map((p) => p.id),
tag_ids: (image.tags ?? []).map((t) => t.id), tag_ids: (image.tags ?? []).map((t) => t.id),
@ -189,6 +194,28 @@ export const ImageEditPanel: React.FC<IProps> = ({
<div className="form-container row px-3"> <div className="form-container row px-3">
<div className="col-12 col-lg-6 col-xl-12"> <div className="col-12 col-lg-6 col-xl-12">
{renderTextField("title", intl.formatMessage({ id: "title" }))} {renderTextField("title", intl.formatMessage({ id: "title" }))}
<Form.Group controlId="url" as={Row}>
<Col xs={3} className="pr-0 url-label">
<Form.Label className="col-form-label">
<FormattedMessage id="url" />
</Form.Label>
</Col>
<Col xs={9}>
<URLField
{...formik.getFieldProps("url")}
onScrapeClick={() => {}}
urlScrapable={() => {
return false;
}}
isInvalid={!!formik.getFieldMeta("url").error}
/>
</Col>
</Form.Group>
{renderTextField(
"date",
intl.formatMessage({ id: "date" }),
"YYYY-MM-DD"
)}
<Form.Group controlId="rating" as={Row}> <Form.Group controlId="rating" as={Row}>
{FormUtils.renderLabel({ {FormUtils.renderLabel({
title: intl.formatMessage({ id: "rating" }), title: intl.formatMessage({ id: "rating" }),

View file

@ -118,7 +118,24 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
} }
if (props.image.files.length === 1) { if (props.image.files.length === 1) {
return <FileInfoPanel file={props.image.files[0]} />; return (
<>
<FileInfoPanel file={props.image.files[0]} />
{props.image.url ? (
<dl className="container image-file-info details-list">
<URLField
id="media_info.downloaded_from"
url={TextUtils.sanitiseURL(props.image.url)}
value={TextUtils.domainFromURL(props.image.url)}
truncate
/>
</dl>
) : (
""
)}
</>
);
} }
async function onSetPrimaryFile(fileID: string) { async function onSetPrimaryFile(fileID: string) {

View file

@ -2,6 +2,7 @@
* Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented. * Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented.
### ✨ New Features ### ✨ New Features
* Added URL and Date fields to Images. ([#3015](https://github.com/stashapp/stash/pull/3015))
* Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195)) * Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195))
* Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113)) * Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113))

View file

@ -4,6 +4,7 @@ import {
createStringCriterionOption, createStringCriterionOption,
NullNumberCriterionOption, NullNumberCriterionOption,
createMandatoryTimestampCriterionOption, createMandatoryTimestampCriterionOption,
createDateCriterionOption,
} from "./criteria/criterion"; } from "./criteria/criterion";
import { PerformerFavoriteCriterionOption } from "./criteria/favorite"; import { PerformerFavoriteCriterionOption } from "./criteria/favorite";
import { ImageIsMissingCriterionOption } from "./criteria/is-missing"; import { ImageIsMissingCriterionOption } from "./criteria/is-missing";
@ -24,6 +25,7 @@ const sortByOptions = [
"o_counter", "o_counter",
"filesize", "filesize",
"file_count", "file_count",
"date",
...MediaSortByOptions, ...MediaSortByOptions,
].map(ListFilterOptions.createSortBy); ].map(ListFilterOptions.createSortBy);
@ -44,6 +46,8 @@ const criterionOptions = [
createMandatoryNumberCriterionOption("performer_count"), createMandatoryNumberCriterionOption("performer_count"),
PerformerFavoriteCriterionOption, PerformerFavoriteCriterionOption,
StudiosCriterionOption, StudiosCriterionOption,
createStringCriterionOption("url"),
createDateCriterionOption("date"),
createMandatoryNumberCriterionOption("file_count"), createMandatoryNumberCriterionOption("file_count"),
createMandatoryTimestampCriterionOption("created_at"), createMandatoryTimestampCriterionOption("created_at"),
createMandatoryTimestampCriterionOption("updated_at"), createMandatoryTimestampCriterionOption("updated_at"),

View file

@ -284,6 +284,26 @@ const sanitiseURL = (url?: string, siteURL?: URL) => {
return `https://${url}`; return `https://${url}`;
}; };
const domainFromURL = (urlString?: string, url?: URL) => {
if (url) {
return url.hostname;
} else if (urlString) {
var urlDomain = "";
try {
var sanitizedUrl = sanitiseURL(urlString);
if (sanitizedUrl) {
urlString = sanitizedUrl;
}
urlDomain = new URL(urlString).hostname;
} catch {
urlDomain = urlString; // We cant determine the hostname so we return the base string
}
return urlDomain;
} else {
return "";
}
};
const formatDate = (intl: IntlShape, date?: string, utc = true) => { const formatDate = (intl: IntlShape, date?: string, utc = true) => {
if (!date) { if (!date) {
return ""; return "";
@ -339,6 +359,7 @@ const TextUtils = {
bitRate, bitRate,
resolution, resolution,
sanitiseURL, sanitiseURL,
domainFromURL,
twitterURL, twitterURL,
instagramURL, instagramURL,
formatDate, formatDate,