Merge pull request #554 from photoview/viktorstrate/issue319
Add faces and map to media sidebar
This commit is contained in:
commit
72cb02d981
|
@ -60,6 +60,8 @@ models:
|
||||||
fields:
|
fields:
|
||||||
faceGroup:
|
faceGroup:
|
||||||
resolver: true
|
resolver: true
|
||||||
|
media:
|
||||||
|
resolver: true
|
||||||
FaceRectangle:
|
FaceRectangle:
|
||||||
model: github.com/photoview/photoview/api/graphql/models.FaceRectangle
|
model: github.com/photoview/photoview/api/graphql/models.FaceRectangle
|
||||||
SiteInfo:
|
SiteInfo:
|
||||||
|
|
|
@ -75,6 +75,11 @@ type ComplexityRoot struct {
|
||||||
Token func(childComplexity int) int
|
Token func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Coordinates struct {
|
||||||
|
Latitude func(childComplexity int) int
|
||||||
|
Longitude func(childComplexity int) int
|
||||||
|
}
|
||||||
|
|
||||||
FaceGroup struct {
|
FaceGroup struct {
|
||||||
ID func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
ImageFaceCount func(childComplexity int) int
|
ImageFaceCount func(childComplexity int) int
|
||||||
|
@ -122,6 +127,7 @@ type ComplexityRoot struct {
|
||||||
MediaExif struct {
|
MediaExif struct {
|
||||||
Aperture func(childComplexity int) int
|
Aperture func(childComplexity int) int
|
||||||
Camera func(childComplexity int) int
|
Camera func(childComplexity int) int
|
||||||
|
Coordinates func(childComplexity int) int
|
||||||
DateShot func(childComplexity int) int
|
DateShot func(childComplexity int) int
|
||||||
Exposure func(childComplexity int) int
|
Exposure func(childComplexity int) int
|
||||||
ExposureProgram func(childComplexity int) int
|
ExposureProgram func(childComplexity int) int
|
||||||
|
@ -282,6 +288,8 @@ type FaceGroupResolver interface {
|
||||||
ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error)
|
ImageFaceCount(ctx context.Context, obj *models.FaceGroup) (int, error)
|
||||||
}
|
}
|
||||||
type ImageFaceResolver interface {
|
type ImageFaceResolver interface {
|
||||||
|
Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error)
|
||||||
|
|
||||||
FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error)
|
FaceGroup(ctx context.Context, obj *models.ImageFace) (*models.FaceGroup, error)
|
||||||
}
|
}
|
||||||
type MediaResolver interface {
|
type MediaResolver interface {
|
||||||
|
@ -473,6 +481,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.AuthorizeResult.Token(childComplexity), true
|
return e.complexity.AuthorizeResult.Token(childComplexity), true
|
||||||
|
|
||||||
|
case "Coordinates.latitude":
|
||||||
|
if e.complexity.Coordinates.Latitude == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Coordinates.Latitude(childComplexity), true
|
||||||
|
|
||||||
|
case "Coordinates.longitude":
|
||||||
|
if e.complexity.Coordinates.Longitude == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.Coordinates.Longitude(childComplexity), true
|
||||||
|
|
||||||
case "FaceGroup.id":
|
case "FaceGroup.id":
|
||||||
if e.complexity.FaceGroup.ID == nil {
|
if e.complexity.FaceGroup.ID == nil {
|
||||||
break
|
break
|
||||||
|
@ -695,6 +717,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
||||||
|
|
||||||
return e.complexity.MediaExif.Camera(childComplexity), true
|
return e.complexity.MediaExif.Camera(childComplexity), true
|
||||||
|
|
||||||
|
case "MediaEXIF.coordinates":
|
||||||
|
if e.complexity.MediaExif.Coordinates == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.MediaExif.Coordinates(childComplexity), true
|
||||||
|
|
||||||
case "MediaEXIF.dateShot":
|
case "MediaEXIF.dateShot":
|
||||||
if e.complexity.MediaExif.DateShot == nil {
|
if e.complexity.MediaExif.DateShot == nil {
|
||||||
break
|
break
|
||||||
|
@ -1745,7 +1774,7 @@ type Query {
|
||||||
"Get media owned by the logged in user, returned in GeoJson format"
|
"Get media owned by the logged in user, returned in GeoJson format"
|
||||||
myMediaGeoJson: Any! @isAuthorized
|
myMediaGeoJson: Any! @isAuthorized
|
||||||
"Get the mapbox api token, returns null if mapbox is not enabled"
|
"Get the mapbox api token, returns null if mapbox is not enabled"
|
||||||
mapboxToken: String @isAuthorized
|
mapboxToken: String
|
||||||
|
|
||||||
shareToken(credentials: ShareTokenCredentials!): ShareToken!
|
shareToken(credentials: ShareTokenCredentials!): ShareToken!
|
||||||
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
|
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
|
||||||
|
@ -1955,8 +1984,6 @@ type Album {
|
||||||
path: [Album!]!
|
path: [Album!]!
|
||||||
|
|
||||||
shares: [ShareToken!]!
|
shares: [ShareToken!]!
|
||||||
|
|
||||||
#coverID: Int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaURL {
|
type MediaURL {
|
||||||
|
@ -2029,6 +2056,15 @@ type MediaEXIF {
|
||||||
flash: Int
|
flash: Int
|
||||||
"An index describing the mode for adjusting the exposure of the image"
|
"An index describing the mode for adjusting the exposure of the image"
|
||||||
exposureProgram: Int
|
exposureProgram: Int
|
||||||
|
"GPS coordinates of where the image was taken"
|
||||||
|
coordinates: Coordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinates {
|
||||||
|
"GPS latitude in degrees"
|
||||||
|
latitude: Float!
|
||||||
|
"GPS longitude in degrees"
|
||||||
|
longitude: Float!
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoMetadata {
|
type VideoMetadata {
|
||||||
|
@ -3459,6 +3495,76 @@ func (ec *executionContext) _AuthorizeResult_token(ctx context.Context, field gr
|
||||||
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Coordinates_latitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "Coordinates",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Latitude, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(float64)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNFloat2float64(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Coordinates_longitude(ctx context.Context, field graphql.CollectedField, obj *models.Coordinates) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "Coordinates",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: false,
|
||||||
|
IsResolver: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Longitude, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(float64)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNFloat2float64(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _FaceGroup_id(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) {
|
func (ec *executionContext) _FaceGroup_id(ctx context.Context, field graphql.CollectedField, obj *models.FaceGroup) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -3789,14 +3895,14 @@ func (ec *executionContext) _ImageFace_media(ctx context.Context, field graphql.
|
||||||
Object: "ImageFace",
|
Object: "ImageFace",
|
||||||
Field: field,
|
Field: field,
|
||||||
Args: nil,
|
Args: nil,
|
||||||
IsMethod: false,
|
IsMethod: true,
|
||||||
IsResolver: false,
|
IsResolver: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
ctx = rctx // use context from middleware stack in children
|
||||||
return obj.Media, nil
|
return ec.resolvers.ImageFace().Media(rctx, obj)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
|
@ -3808,9 +3914,9 @@ func (ec *executionContext) _ImageFace_media(ctx context.Context, field graphql.
|
||||||
}
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(models.Media)
|
res := resTmp.(*models.Media)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNMedia2githubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
|
return ec.marshalNMedia2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _ImageFace_rectangle(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) {
|
func (ec *executionContext) _ImageFace_rectangle(ctx context.Context, field graphql.CollectedField, obj *models.ImageFace) (ret graphql.Marshaler) {
|
||||||
|
@ -4853,6 +4959,38 @@ func (ec *executionContext) _MediaEXIF_exposureProgram(ctx context.Context, fiel
|
||||||
return ec.marshalOInt2ᚖint64(ctx, field.Selections, res)
|
return ec.marshalOInt2ᚖint64(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MediaEXIF_coordinates(ctx context.Context, field graphql.CollectedField, obj *models.MediaEXIF) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "MediaEXIF",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: true,
|
||||||
|
IsResolver: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.Coordinates(), nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(*models.Coordinates)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) {
|
func (ec *executionContext) _MediaURL_url(ctx context.Context, field graphql.CollectedField, obj *models.MediaURL) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
|
@ -7283,28 +7421,8 @@ func (ec *executionContext) _Query_mapboxToken(ctx context.Context, field graphq
|
||||||
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
directive0 := func(rctx context.Context) (interface{}, error) {
|
ctx = rctx // use context from middleware stack in children
|
||||||
ctx = rctx // use context from middleware stack in children
|
return ec.resolvers.Query().MapboxToken(rctx)
|
||||||
return ec.resolvers.Query().MapboxToken(rctx)
|
|
||||||
}
|
|
||||||
directive1 := func(ctx context.Context) (interface{}, error) {
|
|
||||||
if ec.directives.IsAuthorized == nil {
|
|
||||||
return nil, errors.New("directive isAuthorized is not implemented")
|
|
||||||
}
|
|
||||||
return ec.directives.IsAuthorized(ctx, nil, directive0)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp, err := directive1(rctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, graphql.ErrorOnPath(ctx, err)
|
|
||||||
}
|
|
||||||
if tmp == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if data, ok := tmp.(*string); ok {
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf(`unexpected type %T from directive, should be *string`, tmp)
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
|
@ -10429,6 +10547,38 @@ func (ec *executionContext) _AuthorizeResult(ctx context.Context, sel ast.Select
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var coordinatesImplementors = []string{"Coordinates"}
|
||||||
|
|
||||||
|
func (ec *executionContext) _Coordinates(ctx context.Context, sel ast.SelectionSet, obj *models.Coordinates) graphql.Marshaler {
|
||||||
|
fields := graphql.CollectFields(ec.OperationContext, sel, coordinatesImplementors)
|
||||||
|
|
||||||
|
out := graphql.NewFieldSet(fields)
|
||||||
|
var invalids uint32
|
||||||
|
for i, field := range fields {
|
||||||
|
switch field.Name {
|
||||||
|
case "__typename":
|
||||||
|
out.Values[i] = graphql.MarshalString("Coordinates")
|
||||||
|
case "latitude":
|
||||||
|
out.Values[i] = ec._Coordinates_latitude(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
|
case "longitude":
|
||||||
|
out.Values[i] = ec._Coordinates_longitude(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.Dispatch()
|
||||||
|
if invalids > 0 {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
var faceGroupImplementors = []string{"FaceGroup"}
|
var faceGroupImplementors = []string{"FaceGroup"}
|
||||||
|
|
||||||
func (ec *executionContext) _FaceGroup(ctx context.Context, sel ast.SelectionSet, obj *models.FaceGroup) graphql.Marshaler {
|
func (ec *executionContext) _FaceGroup(ctx context.Context, sel ast.SelectionSet, obj *models.FaceGroup) graphql.Marshaler {
|
||||||
|
@ -10545,10 +10695,19 @@ func (ec *executionContext) _ImageFace(ctx context.Context, sel ast.SelectionSet
|
||||||
atomic.AddUint32(&invalids, 1)
|
atomic.AddUint32(&invalids, 1)
|
||||||
}
|
}
|
||||||
case "media":
|
case "media":
|
||||||
out.Values[i] = ec._ImageFace_media(ctx, field, obj)
|
field := field
|
||||||
if out.Values[i] == graphql.Null {
|
out.Concurrently(i, func() (res graphql.Marshaler) {
|
||||||
atomic.AddUint32(&invalids, 1)
|
defer func() {
|
||||||
}
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
res = ec._ImageFace_media(ctx, field, obj)
|
||||||
|
if res == graphql.Null {
|
||||||
|
atomic.AddUint32(&invalids, 1)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
})
|
||||||
case "rectangle":
|
case "rectangle":
|
||||||
out.Values[i] = ec._ImageFace_rectangle(ctx, field, obj)
|
out.Values[i] = ec._ImageFace_rectangle(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
|
@ -10824,6 +10983,8 @@ func (ec *executionContext) _MediaEXIF(ctx context.Context, sel ast.SelectionSet
|
||||||
out.Values[i] = ec._MediaEXIF_flash(ctx, field, obj)
|
out.Values[i] = ec._MediaEXIF_flash(ctx, field, obj)
|
||||||
case "exposureProgram":
|
case "exposureProgram":
|
||||||
out.Values[i] = ec._MediaEXIF_exposureProgram(ctx, field, obj)
|
out.Values[i] = ec._MediaEXIF_exposureProgram(ctx, field, obj)
|
||||||
|
case "coordinates":
|
||||||
|
out.Values[i] = ec._MediaEXIF_coordinates(ctx, field, obj)
|
||||||
default:
|
default:
|
||||||
panic("unknown field " + strconv.Quote(field.Name))
|
panic("unknown field " + strconv.Quote(field.Name))
|
||||||
}
|
}
|
||||||
|
@ -12875,6 +13036,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
|
||||||
return graphql.MarshalBoolean(*v)
|
return graphql.MarshalBoolean(*v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalOCoordinates2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐCoordinates(ctx context.Context, sel ast.SelectionSet, v *models.Coordinates) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return ec._Coordinates(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v interface{}) (*float64, error) {
|
func (ec *executionContext) unmarshalOFloat2ᚖfloat64(ctx context.Context, v interface{}) (*float64, error) {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
@ -31,6 +31,19 @@ type ImageFace struct {
|
||||||
Rectangle FaceRectangle `gorm:"not null"`
|
Rectangle FaceRectangle `gorm:"not null"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *ImageFace) FillMedia(db *gorm.DB) error {
|
||||||
|
if f.Media.ID != 0 {
|
||||||
|
// media already exists
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&f).Association("Media").Find(&f.Media); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type FaceDescriptor face.Descriptor
|
type FaceDescriptor face.Descriptor
|
||||||
|
|
||||||
// GormDataType datatype used in database
|
// GormDataType datatype used in database
|
||||||
|
|
|
@ -15,6 +15,13 @@ type AuthorizeResult struct {
|
||||||
Token *string `json:"token"`
|
Token *string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Coordinates struct {
|
||||||
|
// GPS latitude in degrees
|
||||||
|
Latitude float64 `json:"latitude"`
|
||||||
|
// GPS longitude in degrees
|
||||||
|
Longitude float64 `json:"longitude"`
|
||||||
|
}
|
||||||
|
|
||||||
type MediaDownload struct {
|
type MediaDownload struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
MediaURL *MediaURL `json:"mediaUrl"`
|
MediaURL *MediaURL `json:"mediaUrl"`
|
||||||
|
|
|
@ -28,3 +28,14 @@ func (MediaEXIF) TableName() string {
|
||||||
func (exif *MediaEXIF) Media() *Media {
|
func (exif *MediaEXIF) Media() *Media {
|
||||||
panic("not implemented")
|
panic("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (exif *MediaEXIF) Coordinates() *Coordinates {
|
||||||
|
if exif.GPSLatitude == nil || exif.GPSLongitude == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Coordinates{
|
||||||
|
Latitude: *exif.GPSLatitude,
|
||||||
|
Longitude: *exif.GPSLongitude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,14 @@ func (r imageFaceResolver) FaceGroup(ctx context.Context, obj *models.ImageFace)
|
||||||
return &faceGroup, nil
|
return &faceGroup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r imageFaceResolver) Media(ctx context.Context, obj *models.ImageFace) (*models.Media, error) {
|
||||||
|
if err := obj.FillMedia(r.Database); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &obj.Media, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup, paginate *models.Pagination) ([]*models.ImageFace, error) {
|
func (r faceGroupResolver) ImageFaces(ctx context.Context, obj *models.FaceGroup, paginate *models.Pagination) ([]*models.ImageFace, error) {
|
||||||
user := auth.UserFromContext(ctx)
|
user := auth.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
|
|
|
@ -76,7 +76,7 @@ type Query {
|
||||||
"Get media owned by the logged in user, returned in GeoJson format"
|
"Get media owned by the logged in user, returned in GeoJson format"
|
||||||
myMediaGeoJson: Any! @isAuthorized
|
myMediaGeoJson: Any! @isAuthorized
|
||||||
"Get the mapbox api token, returns null if mapbox is not enabled"
|
"Get the mapbox api token, returns null if mapbox is not enabled"
|
||||||
mapboxToken: String @isAuthorized
|
mapboxToken: String
|
||||||
|
|
||||||
shareToken(credentials: ShareTokenCredentials!): ShareToken!
|
shareToken(credentials: ShareTokenCredentials!): ShareToken!
|
||||||
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
|
shareTokenValidatePassword(credentials: ShareTokenCredentials!): Boolean!
|
||||||
|
@ -358,6 +358,15 @@ type MediaEXIF {
|
||||||
flash: Int
|
flash: Int
|
||||||
"An index describing the mode for adjusting the exposure of the image"
|
"An index describing the mode for adjusting the exposure of the image"
|
||||||
exposureProgram: Int
|
exposureProgram: Int
|
||||||
|
"GPS coordinates of where the image was taken"
|
||||||
|
coordinates: Coordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
type Coordinates {
|
||||||
|
"GPS latitude in degrees"
|
||||||
|
latitude: Float!
|
||||||
|
"GPS longitude in degrees"
|
||||||
|
longitude: Float!
|
||||||
}
|
}
|
||||||
|
|
||||||
type VideoMetadata {
|
type VideoMetadata {
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
skipDefaultValues: locale => locale != 'en',
|
skipDefaultValues: locale => locale != 'en',
|
||||||
sort: true,
|
sort: true,
|
||||||
locales: ['da', 'de', 'en', 'es', 'fr', 'it', 'pl', 'ru', 'sv'],
|
locales: [
|
||||||
|
'da',
|
||||||
|
'de',
|
||||||
|
'en',
|
||||||
|
'es',
|
||||||
|
'fr',
|
||||||
|
'it',
|
||||||
|
'pl',
|
||||||
|
'pt',
|
||||||
|
'ru',
|
||||||
|
'sv',
|
||||||
|
'zh-CN',
|
||||||
|
'zh-HK',
|
||||||
|
],
|
||||||
input: 'src/**/*.{js,ts,jsx,tsx}',
|
input: 'src/**/*.{js,ts,jsx,tsx}',
|
||||||
output: 'src/extractedTranslations/$LOCALE/$NAMESPACE.json',
|
output: 'src/extractedTranslations/$LOCALE/$NAMESPACE.json',
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"@apollo/client": "^3.3.21",
|
"@apollo/client": "^3.3.21",
|
||||||
"@babel/preset-typescript": "^7.14.5",
|
"@babel/preset-typescript": "^7.14.5",
|
||||||
"@craco/craco": "^6.2.0",
|
"@craco/craco": "^6.2.0",
|
||||||
"@headlessui/react": "^1.3.0",
|
"@headlessui/react": "^1.4.1",
|
||||||
"@react-aria/focus": "^3.4.0",
|
"@react-aria/focus": "^3.4.0",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@types/geojson": "^7946.0.8",
|
"@types/geojson": "^7946.0.8",
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^17.0.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"subscriptions-transport-ws": "^0.9.19",
|
"subscriptions-transport-ws": "^0.9.19",
|
||||||
|
"tailwind-override": "^0.2.3",
|
||||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
"url-join": "^4.0.1"
|
"url-join": "^4.0.1"
|
||||||
|
@ -2102,9 +2103,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@headlessui/react": {
|
"node_modules/@headlessui/react": {
|
||||||
"version": "1.3.0",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
|
||||||
"integrity": "sha512-2gqTO6BQ3Jr8vDX1B67n1gl6MGKTt6DBmR+H0qxwj0gTMnR2+Qpktj8alRWxsZBODyOiBb77QSQpE/6gG3MX4Q==",
|
"integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
|
@ -24041,6 +24042,11 @@
|
||||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-override": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-override/-/tailwind-override-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-psWRqXL3TiI2h/YtzRq7dwKO6N7CrsEs4v99rNHgqEclfx4IioM0cHZ9O6pzerV3E6bZi6DhCbeq0z67Xs5PIQ=="
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"name": "@tailwindcss/postcss7-compat",
|
"name": "@tailwindcss/postcss7-compat",
|
||||||
"version": "2.2.4",
|
"version": "2.2.4",
|
||||||
|
@ -28796,9 +28802,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@headlessui/react": {
|
"@headlessui/react": {
|
||||||
"version": "1.3.0",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@headlessui/react/-/react-1.4.1.tgz",
|
||||||
"integrity": "sha512-2gqTO6BQ3Jr8vDX1B67n1gl6MGKTt6DBmR+H0qxwj0gTMnR2+Qpktj8alRWxsZBODyOiBb77QSQpE/6gG3MX4Q==",
|
"integrity": "sha512-gL6Ns5xQM57cZBzX6IVv6L7nsam8rDEpRhs5fg28SN64ikfmuuMgunc+Rw5C1cMScnvFM+cz32ueVrlSFEVlSg==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@humanwhocodes/config-array": {
|
"@humanwhocodes/config-array": {
|
||||||
|
@ -46037,6 +46043,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"tailwind-override": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-override/-/tailwind-override-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-psWRqXL3TiI2h/YtzRq7dwKO6N7CrsEs4v99rNHgqEclfx4IioM0cHZ9O6pzerV3E6bZi6DhCbeq0z67Xs5PIQ=="
|
||||||
|
},
|
||||||
"tailwindcss": {
|
"tailwindcss": {
|
||||||
"version": "npm:@tailwindcss/postcss7-compat@2.2.4",
|
"version": "npm:@tailwindcss/postcss7-compat@2.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss7-compat/-/postcss7-compat-2.2.4.tgz",
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"@apollo/client": "^3.3.21",
|
"@apollo/client": "^3.3.21",
|
||||||
"@babel/preset-typescript": "^7.14.5",
|
"@babel/preset-typescript": "^7.14.5",
|
||||||
"@craco/craco": "^6.2.0",
|
"@craco/craco": "^6.2.0",
|
||||||
"@headlessui/react": "^1.3.0",
|
"@headlessui/react": "^1.4.1",
|
||||||
"@react-aria/focus": "^3.4.0",
|
"@react-aria/focus": "^3.4.0",
|
||||||
"@rollup/plugin-babel": "^5.3.0",
|
"@rollup/plugin-babel": "^5.3.0",
|
||||||
"@types/geojson": "^7946.0.8",
|
"@types/geojson": "^7946.0.8",
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
"react-test-renderer": "^17.0.2",
|
"react-test-renderer": "^17.0.2",
|
||||||
"styled-components": "^5.3.0",
|
"styled-components": "^5.3.0",
|
||||||
"subscriptions-transport-ws": "^0.9.19",
|
"subscriptions-transport-ws": "^0.9.19",
|
||||||
|
"tailwind-override": "^0.2.3",
|
||||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4",
|
||||||
"typescript": "^4.3.5",
|
"typescript": "^4.3.5",
|
||||||
"url-join": "^4.0.1"
|
"url-join": "^4.0.1"
|
||||||
|
@ -63,7 +64,7 @@
|
||||||
"lint:types": "tsc --noemit",
|
"lint:types": "tsc --noemit",
|
||||||
"jest": "craco test --setupFilesAfterEnv ./testing/setupTests.ts",
|
"jest": "craco test --setupFilesAfterEnv ./testing/setupTests.ts",
|
||||||
"jest:ci": "CI=true craco test --setupFilesAfterEnv ./testing/setupTests.ts --verbose --ci --coverage",
|
"jest:ci": "CI=true craco test --setupFilesAfterEnv ./testing/setupTests.ts --verbose --ci --coverage",
|
||||||
"genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts",
|
"genSchemaTypes": "apollo client:codegen --target=typescript --globalTypesFile=src/__generated__/globalTypes.ts && prettier --write */**/__generated__/*.ts",
|
||||||
"extractTranslations": "i18next -c i18next-parser.config.js",
|
"extractTranslations": "i18next -c i18next-parser.config.js",
|
||||||
"prepare": "(cd .. && npx husky install)"
|
"prepare": "(cd .. && npx husky install)"
|
||||||
},
|
},
|
||||||
|
@ -71,12 +72,12 @@
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^12.0.0",
|
"@testing-library/react": "^12.0.0",
|
||||||
"@testing-library/user-event": "^13.1.9",
|
"@testing-library/user-event": "^13.1.9",
|
||||||
|
"apollo": "2.33.4",
|
||||||
|
"apollo-language-server": "1.26.3",
|
||||||
"husky": "^6.0.0",
|
"husky": "^6.0.0",
|
||||||
"i18next-parser": "^4.2.0",
|
"i18next-parser": "^4.2.0",
|
||||||
"lint-staged": "^11.0.1",
|
"lint-staged": "^11.0.1",
|
||||||
"tsc-files": "^1.1.2",
|
"tsc-files": "^1.1.2"
|
||||||
"apollo": "2.33.4",
|
|
||||||
"apollo-language-server": "1.26.3"
|
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
|
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import PeoplePage, {
|
import PeoplePage, {
|
||||||
FaceDetails,
|
FaceDetails,
|
||||||
|
FaceGroup,
|
||||||
MY_FACES_QUERY,
|
MY_FACES_QUERY,
|
||||||
SET_GROUP_LABEL_MUTATION,
|
SET_GROUP_LABEL_MUTATION,
|
||||||
} from './PeoplePage'
|
} from './PeoplePage'
|
||||||
|
@ -144,7 +145,11 @@ describe('FaceDetails component', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MockedProvider mocks={[]} addTypename={false}>
|
<MockedProvider mocks={[]} addTypename={false}>
|
||||||
<FaceDetails group={emptyFaceGroup} />
|
<FaceDetails
|
||||||
|
editLabel={false}
|
||||||
|
setEditLabel={jest.fn()}
|
||||||
|
group={emptyFaceGroup}
|
||||||
|
/>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -159,7 +164,11 @@ describe('FaceDetails component', () => {
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MockedProvider mocks={[]} addTypename={false}>
|
<MockedProvider mocks={[]} addTypename={false}>
|
||||||
<FaceDetails group={labeledFaceGroup} />
|
<FaceDetails
|
||||||
|
editLabel={false}
|
||||||
|
setEditLabel={jest.fn()}
|
||||||
|
group={labeledFaceGroup}
|
||||||
|
/>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -190,7 +199,9 @@ describe('FaceDetails component', () => {
|
||||||
]
|
]
|
||||||
render(
|
render(
|
||||||
<MockedProvider mocks={graphqlMocks} addTypename={false}>
|
<MockedProvider mocks={graphqlMocks} addTypename={false}>
|
||||||
<FaceDetails group={faceGroup} />
|
<MemoryRouter>
|
||||||
|
<FaceGroup group={faceGroup} />
|
||||||
|
</MemoryRouter>
|
||||||
</MockedProvider>
|
</MockedProvider>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -211,4 +222,30 @@ describe('FaceDetails component', () => {
|
||||||
expect(graphqlMocks[0].newData).toHaveBeenCalled()
|
expect(graphqlMocks[0].newData).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('cancel add label to face group', async () => {
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[]} addTypename={false}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<FaceGroup group={faceGroup} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
const btn = screen.getByRole('button')
|
||||||
|
expect(btn).toBeInTheDocument()
|
||||||
|
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Unlabeled')).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.click(btn)
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
expect(input).toBeInTheDocument()
|
||||||
|
expect(input).toHaveValue('')
|
||||||
|
|
||||||
|
fireEvent.change(input, { target: { value: 'John Doe' } })
|
||||||
|
fireEvent.keyDown(input, { key: 'Escape', code: 'Escape' })
|
||||||
|
|
||||||
|
expect(screen.queryByText('Unlabeled')).toBeInTheDocument()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
myFaces_myFaceGroups,
|
myFaces_myFaceGroups,
|
||||||
} from './__generated__/myFaces'
|
} from './__generated__/myFaces'
|
||||||
import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces'
|
import { recognizeUnlabeledFaces } from './__generated__/recognizeUnlabeledFaces'
|
||||||
|
import { tailwindClassNames } from '../../helpers/utils'
|
||||||
|
|
||||||
export const MY_FACES_QUERY = gql`
|
export const MY_FACES_QUERY = gql`
|
||||||
query myFaces($limit: Int, $offset: Int) {
|
query myFaces($limit: Int, $offset: Int) {
|
||||||
|
@ -65,15 +66,8 @@ const RECOGNIZE_UNLABELED_FACES_MUTATION = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const FaceDetailsWrapper = styled.div<{ labeled: boolean }>`
|
const FaceDetailsWrapper = styled.span<{ labeled: boolean }>`
|
||||||
color: ${({ labeled }) => (labeled ? 'black' : '#aaa')};
|
color: ${({ labeled }) => (labeled ? 'black' : '#aaa')};
|
||||||
width: 150px;
|
|
||||||
margin: 12px auto 24px;
|
|
||||||
text-align: center;
|
|
||||||
display: block;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-visible {
|
&:focus-visible {
|
||||||
|
@ -82,12 +76,26 @@ const FaceDetailsWrapper = styled.div<{ labeled: boolean }>`
|
||||||
`
|
`
|
||||||
|
|
||||||
type FaceDetailsProps = {
|
type FaceDetailsProps = {
|
||||||
group: myFaces_myFaceGroups
|
group: {
|
||||||
|
__typename: 'FaceGroup'
|
||||||
|
id: string
|
||||||
|
label: string | null
|
||||||
|
imageFaceCount: number
|
||||||
|
}
|
||||||
|
className?: string
|
||||||
|
textFieldClassName?: string
|
||||||
|
editLabel: boolean
|
||||||
|
setEditLabel: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FaceDetails = ({ group }: FaceDetailsProps) => {
|
export const FaceDetails = ({
|
||||||
|
group,
|
||||||
|
className,
|
||||||
|
textFieldClassName,
|
||||||
|
editLabel,
|
||||||
|
setEditLabel,
|
||||||
|
}: FaceDetailsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [editLabel, setEditLabel] = useState(false)
|
|
||||||
const [inputValue, setInputValue] = useState(group.label ?? '')
|
const [inputValue, setInputValue] = useState(group.label ?? '')
|
||||||
const inputRef = createRef<HTMLInputElement>()
|
const inputRef = createRef<HTMLInputElement>()
|
||||||
|
|
||||||
|
@ -126,11 +134,15 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => {
|
||||||
if (!editLabel) {
|
if (!editLabel) {
|
||||||
label = (
|
label = (
|
||||||
<FaceDetailsWrapper
|
<FaceDetailsWrapper
|
||||||
|
className={tailwindClassNames(
|
||||||
|
className,
|
||||||
|
'whitespace-nowrap inline-block overflow-hidden overflow-clip'
|
||||||
|
)}
|
||||||
labeled={!!group.label}
|
labeled={!!group.label}
|
||||||
onClick={() => setEditLabel(true)}
|
onClick={() => setEditLabel(true)}
|
||||||
>
|
>
|
||||||
<FaceImagesCount>{group.imageFaceCount}</FaceImagesCount>
|
<FaceImagesCount>{group.imageFaceCount}</FaceImagesCount>
|
||||||
<button>
|
<button className="">
|
||||||
{group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')}
|
{group.label ?? t('people_page.face_group.unlabeled', 'Unlabeled')}
|
||||||
</button>
|
</button>
|
||||||
{/* <EditIcon name="pencil" /> */}
|
{/* <EditIcon name="pencil" /> */}
|
||||||
|
@ -138,9 +150,9 @@ export const FaceDetails = ({ group }: FaceDetailsProps) => {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
label = (
|
label = (
|
||||||
<FaceDetailsWrapper labeled={!!group.label}>
|
<FaceDetailsWrapper className={className} labeled={!!group.label}>
|
||||||
<TextField
|
<TextField
|
||||||
className="w-[160px]"
|
className={textFieldClassName}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
// size="mini"
|
// size="mini"
|
||||||
|
@ -181,15 +193,22 @@ type FaceGroupProps = {
|
||||||
group: myFaces_myFaceGroups
|
group: myFaces_myFaceGroups
|
||||||
}
|
}
|
||||||
|
|
||||||
const FaceGroup = ({ group }: FaceGroupProps) => {
|
export const FaceGroup = ({ group }: FaceGroupProps) => {
|
||||||
const previewFace = group.imageFaces[0]
|
const previewFace = group.imageFaces[0]
|
||||||
|
const [editLabel, setEditLabel] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ margin: '12px' }}>
|
<div style={{ margin: '12px' }}>
|
||||||
<Link to={`/people/${group.id}`}>
|
<Link to={`/people/${group.id}`}>
|
||||||
<FaceCircleImage imageFace={previewFace} selectable />
|
<FaceCircleImage imageFace={previewFace} selectable />
|
||||||
</Link>
|
</Link>
|
||||||
<FaceDetails group={group} />
|
<FaceDetails
|
||||||
|
className="block cursor-pointer text-center w-full mt-3"
|
||||||
|
textFieldClassName="w-[140px]"
|
||||||
|
group={group}
|
||||||
|
editLabel={editLabel}
|
||||||
|
setEditLabel={setEditLabel}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { gql, useMutation } from '@apollo/client'
|
import { BaseMutationOptions, gql, useMutation } from '@apollo/client'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
@ -28,16 +28,50 @@ const DETACH_IMAGE_FACES_MUTATION = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const useDetachImageFaces = (
|
||||||
|
mutationOptions: BaseMutationOptions<
|
||||||
|
detachImageFaces,
|
||||||
|
detachImageFacesVariables
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
const [detachImageFacesMutation] = useMutation<
|
||||||
|
detachImageFaces,
|
||||||
|
detachImageFacesVariables
|
||||||
|
>(DETACH_IMAGE_FACES_MUTATION, mutationOptions)
|
||||||
|
|
||||||
|
return async (
|
||||||
|
selectedImageFaces: (
|
||||||
|
| myFaces_myFaceGroups_imageFaces
|
||||||
|
| singleFaceGroup_faceGroup_imageFaces
|
||||||
|
)[]
|
||||||
|
) => {
|
||||||
|
const faceIDs = selectedImageFaces.map(face => face.id)
|
||||||
|
|
||||||
|
const result = await detachImageFacesMutation({
|
||||||
|
variables: {
|
||||||
|
faceIDs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type DetachImageFacesModalProps = {
|
type DetachImageFacesModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen(open: boolean): void
|
setOpen(open: boolean): void
|
||||||
faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup
|
faceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup
|
||||||
|
selectedImageFaces?: (
|
||||||
|
| myFaces_myFaceGroups_imageFaces
|
||||||
|
| singleFaceGroup_faceGroup_imageFaces
|
||||||
|
)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const DetachImageFacesModal = ({
|
const DetachImageFacesModal = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
faceGroup,
|
faceGroup,
|
||||||
|
selectedImageFaces: selectedImageFacesProp,
|
||||||
}: DetachImageFacesModalProps) => {
|
}: DetachImageFacesModalProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
@ -46,10 +80,7 @@ const DetachImageFacesModal = ({
|
||||||
>([])
|
>([])
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
const [detachImageFacesMutation] = useMutation<
|
const detachImageFacesMutation = useDetachImageFaces({
|
||||||
detachImageFaces,
|
|
||||||
detachImageFacesVariables
|
|
||||||
>(DETACH_IMAGE_FACES_MUTATION, {
|
|
||||||
refetchQueries: [
|
refetchQueries: [
|
||||||
{
|
{
|
||||||
query: MY_FACES_QUERY,
|
query: MY_FACES_QUERY,
|
||||||
|
@ -57,6 +88,19 @@ const DetachImageFacesModal = ({
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const detachImageFaces = () => {
|
||||||
|
detachImageFacesMutation(selectedImageFaces).then(({ data }) => {
|
||||||
|
if (isNil(data)) throw new Error('Expected data not to be null')
|
||||||
|
setOpen(false)
|
||||||
|
history.push(`/people/${data.detachImageFaces.id}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNil(selectedImageFacesProp)) return
|
||||||
|
setSelectedImageFaces(selectedImageFacesProp)
|
||||||
|
}, [selectedImageFacesProp])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
setSelectedImageFaces([])
|
setSelectedImageFaces([])
|
||||||
|
@ -65,20 +109,6 @@ const DetachImageFacesModal = ({
|
||||||
|
|
||||||
if (open == false) return null
|
if (open == false) return null
|
||||||
|
|
||||||
const detachImageFaces = () => {
|
|
||||||
const faceIDs = selectedImageFaces.map(face => face.id)
|
|
||||||
|
|
||||||
detachImageFacesMutation({
|
|
||||||
variables: {
|
|
||||||
faceIDs,
|
|
||||||
},
|
|
||||||
}).then(({ data }) => {
|
|
||||||
if (isNil(data)) throw new Error('Expected data not to be null')
|
|
||||||
setOpen(false)
|
|
||||||
history.push(`/people/${data.detachImageFaces.id}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageFaces = faceGroup?.imageFaces ?? []
|
const imageFaces = faceGroup?.imageFaces ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -94,7 +124,7 @@ const DetachImageFacesModal = ({
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
key: 'cancel',
|
key: 'cancel',
|
||||||
label: 'Cancel',
|
label: t('general.action.cancel', 'Cancel'),
|
||||||
onClick: () => setOpen(false),
|
onClick: () => setOpen(false),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,7 +8,7 @@ import React, {
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { isNil } from '../../../helpers/utils'
|
import { isNil } from '../../../helpers/utils'
|
||||||
import { Button, TextField } from '../../../primitives/form/Input'
|
import { Button, TextField } from '../../../primitives/form/Input'
|
||||||
import { SET_GROUP_LABEL_MUTATION } from '../PeoplePage'
|
import { MY_FACES_QUERY, SET_GROUP_LABEL_MUTATION } from '../PeoplePage'
|
||||||
import {
|
import {
|
||||||
setGroupLabel,
|
setGroupLabel,
|
||||||
setGroupLabelVariables,
|
setGroupLabelVariables,
|
||||||
|
@ -112,6 +112,11 @@ const FaceGroupTitle = ({ faceGroup }: FaceGroupTitleProps) => {
|
||||||
open={mergeModalOpen}
|
open={mergeModalOpen}
|
||||||
setOpen={setMergeModalOpen}
|
setOpen={setMergeModalOpen}
|
||||||
sourceFaceGroup={faceGroup}
|
sourceFaceGroup={faceGroup}
|
||||||
|
refetchQueries={[
|
||||||
|
{
|
||||||
|
query: MY_FACES_QUERY,
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
<MoveImageFacesModal
|
<MoveImageFacesModal
|
||||||
open={moveModalOpen}
|
open={moveModalOpen}
|
||||||
|
@ -133,16 +138,24 @@ const FaceGroupTitle = ({ faceGroup }: FaceGroupTitleProps) => {
|
||||||
<div className="mb-2">{title}</div>
|
<div className="mb-2">{title}</div>
|
||||||
<ul className="flex gap-2 flex-wrap mb-6">
|
<ul className="flex gap-2 flex-wrap mb-6">
|
||||||
<li>
|
<li>
|
||||||
<Button onClick={() => setEditLabel(true)}>{t('people_page.action_label.change_label', 'Change label')}</Button>
|
<Button onClick={() => setEditLabel(true)}>
|
||||||
|
{t('people_page.action_label.change_label', 'Change label')}
|
||||||
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Button onClick={() => setMergeModalOpen(true)}>{t('people_page.action_label.merge_face', 'Merge face')}</Button>
|
<Button onClick={() => setMergeModalOpen(true)}>
|
||||||
|
{t('people_page.action_label.merge_people', 'Merge people')}
|
||||||
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Button onClick={() => setDetachModalOpen(true)}>{t('people_page.action_label.detach_face', 'Detach face')}</Button>
|
<Button onClick={() => setDetachModalOpen(true)}>
|
||||||
|
{t('people_page.action_label.detach_images', 'Detach images')}
|
||||||
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Button onClick={() => setMoveModalOpen(true)}>{t('people_page.action_label.move_faces', 'Moves faces')}</Button>
|
<Button onClick={() => setMoveModalOpen(true)}>
|
||||||
|
{t('people_page.action_label.move_faces', 'Move faces')}
|
||||||
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { gql, useMutation, useQuery } from '@apollo/client'
|
import { gql, PureQueryOptions, useMutation, useQuery } from '@apollo/client'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useHistory } from 'react-router-dom'
|
import { useHistory } from 'react-router-dom'
|
||||||
|
@ -31,18 +31,24 @@ const COMBINE_FACES_MUTATION = gql`
|
||||||
type MergeFaceGroupsModalProps = {
|
type MergeFaceGroupsModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen(open: boolean): void
|
setOpen(open: boolean): void
|
||||||
sourceFaceGroup: myFaces_myFaceGroups | singleFaceGroup_faceGroup
|
sourceFaceGroup: {
|
||||||
|
__typename: 'FaceGroup'
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
refetchQueries: PureQueryOptions[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MergeFaceGroupsModal = ({
|
const MergeFaceGroupsModal = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
sourceFaceGroup,
|
sourceFaceGroup,
|
||||||
|
refetchQueries,
|
||||||
}: MergeFaceGroupsModalProps) => {
|
}: MergeFaceGroupsModalProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [selectedFaceGroup, setSelectedFaceGroup] =
|
const [selectedFaceGroup, setSelectedFaceGroup] = useState<
|
||||||
useState<myFaces_myFaceGroups | singleFaceGroup_faceGroup | null>(null)
|
myFaces_myFaceGroups | singleFaceGroup_faceGroup | null
|
||||||
|
>(null)
|
||||||
|
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
const { data } = useQuery<myFaces, myFacesVariables>(MY_FACES_QUERY)
|
const { data } = useQuery<myFaces, myFacesVariables>(MY_FACES_QUERY)
|
||||||
|
@ -50,11 +56,7 @@ const MergeFaceGroupsModal = ({
|
||||||
combineFaces,
|
combineFaces,
|
||||||
combineFacesVariables
|
combineFacesVariables
|
||||||
>(COMBINE_FACES_MUTATION, {
|
>(COMBINE_FACES_MUTATION, {
|
||||||
refetchQueries: [
|
refetchQueries: refetchQueries,
|
||||||
{
|
|
||||||
query: MY_FACES_QUERY,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (open == false) return null
|
if (open == false) return null
|
||||||
|
|
|
@ -40,20 +40,26 @@ type MoveImageFacesModalProps = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
faceGroup: singleFaceGroup_faceGroup
|
faceGroup: singleFaceGroup_faceGroup
|
||||||
|
preselectedImageFaces?: (
|
||||||
|
| singleFaceGroup_faceGroup_imageFaces
|
||||||
|
| myFaces_myFaceGroups_imageFaces
|
||||||
|
)[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const MoveImageFacesModal = ({
|
const MoveImageFacesModal = ({
|
||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
faceGroup,
|
faceGroup,
|
||||||
|
preselectedImageFaces,
|
||||||
}: MoveImageFacesModalProps) => {
|
}: MoveImageFacesModalProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [selectedImageFaces, setSelectedImageFaces] = useState<
|
const [selectedImageFaces, setSelectedImageFaces] = useState<
|
||||||
(singleFaceGroup_faceGroup_imageFaces | myFaces_myFaceGroups_imageFaces)[]
|
(singleFaceGroup_faceGroup_imageFaces | myFaces_myFaceGroups_imageFaces)[]
|
||||||
>([])
|
>([])
|
||||||
const [selectedFaceGroup, setSelectedFaceGroup] =
|
const [selectedFaceGroup, setSelectedFaceGroup] = useState<
|
||||||
useState<myFaces_myFaceGroups | singleFaceGroup_faceGroup | null>(null)
|
myFaces_myFaceGroups | singleFaceGroup_faceGroup | null
|
||||||
|
>(null)
|
||||||
const [imagesSelected, setImagesSelected] = useState(false)
|
const [imagesSelected, setImagesSelected] = useState(false)
|
||||||
const history = useHistory()
|
const history = useHistory()
|
||||||
|
|
||||||
|
@ -68,8 +74,16 @@ const MoveImageFacesModal = ({
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const [loadFaceGroups, { data: faceGroupsData }] =
|
const [loadFaceGroups, { data: faceGroupsData }] = useLazyQuery<
|
||||||
useLazyQuery<myFaces, myFacesVariables>(MY_FACES_QUERY)
|
myFaces,
|
||||||
|
myFacesVariables
|
||||||
|
>(MY_FACES_QUERY)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isNil(preselectedImageFaces)) return
|
||||||
|
setSelectedImageFaces(preselectedImageFaces)
|
||||||
|
setImagesSelected(true)
|
||||||
|
}, [preselectedImageFaces])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (imagesSelected) {
|
if (imagesSelected) {
|
||||||
|
|
|
@ -52,14 +52,6 @@ const MapClusterMarker = ({
|
||||||
const thumbnail = JSON.parse(marker.thumbnail)
|
const thumbnail = JSON.parse(marker.thumbnail)
|
||||||
|
|
||||||
const presentMedia = () => {
|
const presentMedia = () => {
|
||||||
// presentMarkerClicked({
|
|
||||||
// dispatchMedia: dispatchMarkerMedia,
|
|
||||||
// mediaState: markerMediaState,
|
|
||||||
// marker: {
|
|
||||||
// cluster: !!marker.cluster,
|
|
||||||
// id: marker.cluster ? marker.cluster_id : marker.media_id,
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
dispatchMarkerMedia({
|
dispatchMarkerMedia({
|
||||||
type: 'replacePresentMarker',
|
type: 'replacePresentMarker',
|
||||||
marker: {
|
marker: {
|
||||||
|
@ -67,10 +59,6 @@ const MapClusterMarker = ({
|
||||||
id: marker.cluster ? marker.cluster_id : marker.media_id,
|
id: marker.cluster ? marker.cluster_id : marker.media_id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// setPresentMarker({
|
|
||||||
// cluster: !!marker.cluster,
|
|
||||||
// id: marker.cluster ? marker.cluster_id : marker.media_id,
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -80,6 +80,9 @@ type MapPresetMarkerProps = {
|
||||||
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Full-screen present-view that works with PlacesState
|
||||||
|
*/
|
||||||
const MapPresentMarker = ({
|
const MapPresentMarker = ({
|
||||||
map,
|
map,
|
||||||
markerMediaState,
|
markerMediaState,
|
||||||
|
|
|
@ -1,30 +1,24 @@
|
||||||
import { gql, useQuery } from '@apollo/client'
|
import { gql, useQuery } from '@apollo/client'
|
||||||
import type mapboxgl from 'mapbox-gl'
|
import type mapboxgl from 'mapbox-gl'
|
||||||
import React, { useEffect, useReducer, useRef, useState } from 'react'
|
import React, { useReducer } from 'react'
|
||||||
import { Helmet } from 'react-helmet'
|
import { Helmet } from 'react-helmet'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import Layout from '../../components/layout/Layout'
|
import Layout from '../../components/layout/Layout'
|
||||||
import { makeUpdateMarkers } from './mapboxHelperFunctions'
|
import { registerMediaMarkers } from '../../components/mapbox/mapboxHelperFunctions'
|
||||||
import MapPresentMarker from './MapPresentMarker'
|
import useMapboxMap from '../../components/mapbox/MapboxMap'
|
||||||
import { urlPresentModeSetupHook } from '../../components/photoGallery/photoGalleryReducer'
|
import { urlPresentModeSetupHook } from '../../components/photoGallery/photoGalleryReducer'
|
||||||
import { placesReducer } from './placesReducer'
|
import MapPresentMarker from './MapPresentMarker'
|
||||||
|
import { PlacesAction, placesReducer } from './placesReducer'
|
||||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
import { mediaGeoJson } from './__generated__/mediaGeoJson'
|
||||||
|
|
||||||
const MapWrapper = styled.div`
|
const MapWrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 120px);
|
height: calc(100vh - 120px);
|
||||||
`
|
`
|
||||||
|
|
||||||
const MapContainer = styled.div`
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
`
|
|
||||||
|
|
||||||
const MAPBOX_DATA_QUERY = gql`
|
const MAPBOX_DATA_QUERY = gql`
|
||||||
query placePageMapboxToken {
|
query mediaGeoJson {
|
||||||
mapboxToken
|
|
||||||
myMediaGeoJson
|
myMediaGeoJson
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
@ -37,10 +31,9 @@ export type PresentMarker = {
|
||||||
const MapPage = () => {
|
const MapPage = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const [mapboxLibrary, setMapboxLibrary] = useState<typeof mapboxgl | null>()
|
const { data: mapboxData } = useQuery<mediaGeoJson>(MAPBOX_DATA_QUERY, {
|
||||||
const mapContainer = useRef<HTMLDivElement | null>(null)
|
fetchPolicy: 'cache-first',
|
||||||
const map = useRef<mapboxgl.Map | null>(null)
|
})
|
||||||
// const [presentMarker, setPresentMarker] = useState<PresentMarker | null>(null)
|
|
||||||
|
|
||||||
const [markerMediaState, dispatchMarkerMedia] = useReducer(placesReducer, {
|
const [markerMediaState, dispatchMarkerMedia] = useReducer(placesReducer, {
|
||||||
presenting: false,
|
presenting: false,
|
||||||
|
@ -48,76 +41,21 @@ const MapPage = () => {
|
||||||
media: [],
|
media: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: mapboxData } = useQuery(MAPBOX_DATA_QUERY)
|
const { mapContainer, mapboxMap, mapboxToken } = useMapboxMap({
|
||||||
|
configureMapbox: configureMapbox({ mapboxData, dispatchMarkerMedia }),
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
urlPresentModeSetupHook({
|
||||||
async function loadMapboxLibrary() {
|
dispatchMedia: dispatchMarkerMedia,
|
||||||
const mapbox = (await import('mapbox-gl')).default
|
openPresentMode: event => {
|
||||||
|
dispatchMarkerMedia({
|
||||||
setMapboxLibrary(mapbox)
|
type: 'openPresentMode',
|
||||||
}
|
activeIndex: event.state.activeIndex,
|
||||||
loadMapboxLibrary()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
mapboxLibrary == null ||
|
|
||||||
mapContainer.current == null ||
|
|
||||||
mapboxData == null ||
|
|
||||||
map.current != null
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mapboxLibrary.accessToken = mapboxData.mapboxToken
|
|
||||||
|
|
||||||
map.current = new mapboxLibrary.Map({
|
|
||||||
container: mapContainer.current,
|
|
||||||
style: 'mapbox://styles/mapbox/streets-v11',
|
|
||||||
zoom: 1,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Add map navigation control
|
|
||||||
map.current.addControl(new mapboxLibrary.NavigationControl())
|
|
||||||
|
|
||||||
map.current.on('load', () => {
|
|
||||||
if (map.current == null) {
|
|
||||||
console.error('ERROR: map is null')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
map.current.addSource('media', {
|
|
||||||
type: 'geojson',
|
|
||||||
data: mapboxData.myMediaGeoJson,
|
|
||||||
cluster: true,
|
|
||||||
clusterRadius: 50,
|
|
||||||
clusterProperties: {
|
|
||||||
thumbnail: ['coalesce', ['get', 'thumbnail'], false],
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Add dummy layer for features to be queryable
|
if (mapboxData && mapboxToken == null) {
|
||||||
map.current.addLayer({
|
|
||||||
id: 'media-points',
|
|
||||||
type: 'circle',
|
|
||||||
source: 'media',
|
|
||||||
filter: ['!', true],
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateMarkers = makeUpdateMarkers({
|
|
||||||
map: map.current,
|
|
||||||
mapboxLibrary,
|
|
||||||
dispatchMarkerMedia,
|
|
||||||
})
|
|
||||||
|
|
||||||
map.current.on('move', updateMarkers)
|
|
||||||
map.current.on('moveend', updateMarkers)
|
|
||||||
map.current.on('sourcedata', updateMarkers)
|
|
||||||
updateMarkers()
|
|
||||||
})
|
|
||||||
}, [mapContainer, mapboxLibrary, mapboxData])
|
|
||||||
|
|
||||||
if (mapboxData && mapboxData.mapboxToken == null) {
|
|
||||||
return (
|
return (
|
||||||
<Layout title={t('places_page.title', 'Places')}>
|
<Layout title={t('places_page.title', 'Places')}>
|
||||||
<h1>Mapbox token is not set</h1>
|
<h1>Mapbox token is not set</h1>
|
||||||
|
@ -134,34 +72,64 @@ const MapPage = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
urlPresentModeSetupHook({
|
|
||||||
dispatchMedia: dispatchMarkerMedia,
|
|
||||||
openPresentMode: event => {
|
|
||||||
dispatchMarkerMedia({
|
|
||||||
type: 'openPresentMode',
|
|
||||||
activeIndex: event.state.activeIndex,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout title="Places">
|
<Layout title="Places">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
{/* <link rel="stylesheet" href="/mapbox-gl.css" /> */}
|
{/* <link rel="stylesheet" href="/mapbox-gl.css" /> */}
|
||||||
{/* <style type="text/css">{mapboxStyles}</style> */}
|
{/* <style type="text/css">{mapboxStyles}</style> */}
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<MapWrapper>
|
<MapWrapper>{mapContainer}</MapWrapper>
|
||||||
<MapContainer ref={mapContainer}></MapContainer>
|
|
||||||
</MapWrapper>
|
|
||||||
<MapPresentMarker
|
<MapPresentMarker
|
||||||
map={map.current}
|
map={mapboxMap}
|
||||||
markerMediaState={markerMediaState}
|
markerMediaState={markerMediaState}
|
||||||
dispatchMarkerMedia={dispatchMarkerMedia}
|
dispatchMarkerMedia={dispatchMarkerMedia}
|
||||||
// presentMarker={presentMarker}
|
|
||||||
// setPresentMarker={setPresentMarker}
|
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const configureMapbox =
|
||||||
|
({
|
||||||
|
mapboxData,
|
||||||
|
dispatchMarkerMedia,
|
||||||
|
}: {
|
||||||
|
mapboxData?: mediaGeoJson
|
||||||
|
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
||||||
|
}) =>
|
||||||
|
(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl) => {
|
||||||
|
// Add map navigation control
|
||||||
|
map.addControl(new mapboxLibrary.NavigationControl())
|
||||||
|
|
||||||
|
map.on('load', () => {
|
||||||
|
if (map == null) {
|
||||||
|
console.error('ERROR: map is null')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
map.addSource('media', {
|
||||||
|
type: 'geojson',
|
||||||
|
data: mapboxData?.myMediaGeoJson,
|
||||||
|
cluster: true,
|
||||||
|
clusterRadius: 50,
|
||||||
|
clusterProperties: {
|
||||||
|
thumbnail: ['coalesce', ['get', 'thumbnail'], false],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add dummy layer for features to be queryable
|
||||||
|
map.addLayer({
|
||||||
|
id: 'media-points',
|
||||||
|
type: 'circle',
|
||||||
|
source: 'media',
|
||||||
|
filter: ['!', true],
|
||||||
|
})
|
||||||
|
|
||||||
|
registerMediaMarkers({
|
||||||
|
map: map,
|
||||||
|
mapboxLibrary,
|
||||||
|
dispatchMarkerMedia,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export default MapPage
|
export default MapPage
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: mediaGeoJson
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface mediaGeoJson {
|
||||||
|
/**
|
||||||
|
* Get media owned by the logged in user, returned in GeoJson format
|
||||||
|
*/
|
||||||
|
myMediaGeoJson: any
|
||||||
|
}
|
|
@ -1,80 +0,0 @@
|
||||||
import type mapboxgl from 'mapbox-gl'
|
|
||||||
import type geojson from 'geojson'
|
|
||||||
import React from 'react'
|
|
||||||
import ReactDOM from 'react-dom'
|
|
||||||
import MapClusterMarker from './MapClusterMarker'
|
|
||||||
import { MediaMarker } from './MapPresentMarker'
|
|
||||||
import { PlacesAction } from './placesReducer'
|
|
||||||
|
|
||||||
const markers: { [key: string]: mapboxgl.Marker } = {}
|
|
||||||
let markersOnScreen: typeof markers = {}
|
|
||||||
|
|
||||||
type makeUpdateMarkersArgs = {
|
|
||||||
map: mapboxgl.Map
|
|
||||||
mapboxLibrary: typeof mapboxgl
|
|
||||||
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
|
||||||
// setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeUpdateMarkers = ({
|
|
||||||
map,
|
|
||||||
mapboxLibrary,
|
|
||||||
dispatchMarkerMedia,
|
|
||||||
}: makeUpdateMarkersArgs) => () => {
|
|
||||||
const newMarkers: typeof markers = {}
|
|
||||||
const features = map.querySourceFeatures('media')
|
|
||||||
|
|
||||||
// for every media on the screen, create an HTML marker for it (if we didn't yet),
|
|
||||||
// and add it to the map if it's not there already
|
|
||||||
for (const feature of features) {
|
|
||||||
const point = feature.geometry as geojson.Point
|
|
||||||
const coords = point.coordinates as [number, number]
|
|
||||||
const props = feature.properties as MediaMarker
|
|
||||||
if (props == null) {
|
|
||||||
console.warn('WARN: geojson feature had no properties', feature)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const id = props.cluster
|
|
||||||
? `cluster_${props.cluster_id}`
|
|
||||||
: `media_${props.media_id}`
|
|
||||||
|
|
||||||
let marker = markers[id]
|
|
||||||
if (!marker) {
|
|
||||||
const el = createClusterPopupElement(props, {
|
|
||||||
dispatchMarkerMedia,
|
|
||||||
})
|
|
||||||
marker = markers[id] = new mapboxLibrary.Marker({
|
|
||||||
element: el,
|
|
||||||
}).setLngLat(coords)
|
|
||||||
}
|
|
||||||
newMarkers[id] = marker
|
|
||||||
|
|
||||||
if (!markersOnScreen[id]) marker.addTo(map)
|
|
||||||
}
|
|
||||||
// for every marker we've added previously, remove those that are no longer visible
|
|
||||||
for (const id in markersOnScreen) {
|
|
||||||
if (!newMarkers[id]) markersOnScreen[id].remove()
|
|
||||||
}
|
|
||||||
markersOnScreen = newMarkers
|
|
||||||
}
|
|
||||||
|
|
||||||
function createClusterPopupElement(
|
|
||||||
geojsonProps: MediaMarker,
|
|
||||||
{
|
|
||||||
dispatchMarkerMedia,
|
|
||||||
}: {
|
|
||||||
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
// setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
|
|
||||||
const el = document.createElement('div')
|
|
||||||
ReactDOM.render(
|
|
||||||
<MapClusterMarker
|
|
||||||
marker={geojsonProps}
|
|
||||||
dispatchMarkerMedia={dispatchMarkerMedia}
|
|
||||||
/>,
|
|
||||||
el
|
|
||||||
)
|
|
||||||
return el
|
|
||||||
}
|
|
|
@ -66,6 +66,7 @@ export const SHARE_ALBUM_QUERY = gql`
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
exif {
|
exif {
|
||||||
|
id
|
||||||
camera
|
camera
|
||||||
maker
|
maker
|
||||||
lens
|
lens
|
||||||
|
@ -76,6 +77,10 @@ export const SHARE_ALBUM_QUERY = gql`
|
||||||
focalLength
|
focalLength
|
||||||
flash
|
flash
|
||||||
exposureProgram
|
exposureProgram
|
||||||
|
coordinates {
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
ProtectedVideo,
|
ProtectedVideo,
|
||||||
} from '../../components/photoGallery/ProtectedMedia'
|
} from '../../components/photoGallery/ProtectedMedia'
|
||||||
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
||||||
import MediaSidebar from '../../components/sidebar/MediaSidebar'
|
import MediaSidebar from '../../components/sidebar/MediaSidebar/MediaSidebar'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SharePageToken_shareToken_media } from './__generated__/SharePageToken'
|
import { SharePageToken_shareToken_media } from './__generated__/SharePageToken'
|
||||||
import { MediaType } from '../../__generated__/globalTypes'
|
import { MediaType } from '../../__generated__/globalTypes'
|
||||||
|
|
|
@ -59,6 +59,10 @@ export const SHARE_TOKEN_QUERY = gql`
|
||||||
focalLength
|
focalLength
|
||||||
flash
|
flash
|
||||||
exposureProgram
|
exposureProgram
|
||||||
|
coordinates {
|
||||||
|
longitude
|
||||||
|
latitude
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,172 +3,188 @@
|
||||||
// @generated
|
// @generated
|
||||||
// This file was automatically generated and should not be edited.
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
import { MediaType } from "./../../../__generated__/globalTypes";
|
import { MediaType } from './../../../__generated__/globalTypes'
|
||||||
|
|
||||||
// ====================================================
|
// ====================================================
|
||||||
// GraphQL query operation: SharePageToken
|
// GraphQL query operation: SharePageToken
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_album {
|
export interface SharePageToken_shareToken_album {
|
||||||
__typename: "Album";
|
__typename: 'Album'
|
||||||
id: string;
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media_thumbnail {
|
export interface SharePageToken_shareToken_media_thumbnail {
|
||||||
__typename: "MediaURL";
|
__typename: 'MediaURL'
|
||||||
/**
|
/**
|
||||||
* URL for previewing the image
|
* URL for previewing the image
|
||||||
*/
|
*/
|
||||||
url: string;
|
url: string
|
||||||
/**
|
/**
|
||||||
* Width of the image in pixels
|
* Width of the image in pixels
|
||||||
*/
|
*/
|
||||||
width: number;
|
width: number
|
||||||
/**
|
/**
|
||||||
* Height of the image in pixels
|
* Height of the image in pixels
|
||||||
*/
|
*/
|
||||||
height: number;
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media_downloads_mediaUrl {
|
export interface SharePageToken_shareToken_media_downloads_mediaUrl {
|
||||||
__typename: "MediaURL";
|
__typename: 'MediaURL'
|
||||||
/**
|
/**
|
||||||
* URL for previewing the image
|
* URL for previewing the image
|
||||||
*/
|
*/
|
||||||
url: string;
|
url: string
|
||||||
/**
|
/**
|
||||||
* Width of the image in pixels
|
* Width of the image in pixels
|
||||||
*/
|
*/
|
||||||
width: number;
|
width: number
|
||||||
/**
|
/**
|
||||||
* Height of the image in pixels
|
* Height of the image in pixels
|
||||||
*/
|
*/
|
||||||
height: number;
|
height: number
|
||||||
/**
|
/**
|
||||||
* The file size of the resource in bytes
|
* The file size of the resource in bytes
|
||||||
*/
|
*/
|
||||||
fileSize: number;
|
fileSize: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media_downloads {
|
export interface SharePageToken_shareToken_media_downloads {
|
||||||
__typename: "MediaDownload";
|
__typename: 'MediaDownload'
|
||||||
title: string;
|
title: string
|
||||||
mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl;
|
mediaUrl: SharePageToken_shareToken_media_downloads_mediaUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media_highRes {
|
export interface SharePageToken_shareToken_media_highRes {
|
||||||
__typename: "MediaURL";
|
__typename: 'MediaURL'
|
||||||
/**
|
/**
|
||||||
* URL for previewing the image
|
* URL for previewing the image
|
||||||
*/
|
*/
|
||||||
url: string;
|
url: string
|
||||||
/**
|
/**
|
||||||
* Width of the image in pixels
|
* Width of the image in pixels
|
||||||
*/
|
*/
|
||||||
width: number;
|
width: number
|
||||||
/**
|
/**
|
||||||
* Height of the image in pixels
|
* Height of the image in pixels
|
||||||
*/
|
*/
|
||||||
height: number;
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media_videoWeb {
|
export interface SharePageToken_shareToken_media_videoWeb {
|
||||||
__typename: "MediaURL";
|
__typename: 'MediaURL'
|
||||||
/**
|
/**
|
||||||
* URL for previewing the image
|
* URL for previewing the image
|
||||||
*/
|
*/
|
||||||
url: string;
|
url: string
|
||||||
/**
|
/**
|
||||||
* Width of the image in pixels
|
* Width of the image in pixels
|
||||||
*/
|
*/
|
||||||
width: number;
|
width: number
|
||||||
/**
|
/**
|
||||||
* Height of the image in pixels
|
* Height of the image in pixels
|
||||||
*/
|
*/
|
||||||
height: number;
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SharePageToken_shareToken_media_exif_coordinates {
|
||||||
|
__typename: 'Coordinates'
|
||||||
|
/**
|
||||||
|
* GPS longitude in degrees
|
||||||
|
*/
|
||||||
|
longitude: number
|
||||||
|
/**
|
||||||
|
* GPS latitude in degrees
|
||||||
|
*/
|
||||||
|
latitude: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media_exif {
|
export interface SharePageToken_shareToken_media_exif {
|
||||||
__typename: "MediaEXIF";
|
__typename: 'MediaEXIF'
|
||||||
id: string;
|
id: string
|
||||||
/**
|
/**
|
||||||
* The model name of the camera
|
* The model name of the camera
|
||||||
*/
|
*/
|
||||||
camera: string | null;
|
camera: string | null
|
||||||
/**
|
/**
|
||||||
* The maker of the camera
|
* The maker of the camera
|
||||||
*/
|
*/
|
||||||
maker: string | null;
|
maker: string | null
|
||||||
/**
|
/**
|
||||||
* The name of the lens
|
* The name of the lens
|
||||||
*/
|
*/
|
||||||
lens: string | null;
|
lens: string | null
|
||||||
dateShot: any | null;
|
dateShot: any | null
|
||||||
/**
|
/**
|
||||||
* The exposure time of the image
|
* The exposure time of the image
|
||||||
*/
|
*/
|
||||||
exposure: number | null;
|
exposure: number | null
|
||||||
/**
|
/**
|
||||||
* The aperature stops of the image
|
* The aperature stops of the image
|
||||||
*/
|
*/
|
||||||
aperture: number | null;
|
aperture: number | null
|
||||||
/**
|
/**
|
||||||
* The ISO setting of the image
|
* The ISO setting of the image
|
||||||
*/
|
*/
|
||||||
iso: number | null;
|
iso: number | null
|
||||||
/**
|
/**
|
||||||
* The focal length of the lens, when the image was taken
|
* The focal length of the lens, when the image was taken
|
||||||
*/
|
*/
|
||||||
focalLength: number | null;
|
focalLength: number | null
|
||||||
/**
|
/**
|
||||||
* A formatted description of the flash settings, when the image was taken
|
* A formatted description of the flash settings, when the image was taken
|
||||||
*/
|
*/
|
||||||
flash: number | null;
|
flash: number | null
|
||||||
/**
|
/**
|
||||||
* An index describing the mode for adjusting the exposure of the image
|
* An index describing the mode for adjusting the exposure of the image
|
||||||
*/
|
*/
|
||||||
exposureProgram: number | null;
|
exposureProgram: number | null
|
||||||
|
/**
|
||||||
|
* GPS coordinates of where the image was taken
|
||||||
|
*/
|
||||||
|
coordinates: SharePageToken_shareToken_media_exif_coordinates | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken_media {
|
export interface SharePageToken_shareToken_media {
|
||||||
__typename: "Media";
|
__typename: 'Media'
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
type: MediaType;
|
type: MediaType
|
||||||
/**
|
/**
|
||||||
* URL to display the media in a smaller resolution
|
* URL to display the media in a smaller resolution
|
||||||
*/
|
*/
|
||||||
thumbnail: SharePageToken_shareToken_media_thumbnail | null;
|
thumbnail: SharePageToken_shareToken_media_thumbnail | null
|
||||||
downloads: SharePageToken_shareToken_media_downloads[];
|
downloads: SharePageToken_shareToken_media_downloads[]
|
||||||
/**
|
/**
|
||||||
* URL to display the photo in full resolution, will be null for videos
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
*/
|
*/
|
||||||
highRes: SharePageToken_shareToken_media_highRes | null;
|
highRes: SharePageToken_shareToken_media_highRes | null
|
||||||
/**
|
/**
|
||||||
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||||
*/
|
*/
|
||||||
videoWeb: SharePageToken_shareToken_media_videoWeb | null;
|
videoWeb: SharePageToken_shareToken_media_videoWeb | null
|
||||||
exif: SharePageToken_shareToken_media_exif | null;
|
exif: SharePageToken_shareToken_media_exif | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken_shareToken {
|
export interface SharePageToken_shareToken {
|
||||||
__typename: "ShareToken";
|
__typename: 'ShareToken'
|
||||||
token: string;
|
token: string
|
||||||
/**
|
/**
|
||||||
* The album this token shares
|
* The album this token shares
|
||||||
*/
|
*/
|
||||||
album: SharePageToken_shareToken_album | null;
|
album: SharePageToken_shareToken_album | null
|
||||||
/**
|
/**
|
||||||
* The media this token shares
|
* The media this token shares
|
||||||
*/
|
*/
|
||||||
media: SharePageToken_shareToken_media | null;
|
media: SharePageToken_shareToken_media | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageToken {
|
export interface SharePageToken {
|
||||||
shareToken: SharePageToken_shareToken;
|
shareToken: SharePageToken_shareToken
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SharePageTokenVariables {
|
export interface SharePageTokenVariables {
|
||||||
token: string;
|
token: string
|
||||||
password?: string | null;
|
password?: string | null
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,15 @@
|
||||||
// ====================================================
|
// ====================================================
|
||||||
|
|
||||||
export interface albumPathQuery_album_path {
|
export interface albumPathQuery_album_path {
|
||||||
__typename: "Album";
|
__typename: 'Album'
|
||||||
id: string;
|
id: string
|
||||||
title: string;
|
title: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface albumPathQuery_album {
|
export interface albumPathQuery_album {
|
||||||
__typename: "Album";
|
__typename: 'Album'
|
||||||
id: string;
|
id: string
|
||||||
path: albumPathQuery_album_path[];
|
path: albumPathQuery_album_path[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface albumPathQuery {
|
export interface albumPathQuery {
|
||||||
|
@ -24,9 +24,9 @@ export interface albumPathQuery {
|
||||||
* Get album by id, user must own the album or be admin
|
* Get album by id, user must own the album or be admin
|
||||||
* If valid tokenCredentials are provided, the album may be retrived without further authentication
|
* If valid tokenCredentials are provided, the album may be retrived without further authentication
|
||||||
*/
|
*/
|
||||||
album: albumPathQuery_album;
|
album: albumPathQuery_album
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface albumPathQueryVariables {
|
export interface albumPathQueryVariables {
|
||||||
id: string;
|
id: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,8 +10,10 @@ import useDelay from '../../hooks/useDelay'
|
||||||
|
|
||||||
import { ReactComponent as GearIcon } from './icons/gear.svg'
|
import { ReactComponent as GearIcon } from './icons/gear.svg'
|
||||||
|
|
||||||
const BreadcrumbList = styled.ol`
|
export const BreadcrumbList = styled.ol<{ hideLastArrow?: boolean }>`
|
||||||
& li::after {
|
&
|
||||||
|
${({ hideLastArrow }) =>
|
||||||
|
hideLastArrow ? 'li:not(:last-child)::after' : 'li::after'} {
|
||||||
content: '';
|
content: '';
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='5px' height='6px' viewBox='0 0 5 6'%3E%3Cpolyline fill='none' stroke='%23979797' points='0.74 0.167710644 3.57228936 3 0.74 5.83228936' /%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='5px' height='6px' viewBox='0 0 5 6'%3E%3Cpolyline fill='none' stroke='%23979797' points='0.74 0.167710644 3.57228936 3 0.74 5.83228936' /%3E%3C/svg%3E");
|
||||||
width: 5px;
|
width: 5px;
|
||||||
|
|
|
@ -2,8 +2,8 @@ import React from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { MediaType } from '../../__generated__/globalTypes'
|
import { MediaType } from '../../__generated__/globalTypes'
|
||||||
import { MediaSidebarMedia } from '../sidebar/MediaSidebar'
|
import { MediaSidebarMedia } from '../sidebar/MediaSidebar/MediaSidebar'
|
||||||
import { sidebarPhoto_media_faces } from '../sidebar/__generated__/sidebarPhoto'
|
import { sidebarMediaQuery_media_faces } from '../sidebar/MediaSidebar/__generated__/sidebarMediaQuery'
|
||||||
|
|
||||||
interface FaceBoxStyleProps {
|
interface FaceBoxStyleProps {
|
||||||
$minY: number
|
$minY: number
|
||||||
|
@ -23,7 +23,7 @@ const FaceBoxStyle = styled(Link)`
|
||||||
`
|
`
|
||||||
|
|
||||||
type FaceBoxProps = {
|
type FaceBoxProps = {
|
||||||
face: sidebarPhoto_media_faces
|
face: sidebarMediaQuery_media_faces
|
||||||
}
|
}
|
||||||
|
|
||||||
const FaceBox = ({ face /*media*/ }: FaceBoxProps) => {
|
const FaceBox = ({ face /*media*/ }: FaceBoxProps) => {
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
import React, { useState, useRef, useEffect } from 'react'
|
||||||
|
import { gql, useQuery } from '@apollo/client'
|
||||||
|
import type mapboxgl from 'mapbox-gl'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||||
|
import { mapboxToken } from './__generated__/mapboxToken'
|
||||||
|
|
||||||
|
const MAPBOX_TOKEN_QUERY = gql`
|
||||||
|
query mapboxToken {
|
||||||
|
mapboxToken
|
||||||
|
myMediaGeoJson
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MapContainer = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
type MapboxMapProps = {
|
||||||
|
configureMapbox(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl): void
|
||||||
|
mapboxOptions?: Partial<mapboxgl.MapboxOptions>
|
||||||
|
}
|
||||||
|
|
||||||
|
const useMapboxMap = ({
|
||||||
|
configureMapbox,
|
||||||
|
mapboxOptions = undefined,
|
||||||
|
}: MapboxMapProps) => {
|
||||||
|
const [mapboxLibrary, setMapboxLibrary] = useState<typeof mapboxgl>()
|
||||||
|
const mapContainer = useRef<HTMLDivElement | null>(null)
|
||||||
|
const map = useRef<mapboxgl.Map | null>(null)
|
||||||
|
|
||||||
|
const { data: mapboxData } = useQuery<mapboxToken>(MAPBOX_TOKEN_QUERY, {
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function loadMapboxLibrary() {
|
||||||
|
const mapbox = (await import('mapbox-gl')).default
|
||||||
|
|
||||||
|
setMapboxLibrary(mapbox)
|
||||||
|
}
|
||||||
|
loadMapboxLibrary()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
mapboxLibrary == null ||
|
||||||
|
mapContainer.current == null ||
|
||||||
|
mapboxData == null ||
|
||||||
|
map.current != null
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapboxData.mapboxToken)
|
||||||
|
mapboxLibrary.accessToken = mapboxData.mapboxToken
|
||||||
|
|
||||||
|
map.current = new mapboxLibrary.Map({
|
||||||
|
container: mapContainer.current,
|
||||||
|
style: 'mapbox://styles/mapbox/streets-v11',
|
||||||
|
...mapboxOptions,
|
||||||
|
})
|
||||||
|
|
||||||
|
configureMapbox(map.current, mapboxLibrary)
|
||||||
|
map.current?.resize()
|
||||||
|
}, [mapContainer, mapboxLibrary, mapboxData])
|
||||||
|
|
||||||
|
map.current?.resize()
|
||||||
|
|
||||||
|
return {
|
||||||
|
mapContainer: <MapContainer ref={mapContainer}></MapContainer>,
|
||||||
|
mapboxMap: map.current,
|
||||||
|
mapboxLibrary,
|
||||||
|
mapboxToken: mapboxData?.mapboxToken || null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMapboxMap
|
|
@ -0,0 +1,19 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: mapboxToken
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface mapboxToken {
|
||||||
|
/**
|
||||||
|
* Get the mapbox api token, returns null if mapbox is not enabled
|
||||||
|
*/
|
||||||
|
mapboxToken: string | null
|
||||||
|
/**
|
||||||
|
* Get media owned by the logged in user, returned in GeoJson format
|
||||||
|
*/
|
||||||
|
myMediaGeoJson: any
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
import type mapboxgl from 'mapbox-gl'
|
||||||
|
import type geojson from 'geojson'
|
||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom'
|
||||||
|
import MapClusterMarker from '../../Pages/PlacesPage/MapClusterMarker'
|
||||||
|
import { MediaMarker } from '../../Pages/PlacesPage/MapPresentMarker'
|
||||||
|
import { PlacesAction } from '../../Pages/PlacesPage/placesReducer'
|
||||||
|
|
||||||
|
const markers: { [key: string]: mapboxgl.Marker } = {}
|
||||||
|
let markersOnScreen: typeof markers = {}
|
||||||
|
|
||||||
|
type registerMediaMarkersArgs = {
|
||||||
|
map: mapboxgl.Map
|
||||||
|
mapboxLibrary: typeof mapboxgl
|
||||||
|
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add appropriate event handlers to the map, to render and update media markers
|
||||||
|
* Expects the provided mapbox map to contain geojson source of media
|
||||||
|
*/
|
||||||
|
export const registerMediaMarkers = (args: registerMediaMarkersArgs) => {
|
||||||
|
const updateMarkers = makeUpdateMarkers(args)
|
||||||
|
|
||||||
|
args.map.on('move', updateMarkers)
|
||||||
|
args.map.on('moveend', updateMarkers)
|
||||||
|
args.map.on('sourcedata', updateMarkers)
|
||||||
|
updateMarkers()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a function that can be passed to Mapbox to tell it how to render and update the image markers
|
||||||
|
*/
|
||||||
|
const makeUpdateMarkers =
|
||||||
|
({ map, mapboxLibrary, dispatchMarkerMedia }: registerMediaMarkersArgs) =>
|
||||||
|
() => {
|
||||||
|
const newMarkers: typeof markers = {}
|
||||||
|
const features = map.querySourceFeatures('media')
|
||||||
|
|
||||||
|
// for every media on the screen, create an HTML marker for it (if we didn't yet),
|
||||||
|
// and add it to the map if it's not there already
|
||||||
|
for (const feature of features) {
|
||||||
|
const point = feature.geometry as geojson.Point
|
||||||
|
const coords = point.coordinates as [number, number]
|
||||||
|
const props = feature.properties as MediaMarker
|
||||||
|
if (props == null) {
|
||||||
|
console.warn('WARN: geojson feature had no properties', feature)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = props.cluster
|
||||||
|
? `cluster_${props.cluster_id}`
|
||||||
|
: `media_${props.media_id}`
|
||||||
|
|
||||||
|
let marker = markers[id]
|
||||||
|
if (!marker) {
|
||||||
|
const el = createClusterPopupElement(props, {
|
||||||
|
dispatchMarkerMedia,
|
||||||
|
})
|
||||||
|
marker = markers[id] = new mapboxLibrary.Marker({
|
||||||
|
element: el,
|
||||||
|
}).setLngLat(coords)
|
||||||
|
}
|
||||||
|
newMarkers[id] = marker
|
||||||
|
|
||||||
|
if (!markersOnScreen[id]) marker.addTo(map)
|
||||||
|
}
|
||||||
|
// for every marker we've added previously, remove those that are no longer visible
|
||||||
|
for (const id in markersOnScreen) {
|
||||||
|
if (!newMarkers[id]) markersOnScreen[id].remove()
|
||||||
|
}
|
||||||
|
markersOnScreen = newMarkers
|
||||||
|
}
|
||||||
|
|
||||||
|
function createClusterPopupElement(
|
||||||
|
geojsonProps: MediaMarker,
|
||||||
|
{
|
||||||
|
dispatchMarkerMedia,
|
||||||
|
}: {
|
||||||
|
dispatchMarkerMedia: React.Dispatch<PlacesAction>
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// setPresentMarker: React.Dispatch<React.SetStateAction<PresentMarker | null>>
|
||||||
|
const el = document.createElement('div')
|
||||||
|
ReactDOM.render(
|
||||||
|
<MapClusterMarker
|
||||||
|
marker={geojsonProps}
|
||||||
|
dispatchMarkerMedia={dispatchMarkerMedia}
|
||||||
|
/>,
|
||||||
|
el
|
||||||
|
)
|
||||||
|
return el
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ import styled from 'styled-components'
|
||||||
import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail'
|
import { MediaThumbnail, MediaPlaceholder } from './MediaThumbnail'
|
||||||
import PresentView from './presentView/PresentView'
|
import PresentView from './presentView/PresentView'
|
||||||
import { PresentMediaProps_Media } from './presentView/PresentMedia'
|
import { PresentMediaProps_Media } from './presentView/PresentMedia'
|
||||||
import { sidebarPhoto_media_thumbnail } from '../sidebar/__generated__/sidebarPhoto'
|
|
||||||
import {
|
import {
|
||||||
openPresentModeAction,
|
openPresentModeAction,
|
||||||
PhotoGalleryAction,
|
PhotoGalleryAction,
|
||||||
|
@ -13,8 +12,9 @@ import {
|
||||||
toggleFavoriteAction,
|
toggleFavoriteAction,
|
||||||
useMarkFavoriteMutation,
|
useMarkFavoriteMutation,
|
||||||
} from './photoGalleryMutations'
|
} from './photoGalleryMutations'
|
||||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar'
|
||||||
import { SidebarContext } from '../sidebar/Sidebar'
|
import { SidebarContext } from '../sidebar/Sidebar'
|
||||||
|
import { sidebarMediaQuery_media_thumbnail } from '../sidebar/MediaSidebar/__generated__/sidebarMediaQuery'
|
||||||
|
|
||||||
const Gallery = styled.div`
|
const Gallery = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -36,7 +36,7 @@ export const PhotoFiller = styled.div`
|
||||||
`
|
`
|
||||||
|
|
||||||
export interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
|
export interface PhotoGalleryProps_Media extends PresentMediaProps_Media {
|
||||||
thumbnail: sidebarPhoto_media_thumbnail | null
|
thumbnail: sidebarMediaQuery_media_thumbnail | null
|
||||||
favorite?: boolean
|
favorite?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,15 @@ import * as authentication from '../../helpers/authentication'
|
||||||
|
|
||||||
jest.mock('../../helpers/authentication.ts')
|
jest.mock('../../helpers/authentication.ts')
|
||||||
|
|
||||||
|
const authToken = authentication.authToken as jest.Mock<
|
||||||
|
ReturnType<typeof authentication.authToken>
|
||||||
|
>
|
||||||
|
|
||||||
describe('AuthorizedRoute component', () => {
|
describe('AuthorizedRoute component', () => {
|
||||||
const AuthorizedComponent = () => <div>authorized content</div>
|
const AuthorizedComponent = () => <div>authorized content</div>
|
||||||
|
|
||||||
test('not logged in', async () => {
|
test('not logged in', async () => {
|
||||||
authentication.authToken.mockImplementation(() => null)
|
authToken.mockImplementation(() => null)
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/']}>
|
<MemoryRouter initialEntries={['/']}>
|
||||||
|
@ -24,7 +28,7 @@ describe('AuthorizedRoute component', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
test('logged in', async () => {
|
test('logged in', async () => {
|
||||||
authentication.authToken.mockImplementation(() => 'token-here')
|
authToken.mockImplementation(() => 'token-here')
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<MemoryRouter initialEntries={['/']}>
|
<MemoryRouter initialEntries={['/']}>
|
|
@ -12,6 +12,7 @@ import {
|
||||||
resetAlbumCover,
|
resetAlbumCover,
|
||||||
resetAlbumCoverVariables,
|
resetAlbumCoverVariables,
|
||||||
} from './__generated__/resetAlbumCover'
|
} from './__generated__/resetAlbumCover'
|
||||||
|
import { authToken } from '../../helpers/authentication'
|
||||||
|
|
||||||
const RESET_ALBUM_COVER_MUTATION = gql`
|
const RESET_ALBUM_COVER_MUTATION = gql`
|
||||||
mutation resetAlbumCover($albumID: ID!) {
|
mutation resetAlbumCover($albumID: ID!) {
|
||||||
|
@ -62,6 +63,11 @@ export const SidebarPhotoCover = ({ cover_id }: SidebarPhotoCoverProps) => {
|
||||||
setButtonDisabled(false)
|
setButtonDisabled(false)
|
||||||
}, [cover_id])
|
}, [cover_id])
|
||||||
|
|
||||||
|
// hide when not authenticated
|
||||||
|
if (!authToken()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarSection>
|
<SidebarSection>
|
||||||
<SidebarSectionTitle>
|
<SidebarSectionTitle>
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render, screen } from '@testing-library/react'
|
||||||
|
import { MockedProvider } from '@apollo/client/testing'
|
||||||
|
import MediaSidebar, { MediaSidebarMedia } from './MediaSidebar'
|
||||||
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
|
import { MemoryRouter } from 'react-router'
|
||||||
|
|
||||||
|
import * as authentication from '../../../helpers/authentication'
|
||||||
|
|
||||||
|
jest.mock('../../../helpers/authentication.ts')
|
||||||
|
|
||||||
|
const authToken = authentication.authToken as jest.Mock<
|
||||||
|
ReturnType<typeof authentication.authToken>
|
||||||
|
>
|
||||||
|
|
||||||
|
describe('MediaSidebar', () => {
|
||||||
|
const media: MediaSidebarMedia = {
|
||||||
|
__typename: 'Media',
|
||||||
|
id: '6867',
|
||||||
|
title: '122A6069.jpg',
|
||||||
|
type: MediaType.Photo,
|
||||||
|
thumbnail: {
|
||||||
|
__typename: 'MediaURL',
|
||||||
|
url: 'http://localhost:4001/photo/thumbnail.jpg',
|
||||||
|
width: 1024,
|
||||||
|
height: 839,
|
||||||
|
},
|
||||||
|
highRes: {
|
||||||
|
__typename: 'MediaURL',
|
||||||
|
url: 'http://localhost:4001/photo/highres.jpg',
|
||||||
|
width: 5322,
|
||||||
|
height: 4362,
|
||||||
|
},
|
||||||
|
videoWeb: null,
|
||||||
|
album: {
|
||||||
|
__typename: 'Album',
|
||||||
|
id: '2294',
|
||||||
|
title: 'album_name',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
test('render sample image, unauthorized', () => {
|
||||||
|
authToken.mockImplementation(() => null)
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[]} addTypename={false}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<MediaSidebar media={media} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('122A6069.jpg')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('img')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'http://localhost:4001/photo/highres.jpg'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.queryByText('Set as album cover photo')
|
||||||
|
).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Sharing options')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('render sample image, authorized', () => {
|
||||||
|
authToken.mockImplementation(() => 'token-here')
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MockedProvider mocks={[]} addTypename={false}>
|
||||||
|
<MemoryRouter>
|
||||||
|
<MediaSidebar media={media} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('122A6069.jpg')).toBeInTheDocument()
|
||||||
|
expect(screen.getByRole('img')).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
'http://localhost:4001/photo/highres.jpg'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(screen.getByText('Set as album cover photo')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Album path')).toBeInTheDocument()
|
||||||
|
|
||||||
|
screen.debug()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,300 @@
|
||||||
|
import { gql, useLazyQuery } from '@apollo/client'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
import { authToken } from '../../../helpers/authentication'
|
||||||
|
import { isNil } from '../../../helpers/utils'
|
||||||
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
|
import { SidebarFacesOverlay } from '../../facesOverlay/FacesOverlay'
|
||||||
|
import {
|
||||||
|
ProtectedImage,
|
||||||
|
ProtectedVideo,
|
||||||
|
ProtectedVideoProps_Media,
|
||||||
|
} from '../../photoGallery/ProtectedMedia'
|
||||||
|
import { SidebarPhotoCover } from '../AlbumCovers'
|
||||||
|
import { SidebarPhotoShare } from '../Sharing'
|
||||||
|
import SidebarMediaDownload from '../SidebarDownloadMedia'
|
||||||
|
import SidebarHeader from '../SidebarHeader'
|
||||||
|
import { sidebarDownloadQuery_media_downloads } from '../__generated__/sidebarDownloadQuery'
|
||||||
|
import ExifDetails from './MediaSidebarExif'
|
||||||
|
import MediaSidebarPeople from './MediaSidebarPeople'
|
||||||
|
import MediaSidebarMap from './MediaSidebarMap'
|
||||||
|
import {
|
||||||
|
sidebarMediaQuery,
|
||||||
|
sidebarMediaQueryVariables,
|
||||||
|
sidebarMediaQuery_media_album_path,
|
||||||
|
sidebarMediaQuery_media_exif,
|
||||||
|
sidebarMediaQuery_media_faces,
|
||||||
|
sidebarMediaQuery_media_thumbnail,
|
||||||
|
sidebarMediaQuery_media_videoMetadata,
|
||||||
|
} from './__generated__/sidebarMediaQuery'
|
||||||
|
import { BreadcrumbList } from '../../album/AlbumTitle'
|
||||||
|
|
||||||
|
export const SIDEBAR_MEDIA_QUERY = gql`
|
||||||
|
query sidebarMediaQuery($id: ID!) {
|
||||||
|
media(id: $id) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
type
|
||||||
|
highRes {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
thumbnail {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
videoWeb {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
videoMetadata {
|
||||||
|
id
|
||||||
|
width
|
||||||
|
height
|
||||||
|
duration
|
||||||
|
codec
|
||||||
|
framerate
|
||||||
|
bitrate
|
||||||
|
colorProfile
|
||||||
|
audio
|
||||||
|
}
|
||||||
|
exif {
|
||||||
|
id
|
||||||
|
camera
|
||||||
|
maker
|
||||||
|
lens
|
||||||
|
dateShot
|
||||||
|
exposure
|
||||||
|
aperture
|
||||||
|
iso
|
||||||
|
focalLength
|
||||||
|
flash
|
||||||
|
exposureProgram
|
||||||
|
coordinates {
|
||||||
|
latitude
|
||||||
|
longitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
album {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
path {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
faces {
|
||||||
|
id
|
||||||
|
rectangle {
|
||||||
|
minX
|
||||||
|
maxX
|
||||||
|
minY
|
||||||
|
maxY
|
||||||
|
}
|
||||||
|
faceGroup {
|
||||||
|
id
|
||||||
|
label
|
||||||
|
imageFaceCount
|
||||||
|
}
|
||||||
|
media {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
thumbnail {
|
||||||
|
url
|
||||||
|
width
|
||||||
|
height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const PreviewImage = styled(ProtectedImage)`
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
object-fit: contain;
|
||||||
|
`
|
||||||
|
|
||||||
|
const PreviewVideo = styled(ProtectedVideo)`
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media {
|
||||||
|
type: MediaType
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreviewMediaProps = {
|
||||||
|
media: PreviewMediaPropsMedia
|
||||||
|
previewImage?: {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => {
|
||||||
|
if (media.type === MediaType.Photo) {
|
||||||
|
return <PreviewImage src={previewImage?.url} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.type === MediaType.Video) {
|
||||||
|
return <PreviewVideo media={media} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div>ERROR: Unknown media type: {media.type}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarContentProps = {
|
||||||
|
media: MediaSidebarMedia
|
||||||
|
hidePreview?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
let previewImage = null
|
||||||
|
if (media.highRes) previewImage = media.highRes
|
||||||
|
else if (media.thumbnail) previewImage = media.thumbnail
|
||||||
|
|
||||||
|
const imageAspect =
|
||||||
|
previewImage?.width && previewImage?.height
|
||||||
|
? previewImage.height / previewImage.width
|
||||||
|
: 3 / 2
|
||||||
|
|
||||||
|
let sidebarMap = null
|
||||||
|
const mediaCoordinates = media.exif?.coordinates
|
||||||
|
if (mediaCoordinates) {
|
||||||
|
sidebarMap = <MediaSidebarMap coordinates={mediaCoordinates} />
|
||||||
|
}
|
||||||
|
|
||||||
|
let albumPath = null
|
||||||
|
const mediaAlbum = media.album
|
||||||
|
if (!isNil(mediaAlbum)) {
|
||||||
|
const pathElms = [...(mediaAlbum.path ?? []), mediaAlbum].map(album => (
|
||||||
|
<li key={album.id} className="inline-block hover:underline">
|
||||||
|
<Link
|
||||||
|
className="text-blue-900 hover:underline"
|
||||||
|
to={`/album/${album.id}`}
|
||||||
|
>
|
||||||
|
{album.title}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
|
||||||
|
albumPath = (
|
||||||
|
<div className="mx-4 my-4">
|
||||||
|
<h2 className="uppercase text-xs text-gray-900 font-semibold">
|
||||||
|
{t('sidebar.media.album_path', 'Album path')}
|
||||||
|
</h2>
|
||||||
|
<BreadcrumbList hideLastArrow={true}>{pathElms}</BreadcrumbList>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<SidebarHeader title={media.title ?? 'Loading...'} />
|
||||||
|
<div className="lg:mx-4">
|
||||||
|
{!hidePreview && (
|
||||||
|
<div
|
||||||
|
className="w-full h-0 relative"
|
||||||
|
style={{ paddingTop: `${Math.min(imageAspect, 0.75) * 100}%` }}
|
||||||
|
>
|
||||||
|
<PreviewMedia
|
||||||
|
previewImage={previewImage || undefined}
|
||||||
|
media={media}
|
||||||
|
/>
|
||||||
|
<SidebarFacesOverlay media={media} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ExifDetails media={media} />
|
||||||
|
{albumPath}
|
||||||
|
<MediaSidebarPeople media={media} />
|
||||||
|
{sidebarMap}
|
||||||
|
<SidebarMediaDownload media={media} />
|
||||||
|
<SidebarPhotoShare id={media.id} />
|
||||||
|
<div className="mt-8">
|
||||||
|
<SidebarPhotoCover cover_id={media.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaSidebarMedia {
|
||||||
|
__typename: 'Media'
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
type: MediaType
|
||||||
|
highRes?: null | {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
url: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
thumbnail?: sidebarMediaQuery_media_thumbnail | null
|
||||||
|
videoWeb?: null | {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
url: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
|
}
|
||||||
|
videoMetadata?: sidebarMediaQuery_media_videoMetadata | null
|
||||||
|
exif?: sidebarMediaQuery_media_exif | null
|
||||||
|
faces?: sidebarMediaQuery_media_faces[]
|
||||||
|
downloads?: sidebarDownloadQuery_media_downloads[]
|
||||||
|
album?: {
|
||||||
|
__typename: 'Album'
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
path?: sidebarMediaQuery_media_album_path[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaSidebarType = {
|
||||||
|
media: MediaSidebarMedia
|
||||||
|
hidePreview?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => {
|
||||||
|
const [loadMedia, { loading, error, data }] = useLazyQuery<
|
||||||
|
sidebarMediaQuery,
|
||||||
|
sidebarMediaQueryVariables
|
||||||
|
>(SIDEBAR_MEDIA_QUERY)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (media != null && authToken()) {
|
||||||
|
loadMedia({
|
||||||
|
variables: {
|
||||||
|
id: media.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, [media])
|
||||||
|
|
||||||
|
if (!media) return null
|
||||||
|
|
||||||
|
if (!authToken()) {
|
||||||
|
return <SidebarContent media={media} hidePreview={hidePreview} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) return <div>{error.message}</div>
|
||||||
|
|
||||||
|
if (loading || data == null) {
|
||||||
|
return <SidebarContent media={media} hidePreview={hidePreview} />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SidebarContent media={data.media} hidePreview={hidePreview} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaSidebar
|
|
@ -1,13 +1,15 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import { MetadataInfo } from './MediaSidebar'
|
import ExifDetails from './MediaSidebarExif'
|
||||||
|
import { MediaSidebarMedia } from './MediaSidebar'
|
||||||
|
import { MediaType } from '../../../__generated__/globalTypes'
|
||||||
|
|
||||||
describe('MetadataInfo', () => {
|
describe('ExifDetails', () => {
|
||||||
test('without EXIF information', async () => {
|
test('without EXIF information', async () => {
|
||||||
const media = {
|
const media: MediaSidebarMedia = {
|
||||||
id: '1730',
|
id: '1730',
|
||||||
title: 'media_name.jpg',
|
title: 'media_name.jpg',
|
||||||
type: 'Photo',
|
type: MediaType.Photo,
|
||||||
exif: {
|
exif: {
|
||||||
id: '0',
|
id: '0',
|
||||||
camera: null,
|
camera: null,
|
||||||
|
@ -20,12 +22,13 @@ describe('MetadataInfo', () => {
|
||||||
focalLength: null,
|
focalLength: null,
|
||||||
flash: null,
|
flash: null,
|
||||||
exposureProgram: null,
|
exposureProgram: null,
|
||||||
|
coordinates: null,
|
||||||
__typename: 'MediaEXIF',
|
__typename: 'MediaEXIF',
|
||||||
},
|
},
|
||||||
__typename: 'Media',
|
__typename: 'Media',
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<MetadataInfo media={media} />)
|
render(<ExifDetails media={media} />)
|
||||||
|
|
||||||
expect(screen.queryByText('Camera')).not.toBeInTheDocument()
|
expect(screen.queryByText('Camera')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('Maker')).not.toBeInTheDocument()
|
expect(screen.queryByText('Maker')).not.toBeInTheDocument()
|
||||||
|
@ -37,13 +40,14 @@ describe('MetadataInfo', () => {
|
||||||
expect(screen.queryByText('ISO')).not.toBeInTheDocument()
|
expect(screen.queryByText('ISO')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('Focal length')).not.toBeInTheDocument()
|
expect(screen.queryByText('Focal length')).not.toBeInTheDocument()
|
||||||
expect(screen.queryByText('Flash')).not.toBeInTheDocument()
|
expect(screen.queryByText('Flash')).not.toBeInTheDocument()
|
||||||
|
expect(screen.queryByText('Coordinates')).not.toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
test('with EXIF information', async () => {
|
test('with EXIF information', async () => {
|
||||||
const media = {
|
const media: MediaSidebarMedia = {
|
||||||
id: '1730',
|
id: '1730',
|
||||||
title: 'media_name.jpg',
|
title: 'media_name.jpg',
|
||||||
type: 'Photo',
|
type: MediaType.Photo,
|
||||||
exif: {
|
exif: {
|
||||||
id: '1666',
|
id: '1666',
|
||||||
camera: 'Canon EOS R',
|
camera: 'Canon EOS R',
|
||||||
|
@ -56,12 +60,17 @@ describe('MetadataInfo', () => {
|
||||||
focalLength: 24,
|
focalLength: 24,
|
||||||
flash: 9,
|
flash: 9,
|
||||||
exposureProgram: 3,
|
exposureProgram: 3,
|
||||||
|
coordinates: {
|
||||||
|
__typename: 'Coordinates',
|
||||||
|
latitude: 41.40338,
|
||||||
|
longitude: 2.17403,
|
||||||
|
},
|
||||||
__typename: 'MediaEXIF',
|
__typename: 'MediaEXIF',
|
||||||
},
|
},
|
||||||
__typename: 'Media',
|
__typename: 'Media',
|
||||||
}
|
}
|
||||||
|
|
||||||
render(<MetadataInfo media={media} />)
|
render(<ExifDetails media={media} />)
|
||||||
|
|
||||||
expect(screen.getByText('Camera')).toBeInTheDocument()
|
expect(screen.getByText('Camera')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Canon EOS R')).toBeInTheDocument()
|
expect(screen.getByText('Canon EOS R')).toBeInTheDocument()
|
||||||
|
@ -94,5 +103,8 @@ describe('MetadataInfo', () => {
|
||||||
|
|
||||||
expect(screen.getByText('Flash')).toBeInTheDocument()
|
expect(screen.getByText('Flash')).toBeInTheDocument()
|
||||||
expect(screen.getByText('On, Fired')).toBeInTheDocument()
|
expect(screen.getByText('On, Fired')).toBeInTheDocument()
|
||||||
|
|
||||||
|
expect(screen.getByText('Coordinates')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('41.40338, 2.17403')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
})
|
})
|
|
@ -1,143 +1,20 @@
|
||||||
import React, { useEffect } from 'react'
|
import React from 'react'
|
||||||
import { useLazyQuery, gql } from '@apollo/client'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
import { authToken } from '../../helpers/authentication'
|
|
||||||
import {
|
|
||||||
ProtectedImage,
|
|
||||||
ProtectedVideo,
|
|
||||||
ProtectedVideoProps_Media,
|
|
||||||
} from '../photoGallery/ProtectedMedia'
|
|
||||||
import { SidebarPhotoShare } from './Sharing'
|
|
||||||
import SidebarMediaDownload from './SidebarDownloadMedia'
|
|
||||||
import SidebarItem from './SidebarItem'
|
|
||||||
import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
|
|
||||||
import { isNil } from '../../helpers/utils'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { MediaType } from '../../__generated__/globalTypes'
|
import styled from 'styled-components'
|
||||||
import { TranslationFn } from '../../localization'
|
import { isNil } from '../../../helpers/utils'
|
||||||
import {
|
import { TranslationFn } from '../../../localization'
|
||||||
sidebarPhoto,
|
import SidebarItem from '../SidebarItem'
|
||||||
sidebarPhotoVariables,
|
import { MediaSidebarMedia } from './MediaSidebar'
|
||||||
sidebarPhoto_media_exif,
|
|
||||||
sidebarPhoto_media_faces,
|
|
||||||
sidebarPhoto_media_thumbnail,
|
|
||||||
sidebarPhoto_media_videoMetadata,
|
|
||||||
} from './__generated__/sidebarPhoto'
|
|
||||||
|
|
||||||
import { sidebarDownloadQuery_media_downloads } from './__generated__/sidebarDownloadQuery'
|
|
||||||
import SidebarHeader from './SidebarHeader'
|
|
||||||
import { SidebarPhotoCover } from './AlbumCovers'
|
|
||||||
|
|
||||||
const SIDEBAR_MEDIA_QUERY = gql`
|
|
||||||
query sidebarPhoto($id: ID!) {
|
|
||||||
media(id: $id) {
|
|
||||||
id
|
|
||||||
title
|
|
||||||
type
|
|
||||||
highRes {
|
|
||||||
url
|
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
|
||||||
thumbnail {
|
|
||||||
url
|
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
|
||||||
videoWeb {
|
|
||||||
url
|
|
||||||
width
|
|
||||||
height
|
|
||||||
}
|
|
||||||
videoMetadata {
|
|
||||||
id
|
|
||||||
width
|
|
||||||
height
|
|
||||||
duration
|
|
||||||
codec
|
|
||||||
framerate
|
|
||||||
bitrate
|
|
||||||
colorProfile
|
|
||||||
audio
|
|
||||||
}
|
|
||||||
exif {
|
|
||||||
id
|
|
||||||
camera
|
|
||||||
maker
|
|
||||||
lens
|
|
||||||
dateShot
|
|
||||||
exposure
|
|
||||||
aperture
|
|
||||||
iso
|
|
||||||
focalLength
|
|
||||||
flash
|
|
||||||
exposureProgram
|
|
||||||
}
|
|
||||||
faces {
|
|
||||||
id
|
|
||||||
rectangle {
|
|
||||||
minX
|
|
||||||
maxX
|
|
||||||
minY
|
|
||||||
maxY
|
|
||||||
}
|
|
||||||
faceGroup {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const PreviewImage = styled(ProtectedImage)`
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
object-fit: contain;
|
|
||||||
`
|
|
||||||
|
|
||||||
const PreviewVideo = styled(ProtectedVideo)`
|
|
||||||
position: absolute;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
`
|
|
||||||
|
|
||||||
interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media {
|
|
||||||
type: MediaType
|
|
||||||
}
|
|
||||||
|
|
||||||
type PreviewMediaProps = {
|
|
||||||
media: PreviewMediaPropsMedia
|
|
||||||
previewImage?: {
|
|
||||||
url: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => {
|
|
||||||
if (media.type === MediaType.Photo) {
|
|
||||||
return <PreviewImage src={previewImage?.url} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (media.type === MediaType.Video) {
|
|
||||||
return <PreviewVideo media={media} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div>ERROR: Unknown media type: {media.type}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const MetadataInfoContainer = styled.div`
|
const MetadataInfoContainer = styled.div`
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
`
|
`
|
||||||
|
|
||||||
type MediaInfoProps = {
|
type ExifDetailsProps = {
|
||||||
media?: MediaSidebarMedia
|
media?: MediaSidebarMedia
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MetadataInfo = ({ media }: MediaInfoProps) => {
|
const ExifDetails = ({ media }: ExifDetailsProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
let exifItems: JSX.Element[] = []
|
let exifItems: JSX.Element[] = []
|
||||||
|
|
||||||
|
@ -170,6 +47,13 @@ export const MetadataInfo = ({ media }: MediaInfoProps) => {
|
||||||
exif.exposure = `1/${Math.round(1 / exif.exposure)}`
|
exif.exposure = `1/${Math.round(1 / exif.exposure)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const coordinates = media.exif.coordinates
|
||||||
|
if (!isNil(coordinates)) {
|
||||||
|
exif.coordinates = `${
|
||||||
|
Math.round(coordinates.latitude * 1000000) / 1000000
|
||||||
|
}, ${Math.round(coordinates.longitude * 1000000) / 1000000}`
|
||||||
|
}
|
||||||
|
|
||||||
const exposurePrograms = exposureProgramsLookup(t)
|
const exposurePrograms = exposureProgramsLookup(t)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -246,6 +130,7 @@ const exifNameLookup = (t: TranslationFn): { [key: string]: string } => ({
|
||||||
iso: t('sidebar.media.exif.name.iso', 'ISO'),
|
iso: t('sidebar.media.exif.name.iso', 'ISO'),
|
||||||
focalLength: t('sidebar.media.exif.name.focal_length', 'Focal length'),
|
focalLength: t('sidebar.media.exif.name.focal_length', 'Focal length'),
|
||||||
flash: t('sidebar.media.exif.name.flash', 'Flash'),
|
flash: t('sidebar.media.exif.name.flash', 'Flash'),
|
||||||
|
coordinates: t('sidebar.media.exif.name.coordinates', 'Coordinates'),
|
||||||
})
|
})
|
||||||
|
|
||||||
// From https://exiftool.org/TagNames/EXIF.html
|
// From https://exiftool.org/TagNames/EXIF.html
|
||||||
|
@ -331,106 +216,4 @@ const flashLookup = (t: TranslationFn): { [key: number]: string } => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarContentProps = {
|
export default ExifDetails
|
||||||
media: MediaSidebarMedia
|
|
||||||
hidePreview?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
|
|
||||||
let previewImage = null
|
|
||||||
if (media.highRes) previewImage = media.highRes
|
|
||||||
else if (media.thumbnail) previewImage = media.thumbnail
|
|
||||||
|
|
||||||
const imageAspect =
|
|
||||||
previewImage?.width && previewImage?.height
|
|
||||||
? previewImage.height / previewImage.width
|
|
||||||
: 3 / 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<SidebarHeader title={media.title ?? 'Loading...'} />
|
|
||||||
<div className="lg:mx-4">
|
|
||||||
{!hidePreview && (
|
|
||||||
<div
|
|
||||||
className="w-full h-0 relative"
|
|
||||||
style={{ paddingTop: `${Math.min(imageAspect, 0.75) * 100}%` }}
|
|
||||||
>
|
|
||||||
<PreviewMedia
|
|
||||||
previewImage={previewImage || undefined}
|
|
||||||
media={media}
|
|
||||||
/>
|
|
||||||
<SidebarFacesOverlay media={media} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<MetadataInfo media={media} />
|
|
||||||
<SidebarMediaDownload media={media} />
|
|
||||||
<SidebarPhotoShare id={media.id} />
|
|
||||||
<div className="mt-8">
|
|
||||||
<SidebarPhotoCover cover_id={media.id} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MediaSidebarMedia {
|
|
||||||
__typename: 'Media'
|
|
||||||
id: string
|
|
||||||
title?: string
|
|
||||||
type: MediaType
|
|
||||||
highRes?: null | {
|
|
||||||
__typename: 'MediaURL'
|
|
||||||
url: string
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
}
|
|
||||||
thumbnail?: sidebarPhoto_media_thumbnail | null
|
|
||||||
videoWeb?: null | {
|
|
||||||
__typename: 'MediaURL'
|
|
||||||
url: string
|
|
||||||
width?: number
|
|
||||||
height?: number
|
|
||||||
}
|
|
||||||
videoMetadata?: sidebarPhoto_media_videoMetadata | null
|
|
||||||
exif?: sidebarPhoto_media_exif | null
|
|
||||||
faces?: sidebarPhoto_media_faces[]
|
|
||||||
downloads?: sidebarDownloadQuery_media_downloads[]
|
|
||||||
}
|
|
||||||
|
|
||||||
type MediaSidebarType = {
|
|
||||||
media: MediaSidebarMedia
|
|
||||||
hidePreview?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => {
|
|
||||||
const [loadMedia, { loading, error, data }] = useLazyQuery<
|
|
||||||
sidebarPhoto,
|
|
||||||
sidebarPhotoVariables
|
|
||||||
>(SIDEBAR_MEDIA_QUERY)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (media != null && authToken()) {
|
|
||||||
loadMedia({
|
|
||||||
variables: {
|
|
||||||
id: media.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [media])
|
|
||||||
|
|
||||||
if (!media) return null
|
|
||||||
|
|
||||||
if (!authToken()) {
|
|
||||||
return <SidebarContent media={media} hidePreview={hidePreview} />
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) return <div>{error.message}</div>
|
|
||||||
|
|
||||||
if (loading || data == null) {
|
|
||||||
return <SidebarContent media={media} hidePreview={hidePreview} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <SidebarContent media={data.media} hidePreview={hidePreview} />
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaSidebar
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { isNil } from '../../../helpers/utils'
|
||||||
|
import useMapboxMap from '../../mapbox/MapboxMap'
|
||||||
|
import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents'
|
||||||
|
import { sidebarMediaQuery_media_exif_coordinates } from './__generated__/sidebarMediaQuery'
|
||||||
|
|
||||||
|
type MediaSidebarMapProps = {
|
||||||
|
coordinates: sidebarMediaQuery_media_exif_coordinates
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaSidebarMap = ({ coordinates }: MediaSidebarMapProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const { mapContainer, mapboxToken } = useMapboxMap({
|
||||||
|
mapboxOptions: {
|
||||||
|
interactive: false,
|
||||||
|
zoom: 12,
|
||||||
|
center: {
|
||||||
|
lat: coordinates.latitude,
|
||||||
|
lng: coordinates.longitude,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configureMapbox: (map, mapboxLibrary) => {
|
||||||
|
// todo
|
||||||
|
map.addControl(
|
||||||
|
new mapboxLibrary.NavigationControl({ showCompass: false })
|
||||||
|
)
|
||||||
|
|
||||||
|
const centerMarker = new mapboxLibrary.Marker({
|
||||||
|
color: 'red',
|
||||||
|
scale: 0.8,
|
||||||
|
})
|
||||||
|
centerMarker.setLngLat({
|
||||||
|
lat: coordinates.latitude,
|
||||||
|
lng: coordinates.longitude,
|
||||||
|
})
|
||||||
|
centerMarker.addTo(map)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isNil(mapboxToken)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarSection>
|
||||||
|
<SidebarSectionTitle>
|
||||||
|
{t('sidebar.location.title', 'Location')}
|
||||||
|
</SidebarSectionTitle>
|
||||||
|
<div className="w-full h-64">{mapContainer}</div>
|
||||||
|
</SidebarSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaSidebarMap
|
|
@ -0,0 +1,218 @@
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { Link, useHistory } from 'react-router-dom'
|
||||||
|
import FaceCircleImage from '../../../Pages/PeoplePage/FaceCircleImage'
|
||||||
|
import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents'
|
||||||
|
import { MediaSidebarMedia, SIDEBAR_MEDIA_QUERY } from './MediaSidebar'
|
||||||
|
import { sidebarMediaQuery_media_faces } from './__generated__/sidebarMediaQuery'
|
||||||
|
|
||||||
|
import { ReactComponent as PeopleDotsIcon } from './icons/peopleDotsIcon.svg'
|
||||||
|
import { Menu } from '@headlessui/react'
|
||||||
|
import { Button } from '../../../primitives/form/Input'
|
||||||
|
import { ArrowPopoverPanel } from '../Sharing'
|
||||||
|
import { isNil, tailwindClassNames } from '../../../helpers/utils'
|
||||||
|
import MergeFaceGroupsModal from '../../../Pages/PeoplePage/SingleFaceGroup/MergeFaceGroupsModal'
|
||||||
|
import { useDetachImageFaces } from '../../../Pages/PeoplePage/SingleFaceGroup/DetachImageFacesModal'
|
||||||
|
import MoveImageFacesModal from '../../../Pages/PeoplePage/SingleFaceGroup/MoveImageFacesModal'
|
||||||
|
import { FaceDetails } from '../../../Pages/PeoplePage/PeoplePage'
|
||||||
|
|
||||||
|
type PersonMoreMenuItemProps = {
|
||||||
|
label: string
|
||||||
|
className?: string
|
||||||
|
onClick(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersonMoreMenuItem = ({
|
||||||
|
label,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}: PersonMoreMenuItemProps) => {
|
||||||
|
return (
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) => (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={tailwindClassNames(
|
||||||
|
`whitespace-normal w-full block py-1 cursor-pointer ${
|
||||||
|
active ? 'bg-gray-50 text-black' : 'text-gray-700'
|
||||||
|
}`,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersonMoreMenuProps = {
|
||||||
|
face: sidebarMediaQuery_media_faces
|
||||||
|
setChangeLabel: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
className?: string
|
||||||
|
menuFlipped: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const PersonMoreMenu = ({
|
||||||
|
menuFlipped,
|
||||||
|
face,
|
||||||
|
setChangeLabel,
|
||||||
|
className,
|
||||||
|
}: PersonMoreMenuProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const [mergeModalOpen, setMergeModalOpen] = useState(false)
|
||||||
|
const [moveModalOpen, setMoveModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const refetchQueries = [
|
||||||
|
{
|
||||||
|
query: SIDEBAR_MEDIA_QUERY,
|
||||||
|
variables: {
|
||||||
|
id: face.media.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const history = useHistory()
|
||||||
|
const detachImageFaceMutation = useDetachImageFaces({
|
||||||
|
refetchQueries,
|
||||||
|
})
|
||||||
|
|
||||||
|
const modals = (
|
||||||
|
<>
|
||||||
|
<MergeFaceGroupsModal
|
||||||
|
sourceFaceGroup={face.faceGroup}
|
||||||
|
open={mergeModalOpen}
|
||||||
|
setOpen={setMergeModalOpen}
|
||||||
|
refetchQueries={refetchQueries}
|
||||||
|
/>
|
||||||
|
<MoveImageFacesModal
|
||||||
|
faceGroup={{ imageFaces: [], ...face.faceGroup }}
|
||||||
|
open={moveModalOpen}
|
||||||
|
setOpen={setMoveModalOpen}
|
||||||
|
preselectedImageFaces={[face]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
const detachImageFace = () => {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
t(
|
||||||
|
'sidebar.people.confirm_image_detach',
|
||||||
|
'Are you sure you want to detach this image?'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
detachImageFaceMutation([face]).then(({ data }) => {
|
||||||
|
if (isNil(data)) throw new Error('Expected data not to be null')
|
||||||
|
history.push(`/people/${data.detachImageFaces.id}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu
|
||||||
|
as="div"
|
||||||
|
className={tailwindClassNames('relative inline-block', className)}
|
||||||
|
>
|
||||||
|
<Menu.Button as={Button} className="px-1.5 py-1.5 align-middle ml-1">
|
||||||
|
<PeopleDotsIcon className="text-gray-500" />
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="">
|
||||||
|
<ArrowPopoverPanel width={120} flipped={menuFlipped}>
|
||||||
|
<PersonMoreMenuItem
|
||||||
|
onClick={() => setChangeLabel(true)}
|
||||||
|
className="border-b"
|
||||||
|
label={t('people_page.action_label.change_label', 'Change label')}
|
||||||
|
/>
|
||||||
|
<PersonMoreMenuItem
|
||||||
|
onClick={() => setMergeModalOpen(true)}
|
||||||
|
className="border-b"
|
||||||
|
label={t('sidebar.people.action_label.merge_face', 'Merge face')}
|
||||||
|
/>
|
||||||
|
<PersonMoreMenuItem
|
||||||
|
onClick={() => detachImageFace()}
|
||||||
|
className="border-b"
|
||||||
|
label={t(
|
||||||
|
'sidebar.people.action_label.detach_image',
|
||||||
|
'Detach image'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<PersonMoreMenuItem
|
||||||
|
onClick={() => setMoveModalOpen(true)}
|
||||||
|
label={t('sidebar.people.action_label.move_face', 'Move face')}
|
||||||
|
/>
|
||||||
|
</ArrowPopoverPanel>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
{modals}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaSidebarFaceProps = {
|
||||||
|
face: sidebarMediaQuery_media_faces
|
||||||
|
menuFlipped: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaSidebarPerson = ({ face, menuFlipped }: MediaSidebarFaceProps) => {
|
||||||
|
const [changeLabel, setChangeLabel] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className="inline-block">
|
||||||
|
<Link to={`/people/${face.faceGroup.id}`}>
|
||||||
|
<FaceCircleImage imageFace={face} selectable={true} size="92px" />
|
||||||
|
</Link>
|
||||||
|
<div className="mt-1 whitespace-nowrap">
|
||||||
|
<FaceDetails
|
||||||
|
className="text-sm max-w-[80px] align-middle"
|
||||||
|
textFieldClassName="w-[100px]"
|
||||||
|
group={face.faceGroup}
|
||||||
|
editLabel={changeLabel}
|
||||||
|
setEditLabel={setChangeLabel}
|
||||||
|
/>
|
||||||
|
{!changeLabel && (
|
||||||
|
<PersonMoreMenu
|
||||||
|
menuFlipped={menuFlipped}
|
||||||
|
className="pl-0.5"
|
||||||
|
face={face}
|
||||||
|
setChangeLabel={setChangeLabel}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MediaSidebarFacesProps = {
|
||||||
|
media: MediaSidebarMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
const MediaSidebarPeople = ({ media }: MediaSidebarFacesProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const faceElms = (media.faces ?? []).map((face, i) => (
|
||||||
|
<MediaSidebarPerson key={face.id} face={face} menuFlipped={i == 0} />
|
||||||
|
))
|
||||||
|
|
||||||
|
if (faceElms.length == 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarSection>
|
||||||
|
<SidebarSectionTitle>
|
||||||
|
{t('sidebar.people.title', 'People')}
|
||||||
|
</SidebarSectionTitle>
|
||||||
|
<div
|
||||||
|
className="overflow-x-auto mb-[-200px]"
|
||||||
|
style={{ scrollbarWidth: 'none' }}
|
||||||
|
>
|
||||||
|
<ul className="flex gap-4 mx-4">{faceElms}</ul>
|
||||||
|
<div className="h-[200px]"></div>
|
||||||
|
</div>
|
||||||
|
</SidebarSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaSidebarPeople
|
|
@ -0,0 +1,229 @@
|
||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
// @generated
|
||||||
|
// This file was automatically generated and should not be edited.
|
||||||
|
|
||||||
|
import { MediaType } from './../../../../__generated__/globalTypes'
|
||||||
|
|
||||||
|
// ====================================================
|
||||||
|
// GraphQL query operation: sidebarMediaQuery
|
||||||
|
// ====================================================
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_highRes {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_thumbnail {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_videoWeb {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_videoMetadata {
|
||||||
|
__typename: 'VideoMetadata'
|
||||||
|
id: string
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
duration: number
|
||||||
|
codec: string | null
|
||||||
|
framerate: number | null
|
||||||
|
bitrate: string | null
|
||||||
|
colorProfile: string | null
|
||||||
|
audio: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_exif_coordinates {
|
||||||
|
__typename: 'Coordinates'
|
||||||
|
/**
|
||||||
|
* GPS latitude in degrees
|
||||||
|
*/
|
||||||
|
latitude: number
|
||||||
|
/**
|
||||||
|
* GPS longitude in degrees
|
||||||
|
*/
|
||||||
|
longitude: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_exif {
|
||||||
|
__typename: 'MediaEXIF'
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* The model name of the camera
|
||||||
|
*/
|
||||||
|
camera: string | null
|
||||||
|
/**
|
||||||
|
* The maker of the camera
|
||||||
|
*/
|
||||||
|
maker: string | null
|
||||||
|
/**
|
||||||
|
* The name of the lens
|
||||||
|
*/
|
||||||
|
lens: string | null
|
||||||
|
dateShot: any | null
|
||||||
|
/**
|
||||||
|
* The exposure time of the image
|
||||||
|
*/
|
||||||
|
exposure: number | null
|
||||||
|
/**
|
||||||
|
* The aperature stops of the image
|
||||||
|
*/
|
||||||
|
aperture: number | null
|
||||||
|
/**
|
||||||
|
* The ISO setting of the image
|
||||||
|
*/
|
||||||
|
iso: number | null
|
||||||
|
/**
|
||||||
|
* The focal length of the lens, when the image was taken
|
||||||
|
*/
|
||||||
|
focalLength: number | null
|
||||||
|
/**
|
||||||
|
* A formatted description of the flash settings, when the image was taken
|
||||||
|
*/
|
||||||
|
flash: number | null
|
||||||
|
/**
|
||||||
|
* An index describing the mode for adjusting the exposure of the image
|
||||||
|
*/
|
||||||
|
exposureProgram: number | null
|
||||||
|
/**
|
||||||
|
* GPS coordinates of where the image was taken
|
||||||
|
*/
|
||||||
|
coordinates: sidebarMediaQuery_media_exif_coordinates | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_album_path {
|
||||||
|
__typename: 'Album'
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_album {
|
||||||
|
__typename: 'Album'
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
path: sidebarMediaQuery_media_album_path[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_faces_rectangle {
|
||||||
|
__typename: 'FaceRectangle'
|
||||||
|
minX: number
|
||||||
|
maxX: number
|
||||||
|
minY: number
|
||||||
|
maxY: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_faces_faceGroup {
|
||||||
|
__typename: 'FaceGroup'
|
||||||
|
id: string
|
||||||
|
label: string | null
|
||||||
|
imageFaceCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_faces_media_thumbnail {
|
||||||
|
__typename: 'MediaURL'
|
||||||
|
/**
|
||||||
|
* URL for previewing the image
|
||||||
|
*/
|
||||||
|
url: string
|
||||||
|
/**
|
||||||
|
* Width of the image in pixels
|
||||||
|
*/
|
||||||
|
width: number
|
||||||
|
/**
|
||||||
|
* Height of the image in pixels
|
||||||
|
*/
|
||||||
|
height: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_faces_media {
|
||||||
|
__typename: 'Media'
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: sidebarMediaQuery_media_faces_media_thumbnail | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media_faces {
|
||||||
|
__typename: 'ImageFace'
|
||||||
|
id: string
|
||||||
|
rectangle: sidebarMediaQuery_media_faces_rectangle
|
||||||
|
faceGroup: sidebarMediaQuery_media_faces_faceGroup
|
||||||
|
media: sidebarMediaQuery_media_faces_media
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery_media {
|
||||||
|
__typename: 'Media'
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
type: MediaType
|
||||||
|
/**
|
||||||
|
* URL to display the photo in full resolution, will be null for videos
|
||||||
|
*/
|
||||||
|
highRes: sidebarMediaQuery_media_highRes | null
|
||||||
|
/**
|
||||||
|
* URL to display the media in a smaller resolution
|
||||||
|
*/
|
||||||
|
thumbnail: sidebarMediaQuery_media_thumbnail | null
|
||||||
|
/**
|
||||||
|
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
||||||
|
*/
|
||||||
|
videoWeb: sidebarMediaQuery_media_videoWeb | null
|
||||||
|
videoMetadata: sidebarMediaQuery_media_videoMetadata | null
|
||||||
|
exif: sidebarMediaQuery_media_exif | null
|
||||||
|
/**
|
||||||
|
* The album that holds the media
|
||||||
|
*/
|
||||||
|
album: sidebarMediaQuery_media_album
|
||||||
|
faces: sidebarMediaQuery_media_faces[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQuery {
|
||||||
|
/**
|
||||||
|
* Get media by id, user must own the media or be admin.
|
||||||
|
* If valid tokenCredentials are provided, the media may be retrived without further authentication
|
||||||
|
*/
|
||||||
|
media: sidebarMediaQuery_media
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface sidebarMediaQueryVariables {
|
||||||
|
id: string
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="12px" height="3px" viewBox="0 0 8 2" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path d="M1,0 C1.55228475,0 2,0.44771525 2,1 C2,1.55228475 1.55228475,2 1,2 C0.44771525,2 0,1.55228475 0,1 C0,0.44771525 0.44771525,0 1,0 Z M4,0 C4.55228475,0 5,0.44771525 5,1 C5,1.55228475 4.55228475,2 4,2 C3.44771525,2 3,1.55228475 3,1 C3,0.44771525 3.44771525,0 4,0 Z M7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 Z" fill="currentColor"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 583 B |
|
@ -13,7 +13,6 @@ import {
|
||||||
sidebareDeleteShare,
|
sidebareDeleteShare,
|
||||||
sidebareDeleteShareVariables,
|
sidebareDeleteShareVariables,
|
||||||
} from './__generated__/sidebareDeleteShare'
|
} from './__generated__/sidebareDeleteShare'
|
||||||
import { sidbarGetPhotoShares_media_shares } from './__generated__/sidbarGetPhotoShares'
|
|
||||||
import {
|
import {
|
||||||
sidebarPhotoAddShare,
|
sidebarPhotoAddShare,
|
||||||
sidebarPhotoAddShareVariables,
|
sidebarPhotoAddShareVariables,
|
||||||
|
@ -25,6 +24,7 @@ import {
|
||||||
import {
|
import {
|
||||||
sidebarGetPhotoShares,
|
sidebarGetPhotoShares,
|
||||||
sidebarGetPhotoSharesVariables,
|
sidebarGetPhotoSharesVariables,
|
||||||
|
sidebarGetPhotoShares_media_shares,
|
||||||
} from './__generated__/sidebarGetPhotoShares'
|
} from './__generated__/sidebarGetPhotoShares'
|
||||||
import {
|
import {
|
||||||
sidebarGetAlbumShares,
|
sidebarGetAlbumShares,
|
||||||
|
@ -106,18 +106,38 @@ const DELETE_SHARE_MUTATION = gql`
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ArrowPopoverPanel = styled.div.attrs({
|
export const ArrowPopoverPanel = styled.div.attrs({
|
||||||
className:
|
className:
|
||||||
'absolute right-6 -top-3 bg-white rounded shadow-md border border-gray-200 w-[260px]',
|
'absolute -top-3 bg-white rounded shadow-md border border-gray-200 z-10',
|
||||||
})`
|
})<{ width: number; flipped?: boolean }>`
|
||||||
|
width: ${({ width }) => width}px;
|
||||||
|
|
||||||
|
${({ flipped }) =>
|
||||||
|
flipped
|
||||||
|
? `
|
||||||
|
left: 32px;
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
right: 24px;
|
||||||
|
`}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 18px;
|
top: 18px;
|
||||||
right: -7px;
|
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 14'%3E%3Cpolyline stroke-width='1' stroke='%23E2E2E2' fill='%23FFFFFF' points='1 0 7 7 1 14'%3E%3C/polyline%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 14'%3E%3Cpolyline stroke-width='1' stroke='%23E2E2E2' fill='%23FFFFFF' points='1 0 7 7 1 14'%3E%3C/polyline%3E%3C/svg%3E");
|
||||||
|
|
||||||
|
${({ flipped }) =>
|
||||||
|
flipped
|
||||||
|
? `
|
||||||
|
left: -7px;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
right: -7px;
|
||||||
|
`}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
@ -247,7 +267,7 @@ const MorePopover = ({ id, share, query }: MorePopoverProps) => {
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Popover.Panel>
|
<Popover.Panel>
|
||||||
<ArrowPopoverPanel>
|
<ArrowPopoverPanel width={260}>
|
||||||
<MorePopoverSectionPassword id={id} share={share} query={query} />
|
<MorePopoverSectionPassword id={id} share={share} query={query} />
|
||||||
<div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2">
|
<div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2">
|
||||||
<Checkbox label="Expiration date" />
|
<Checkbox label="Expiration date" />
|
||||||
|
@ -358,7 +378,7 @@ type SidebarShareProps = {
|
||||||
id: string
|
id: string
|
||||||
isPhoto: boolean
|
isPhoto: boolean
|
||||||
loading: boolean
|
loading: boolean
|
||||||
shares?: sidbarGetPhotoShares_media_shares[]
|
shares?: sidebarGetPhotoShares_media_shares[]
|
||||||
shareItem(item: { variables: { id: string } }): void
|
shareItem(item: { variables: { id: string } }): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useLazyQuery, gql } from '@apollo/client'
|
||||||
import { authToken } from '../../helpers/authentication'
|
import { authToken } from '../../helpers/authentication'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { TranslationFn } from '../../localization'
|
import { TranslationFn } from '../../localization'
|
||||||
import { MediaSidebarMedia } from './MediaSidebar'
|
import { MediaSidebarMedia } from './MediaSidebar/MediaSidebar'
|
||||||
import {
|
import {
|
||||||
sidebarDownloadQuery,
|
sidebarDownloadQuery,
|
||||||
sidebarDownloadQueryVariables,
|
sidebarDownloadQueryVariables,
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL mutation operation: setAlbumCoverID
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface setAlbumCoverID_setAlbumCoverID {
|
|
||||||
__typename: 'Album'
|
|
||||||
id: string
|
|
||||||
coverID: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface setAlbumCoverID {
|
|
||||||
/**
|
|
||||||
* Assign a cover image to an album, set coverID to -1 to remove the current one
|
|
||||||
*/
|
|
||||||
setAlbumCoverID: setAlbumCoverID_setAlbumCoverID
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface setAlbumCoverIDVariables {
|
|
||||||
albumID: string
|
|
||||||
coverID: string
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL query operation: sidbarGetAlbumShares
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface sidbarGetAlbumShares_album_shares {
|
|
||||||
__typename: "ShareToken";
|
|
||||||
id: string;
|
|
||||||
token: string;
|
|
||||||
/**
|
|
||||||
* Whether or not a password is needed to access the share
|
|
||||||
*/
|
|
||||||
hasPassword: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidbarGetAlbumShares_album {
|
|
||||||
__typename: "Album";
|
|
||||||
id: string;
|
|
||||||
shares: (sidbarGetAlbumShares_album_shares | null)[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidbarGetAlbumShares {
|
|
||||||
/**
|
|
||||||
* Get album by id, user must own the album or be admin
|
|
||||||
* If valid tokenCredentials are provided, the album may be retrived without further authentication
|
|
||||||
*/
|
|
||||||
album: sidbarGetAlbumShares_album;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidbarGetAlbumSharesVariables {
|
|
||||||
id: string;
|
|
||||||
}
|
|
|
@ -1,36 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL query operation: sidbarGetPhotoShares
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface sidbarGetPhotoShares_media_shares {
|
|
||||||
__typename: "ShareToken";
|
|
||||||
id: string;
|
|
||||||
token: string;
|
|
||||||
/**
|
|
||||||
* Whether or not a password is needed to access the share
|
|
||||||
*/
|
|
||||||
hasPassword: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidbarGetPhotoShares_media {
|
|
||||||
__typename: "Media";
|
|
||||||
id: string;
|
|
||||||
shares: sidbarGetPhotoShares_media_shares[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidbarGetPhotoShares {
|
|
||||||
/**
|
|
||||||
* Get media by id, user must own the media or be admin.
|
|
||||||
* If valid tokenCredentials are provided, the media may be retrived without further authentication
|
|
||||||
*/
|
|
||||||
media: sidbarGetPhotoShares_media;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidbarGetPhotoSharesVariables {
|
|
||||||
id: string;
|
|
||||||
}
|
|
|
@ -1,167 +0,0 @@
|
||||||
/* tslint:disable */
|
|
||||||
/* eslint-disable */
|
|
||||||
// @generated
|
|
||||||
// This file was automatically generated and should not be edited.
|
|
||||||
|
|
||||||
import { MediaType } from "./../../../__generated__/globalTypes";
|
|
||||||
|
|
||||||
// ====================================================
|
|
||||||
// GraphQL query operation: sidebarPhoto
|
|
||||||
// ====================================================
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_highRes {
|
|
||||||
__typename: "MediaURL";
|
|
||||||
/**
|
|
||||||
* URL for previewing the image
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* Width of the image in pixels
|
|
||||||
*/
|
|
||||||
width: number;
|
|
||||||
/**
|
|
||||||
* Height of the image in pixels
|
|
||||||
*/
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_thumbnail {
|
|
||||||
__typename: "MediaURL";
|
|
||||||
/**
|
|
||||||
* URL for previewing the image
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* Width of the image in pixels
|
|
||||||
*/
|
|
||||||
width: number;
|
|
||||||
/**
|
|
||||||
* Height of the image in pixels
|
|
||||||
*/
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_videoWeb {
|
|
||||||
__typename: "MediaURL";
|
|
||||||
/**
|
|
||||||
* URL for previewing the image
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
/**
|
|
||||||
* Width of the image in pixels
|
|
||||||
*/
|
|
||||||
width: number;
|
|
||||||
/**
|
|
||||||
* Height of the image in pixels
|
|
||||||
*/
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_videoMetadata {
|
|
||||||
__typename: "VideoMetadata";
|
|
||||||
id: string;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
duration: number;
|
|
||||||
codec: string | null;
|
|
||||||
framerate: number | null;
|
|
||||||
bitrate: string | null;
|
|
||||||
colorProfile: string | null;
|
|
||||||
audio: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_exif {
|
|
||||||
__typename: "MediaEXIF";
|
|
||||||
id: string;
|
|
||||||
/**
|
|
||||||
* The model name of the camera
|
|
||||||
*/
|
|
||||||
camera: string | null;
|
|
||||||
/**
|
|
||||||
* The maker of the camera
|
|
||||||
*/
|
|
||||||
maker: string | null;
|
|
||||||
/**
|
|
||||||
* The name of the lens
|
|
||||||
*/
|
|
||||||
lens: string | null;
|
|
||||||
dateShot: any | null;
|
|
||||||
/**
|
|
||||||
* The exposure time of the image
|
|
||||||
*/
|
|
||||||
exposure: number | null;
|
|
||||||
/**
|
|
||||||
* The aperature stops of the image
|
|
||||||
*/
|
|
||||||
aperture: number | null;
|
|
||||||
/**
|
|
||||||
* The ISO setting of the image
|
|
||||||
*/
|
|
||||||
iso: number | null;
|
|
||||||
/**
|
|
||||||
* The focal length of the lens, when the image was taken
|
|
||||||
*/
|
|
||||||
focalLength: number | null;
|
|
||||||
/**
|
|
||||||
* A formatted description of the flash settings, when the image was taken
|
|
||||||
*/
|
|
||||||
flash: number | null;
|
|
||||||
/**
|
|
||||||
* An index describing the mode for adjusting the exposure of the image
|
|
||||||
*/
|
|
||||||
exposureProgram: number | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_faces_rectangle {
|
|
||||||
__typename: "FaceRectangle";
|
|
||||||
minX: number;
|
|
||||||
maxX: number;
|
|
||||||
minY: number;
|
|
||||||
maxY: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_faces_faceGroup {
|
|
||||||
__typename: "FaceGroup";
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media_faces {
|
|
||||||
__typename: "ImageFace";
|
|
||||||
id: string;
|
|
||||||
rectangle: sidebarPhoto_media_faces_rectangle;
|
|
||||||
faceGroup: sidebarPhoto_media_faces_faceGroup;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto_media {
|
|
||||||
__typename: "Media";
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
type: MediaType;
|
|
||||||
/**
|
|
||||||
* URL to display the photo in full resolution, will be null for videos
|
|
||||||
*/
|
|
||||||
highRes: sidebarPhoto_media_highRes | null;
|
|
||||||
/**
|
|
||||||
* URL to display the media in a smaller resolution
|
|
||||||
*/
|
|
||||||
thumbnail: sidebarPhoto_media_thumbnail | null;
|
|
||||||
/**
|
|
||||||
* URL to get the video in a web format that can be played in the browser, will be null for photos
|
|
||||||
*/
|
|
||||||
videoWeb: sidebarPhoto_media_videoWeb | null;
|
|
||||||
videoMetadata: sidebarPhoto_media_videoMetadata | null;
|
|
||||||
exif: sidebarPhoto_media_exif | null;
|
|
||||||
faces: sidebarPhoto_media_faces[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhoto {
|
|
||||||
/**
|
|
||||||
* Get media by id, user must own the media or be admin.
|
|
||||||
* If valid tokenCredentials are provided, the media may be retrived without further authentication
|
|
||||||
*/
|
|
||||||
media: sidebarPhoto_media;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface sidebarPhotoVariables {
|
|
||||||
id: string;
|
|
||||||
}
|
|
|
@ -53,7 +53,9 @@ const DateSelector = ({ filterDate, setFilterDate }: DateSelectorProps) => {
|
||||||
|
|
||||||
const yearItems = years.map<DropdownItem>(x => ({
|
const yearItems = years.map<DropdownItem>(x => ({
|
||||||
value: `${x}`,
|
value: `${x}`,
|
||||||
label: `${x} and earlier`,
|
label: t('timeline_filter.date.dropdown_year', '{{year}} and earlier', {
|
||||||
|
year: x,
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
items = [...items, ...yearItems]
|
items = [...items, ...yearItems]
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
toggleFavoriteAction,
|
toggleFavoriteAction,
|
||||||
useMarkFavoriteMutation,
|
useMarkFavoriteMutation,
|
||||||
} from '../photoGallery/photoGalleryMutations'
|
} from '../photoGallery/photoGalleryMutations'
|
||||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar'
|
||||||
import { SidebarContext } from '../sidebar/Sidebar'
|
import { SidebarContext } from '../sidebar/Sidebar'
|
||||||
import {
|
import {
|
||||||
getActiveTimelineImage,
|
getActiveTimelineImage,
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "Ændre navn",
|
"change_label": "Ændre navn",
|
||||||
"detach_face": "Løsriv billeder",
|
"detach_images": "Løsriv billeder",
|
||||||
"merge_face": "Sammenflet personer",
|
"merge_people": "Sammenflet personer",
|
||||||
"move_faces": "Flyt ansigter"
|
"move_faces": "Flyt ansigter"
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Download"
|
"title": "Download"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": "Lokation"
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "Album sti",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Actionprogram",
|
"action_program": "Actionprogram",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Blænde",
|
"aperture": "Blænde",
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
|
"coordinates": "Koordinater",
|
||||||
"date_shot": "Dato",
|
"date_shot": "Dato",
|
||||||
"exposure": "Lukketid",
|
"exposure": "Lukketid",
|
||||||
"exposure_program": "Lukketid program",
|
"exposure_program": "Lukketid program",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "Løsriv billede",
|
||||||
|
"merge_face": "Sammenflet person",
|
||||||
|
"move_face": "Flyt person"
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "Er du sikker på at du vil løsrive dette billede?",
|
||||||
|
"title": "Personer"
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Tilføj deling",
|
"add_share": "Tilføj deling",
|
||||||
"copy_link": "Kopier link",
|
"copy_link": "Kopier link",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "Fra i dag",
|
"dropdown_all": "Fra i dag",
|
||||||
|
"dropdown_year": "{{year}} og tidligere",
|
||||||
"label": "Dato"
|
"label": "Dato"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -42,6 +42,15 @@
|
||||||
"merge_face": null,
|
"merge_face": null,
|
||||||
"detach_face": null,
|
"detach_face": null,
|
||||||
"move_faces": null
|
"move_faces": null
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "Alle billeder fra denne gruppe vil blive flettet sammen med den valgte gruppe",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "Vælg destinationsgruppe"
|
||||||
|
},
|
||||||
|
"title": "Vælg gruppe at flette med"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
@ -71,6 +80,9 @@
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": null,
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": "Album"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "",
|
"change_label": "",
|
||||||
"detach_face": "",
|
"detach_images": "",
|
||||||
"merge_face": "",
|
"merge_people": "",
|
||||||
"move_faces": ""
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Download"
|
"title": "Download"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Action (kurze Verschlusszeit)",
|
"action_program": "Action (kurze Verschlusszeit)",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Blende",
|
"aperture": "Blende",
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Aufnahmedatum",
|
"date_shot": "Aufnahmedatum",
|
||||||
"exposure": "Belichtung",
|
"exposure": "Belichtung",
|
||||||
"exposure_program": "Programm",
|
"exposure_program": "Programm",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Freigabe hinzufügen",
|
"add_share": "Freigabe hinzufügen",
|
||||||
"copy_link": "Link kopieren",
|
"copy_link": "Link kopieren",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "",
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": ""
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,6 +61,13 @@
|
||||||
"title": null
|
"title": null
|
||||||
},
|
},
|
||||||
"title": null
|
"title": null
|
||||||
|
},
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "",
|
||||||
|
"destination_table": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
@ -115,6 +122,9 @@
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": null,
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "Change label",
|
"change_label": "Change label",
|
||||||
"detach_face": "Detach face",
|
"detach_images": "Detach face",
|
||||||
"merge_face": "Merge face",
|
"merge_people": "Merge face",
|
||||||
"move_faces": "Move faces"
|
"move_faces": "Move faces"
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Download"
|
"title": "Download"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": "Location"
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "Album path",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Action program",
|
"action_program": "Action program",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Aperture",
|
"aperture": "Aperture",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
|
"coordinates": "Coordinates",
|
||||||
"date_shot": "Date shot",
|
"date_shot": "Date shot",
|
||||||
"exposure": "Exposure",
|
"exposure": "Exposure",
|
||||||
"exposure_program": "Program",
|
"exposure_program": "Program",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "Detach image",
|
||||||
|
"merge_face": "Merge face",
|
||||||
|
"move_face": "Move face"
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "Are you sure you want to detach this image?",
|
||||||
|
"title": "People"
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Add shares",
|
"add_share": "Add shares",
|
||||||
"copy_link": "Copy Link",
|
"copy_link": "Copy Link",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "From today",
|
"dropdown_all": "From today",
|
||||||
|
"dropdown_year": "{{year}} and earlier",
|
||||||
"label": "Date"
|
"label": "Date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -36,6 +36,15 @@
|
||||||
"select_image_faces": {
|
"select_image_faces": {
|
||||||
"search_images_placeholder": "Search images..."
|
"search_images_placeholder": "Search images..."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "All images within this face group will be merged into the selected face group.",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "Select the destination face"
|
||||||
|
},
|
||||||
|
"title": "Merge Face Groups"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
@ -54,6 +63,9 @@
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Public shares"
|
"table_header": "Public shares"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": "Album"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "",
|
"change_label": "",
|
||||||
"detach_face": "",
|
"detach_images": "",
|
||||||
"merge_face": "",
|
"merge_people": "",
|
||||||
"move_faces": ""
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Descargar"
|
"title": "Descargar"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Programa de acción",
|
"action_program": "Programa de acción",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Abertura",
|
"aperture": "Abertura",
|
||||||
"camera": "Cámara",
|
"camera": "Cámara",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Fecha de captura",
|
"date_shot": "Fecha de captura",
|
||||||
"exposure": "Exposición",
|
"exposure": "Exposición",
|
||||||
"exposure_program": "Programa",
|
"exposure_program": "Programa",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Añadir compartido",
|
"add_share": "Añadir compartido",
|
||||||
"copy_link": "Copiar enlace",
|
"copy_link": "Copiar enlace",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "",
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": ""
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,6 +61,13 @@
|
||||||
"title": null
|
"title": null
|
||||||
},
|
},
|
||||||
"title": null
|
"title": null
|
||||||
|
},
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "",
|
||||||
|
"destination_table": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
@ -120,6 +127,9 @@
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": null,
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "Changer le label",
|
"change_label": "Changer le label",
|
||||||
"detach_face": "Détacher le visage",
|
"detach_images": "Détacher le visage",
|
||||||
"merge_face": "Fusionner le visage",
|
"merge_people": "Fusionner le visage",
|
||||||
"move_faces": "Déplacer les visages"
|
"move_faces": "Déplacer les visages"
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Télécharger"
|
"title": "Télécharger"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Programme d'action",
|
"action_program": "Programme d'action",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Ouverture",
|
"aperture": "Ouverture",
|
||||||
"camera": "Modèle",
|
"camera": "Modèle",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Prise de vue",
|
"date_shot": "Prise de vue",
|
||||||
"exposure": "Vitesse",
|
"exposure": "Vitesse",
|
||||||
"exposure_program": "Programme",
|
"exposure_program": "Programme",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Ajouter un partage",
|
"add_share": "Ajouter un partage",
|
||||||
"copy_link": "Copier le lien",
|
"copy_link": "Copier le lien",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "Depuis aujourd'hui",
|
"dropdown_all": "Depuis aujourd'hui",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": "Date"
|
"label": "Date"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -60,6 +60,13 @@
|
||||||
"title": null
|
"title": null
|
||||||
},
|
},
|
||||||
"title": null
|
"title": null
|
||||||
|
},
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "Toutes les images de ce groupe de visages seront fusionnées dans le groupe de visages sélectionné.",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "Sélectionner le visage de destination"
|
||||||
|
},
|
||||||
|
"title": "Fusionner des groupes de visages"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
@ -95,6 +102,9 @@
|
||||||
},
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"table_header": "Partages publics"
|
"table_header": "Partages publics"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "",
|
"change_label": "",
|
||||||
"detach_face": "",
|
"detach_images": "",
|
||||||
"merge_face": "",
|
"merge_people": "",
|
||||||
"move_faces": ""
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Download"
|
"title": "Download"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Programma sport",
|
"action_program": "Programma sport",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Apertura",
|
"aperture": "Apertura",
|
||||||
"camera": "Fotocamera",
|
"camera": "Fotocamera",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Data di scatto",
|
"date_shot": "Data di scatto",
|
||||||
"exposure": "Esposizione",
|
"exposure": "Esposizione",
|
||||||
"exposure_program": "Programma",
|
"exposure_program": "Programma",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Aggiungi condivisione",
|
"add_share": "Aggiungi condivisione",
|
||||||
"copy_link": "Copia il link",
|
"copy_link": "Copia il link",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "",
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": ""
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,6 +49,15 @@
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": null
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "Tutte le immagini di questo volto saranno unite alle immagini del volto selezionato",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "Seleziona il volto di destinazione"
|
||||||
|
},
|
||||||
|
"title": "Unisci immagini volto"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
@ -82,6 +91,9 @@
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": null,
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "",
|
"change_label": "",
|
||||||
"detach_face": "",
|
"detach_images": "",
|
||||||
"merge_face": "",
|
"merge_people": "",
|
||||||
"move_faces": ""
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -264,7 +264,11 @@
|
||||||
},
|
},
|
||||||
"title": "Pobierz"
|
"title": "Pobierz"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Program działania",
|
"action_program": "Program działania",
|
||||||
|
@ -293,6 +297,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Przysłona",
|
"aperture": "Przysłona",
|
||||||
"camera": "Aparat ",
|
"camera": "Aparat ",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Data wykonania",
|
"date_shot": "Data wykonania",
|
||||||
"exposure": "Ekspozycja",
|
"exposure": "Ekspozycja",
|
||||||
"exposure_program": "Program",
|
"exposure_program": "Program",
|
||||||
|
@ -304,6 +309,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Dodaj udział",
|
"add_share": "Dodaj udział",
|
||||||
"copy_link": "Skopiuj link",
|
"copy_link": "Skopiuj link",
|
||||||
|
@ -324,6 +338,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "",
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": ""
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,6 +61,13 @@
|
||||||
"title": null
|
"title": null
|
||||||
},
|
},
|
||||||
"title": null
|
"title": null
|
||||||
|
},
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "",
|
||||||
|
"destination_table": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
@ -137,6 +144,9 @@
|
||||||
"table_header": "Publiczne udostępnienia",
|
"table_header": "Publiczne udostępnienia",
|
||||||
"delete": null,
|
"delete": null,
|
||||||
"more": null
|
"more": null
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|
|
@ -61,17 +61,17 @@
|
||||||
"description": "Galeria de fotos simples e fácil de usar para servidores pessoais"
|
"description": "Galeria de fotos simples e fácil de usar para servidores pessoais"
|
||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
|
"action_label": {
|
||||||
|
"change_label": "",
|
||||||
|
"detach_images": "",
|
||||||
|
"merge_people": "",
|
||||||
|
"move_faces": ""
|
||||||
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "Etiqueta",
|
"label_placeholder": "Etiqueta",
|
||||||
"unlabeled": "Sem etiqueta",
|
"unlabeled": "Sem etiqueta",
|
||||||
"unlabeled_person": "Pessoa sem etiqueta"
|
"unlabeled_person": "Pessoa sem etiqueta"
|
||||||
},
|
},
|
||||||
"action_label": {
|
|
||||||
"change_label": null,
|
|
||||||
"merge_face": null,
|
|
||||||
"detach_face": null,
|
|
||||||
"move_faces": null
|
|
||||||
},
|
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": "Juntar"
|
"merge": "Juntar"
|
||||||
|
@ -215,6 +215,27 @@
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
|
"album_cover": "",
|
||||||
|
"download": {
|
||||||
|
"high-resolutions": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
"title_placeholder": "Título do Álbum"
|
"title_placeholder": "Título do Álbum"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
|
@ -238,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Descarregar"
|
"title": "Descarregar"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Programa de ação",
|
"action_program": "Programa de ação",
|
||||||
|
@ -267,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Abertura",
|
"aperture": "Abertura",
|
||||||
"camera": "Câmera",
|
"camera": "Câmera",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Data da foto",
|
"date_shot": "Data da foto",
|
||||||
"exposure": "Exposição",
|
"exposure": "Exposição",
|
||||||
"exposure_program": "Programa",
|
"exposure_program": "Programa",
|
||||||
|
@ -278,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Adicionar partilha",
|
"add_share": "Adicionar partilha",
|
||||||
"copy_link": "Copiar link",
|
"copy_link": "Copiar link",
|
||||||
|
@ -295,6 +330,13 @@
|
||||||
"places": "Locais",
|
"places": "Locais",
|
||||||
"settings": "Configurações"
|
"settings": "Configurações"
|
||||||
},
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"loading_album": "Carregar Álbum",
|
"loading_album": "Carregar Álbum",
|
||||||
"login": "Login",
|
"login": "Login",
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"people_page": {
|
||||||
|
"action_label": {
|
||||||
|
"change_label": null,
|
||||||
|
"merge_people": null,
|
||||||
|
"detach_images": null,
|
||||||
|
"move_faces": null
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "Todas as imagens neste grupo de caras serão juntas no grupo selecionado.",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "Selecione a cara de destino"
|
||||||
|
},
|
||||||
|
"title": "Juntar grupos de cara"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "",
|
"change_label": "",
|
||||||
"detach_face": "",
|
"detach_images": "",
|
||||||
"merge_face": "",
|
"merge_people": "",
|
||||||
"move_faces": ""
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -264,7 +264,11 @@
|
||||||
},
|
},
|
||||||
"title": "Скачать"
|
"title": "Скачать"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Действие программа",
|
"action_program": "Действие программа",
|
||||||
|
@ -293,6 +297,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Диафрагма",
|
"aperture": "Диафрагма",
|
||||||
"camera": "Камера",
|
"camera": "Камера",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Дата снимка",
|
"date_shot": "Дата снимка",
|
||||||
"exposure": "Экспозиция",
|
"exposure": "Экспозиция",
|
||||||
"exposure_program": "Программа",
|
"exposure_program": "Программа",
|
||||||
|
@ -304,6 +309,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Поделится",
|
"add_share": "Поделится",
|
||||||
"copy_link": "Скопировать ссылку",
|
"copy_link": "Скопировать ссылку",
|
||||||
|
@ -324,6 +338,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "",
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": ""
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,6 +49,15 @@
|
||||||
},
|
},
|
||||||
"tableselect_image_faces": {
|
"tableselect_image_faces": {
|
||||||
"search_images_placeholder": null
|
"search_images_placeholder": null
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "Все фотографии в этой группе лиц будут обьеденены с выбранной группой.",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "Выберете конечное лицо"
|
||||||
|
},
|
||||||
|
"title": "Обьединить группы лиц"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
|
@ -99,6 +108,9 @@
|
||||||
"table_header": "Общий доступ",
|
"table_header": "Общий доступ",
|
||||||
"delete": null,
|
"delete": null,
|
||||||
"more": null
|
"more": null
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"album_filter": {
|
"album_filter": {
|
||||||
|
|
|
@ -63,8 +63,8 @@
|
||||||
"people_page": {
|
"people_page": {
|
||||||
"action_label": {
|
"action_label": {
|
||||||
"change_label": "",
|
"change_label": "",
|
||||||
"detach_face": "",
|
"detach_images": "",
|
||||||
"merge_face": "",
|
"merge_people": "",
|
||||||
"move_faces": ""
|
"move_faces": ""
|
||||||
},
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
|
@ -259,7 +259,11 @@
|
||||||
},
|
},
|
||||||
"title": "Ladda ner"
|
"title": "Ladda ner"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "Actionprogram",
|
"action_program": "Actionprogram",
|
||||||
|
@ -288,6 +292,7 @@
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "Bländare",
|
"aperture": "Bländare",
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "Datum tagen",
|
"date_shot": "Datum tagen",
|
||||||
"exposure": "Exponering",
|
"exposure": "Exponering",
|
||||||
"exposure_program": "Program",
|
"exposure_program": "Program",
|
||||||
|
@ -299,6 +304,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "Dela",
|
"add_share": "Dela",
|
||||||
"copy_link": "Kopiera länk",
|
"copy_link": "Kopiera länk",
|
||||||
|
@ -319,6 +333,7 @@
|
||||||
"timeline_filter": {
|
"timeline_filter": {
|
||||||
"date": {
|
"date": {
|
||||||
"dropdown_all": "",
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
"label": ""
|
"label": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,6 +61,13 @@
|
||||||
"title": null
|
"title": null
|
||||||
},
|
},
|
||||||
"title": null
|
"title": null
|
||||||
|
},
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "",
|
||||||
|
"destination_table": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"title": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
|
@ -123,6 +130,9 @@
|
||||||
"mega_byte_plural": null,
|
"mega_byte_plural": null,
|
||||||
"tera_byte_plural": null
|
"tera_byte_plural": null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"album": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"title": {
|
"title": {
|
||||||
|
|
|
@ -61,17 +61,17 @@
|
||||||
"description": "简单及易用的照片库给个人服务器"
|
"description": "简单及易用的照片库给个人服务器"
|
||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
|
"action_label": {
|
||||||
|
"change_label": "",
|
||||||
|
"detach_images": "",
|
||||||
|
"merge_people": "",
|
||||||
|
"move_faces": ""
|
||||||
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "名称",
|
"label_placeholder": "名称",
|
||||||
"unlabeled": "未命名",
|
"unlabeled": "未命名",
|
||||||
"unlabeled_person": "未命名人物"
|
"unlabeled_person": "未命名人物"
|
||||||
},
|
},
|
||||||
"action_label": {
|
|
||||||
"change_label": null,
|
|
||||||
"merge_face": null,
|
|
||||||
"detach_face": null,
|
|
||||||
"move_faces": null
|
|
||||||
},
|
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": "合并"
|
"merge": "合并"
|
||||||
|
@ -215,30 +215,36 @@
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
|
"album_cover": "",
|
||||||
|
"download": {
|
||||||
|
"high-resolutions": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
"title_placeholder": "相册名称"
|
"title_placeholder": "相册名称"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte": null,
|
"byte": "",
|
||||||
"giga_byte": null,
|
"giga_byte": "",
|
||||||
"kilo_byte": null,
|
"kilo_byte": "",
|
||||||
"mega_byte": null,
|
"mega_byte": "",
|
||||||
"tera_byte": null,
|
"tera_byte": ""
|
||||||
"byte_0": null,
|
|
||||||
"byte_1": null,
|
|
||||||
"byte_2": null,
|
|
||||||
"giga_byte_0": null,
|
|
||||||
"giga_byte_1": null,
|
|
||||||
"giga_byte_2": null,
|
|
||||||
"kilo_byte_0": null,
|
|
||||||
"kilo_byte_1": null,
|
|
||||||
"kilo_byte_2": null,
|
|
||||||
"mega_byte_0": null,
|
|
||||||
"mega_byte_1": null,
|
|
||||||
"mega_byte_2": null,
|
|
||||||
"tera_byte_0": null,
|
|
||||||
"tera_byte_1": null,
|
|
||||||
"tera_byte_2": null
|
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "尺寸",
|
"dimensions": "尺寸",
|
||||||
|
@ -248,7 +254,11 @@
|
||||||
},
|
},
|
||||||
"title": "下载"
|
"title": "下载"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "动态模式",
|
"action_program": "动态模式",
|
||||||
|
@ -264,19 +274,20 @@
|
||||||
},
|
},
|
||||||
"flash": {
|
"flash": {
|
||||||
"auto": "自动",
|
"auto": "自动",
|
||||||
"did_not_fire": null,
|
"did_not_fire": "",
|
||||||
"fired": null,
|
"fired": "",
|
||||||
"no_flash": null,
|
"no_flash": "",
|
||||||
"no_flash_function": null,
|
"no_flash_function": "",
|
||||||
"off": "关闭",
|
"off": "关闭",
|
||||||
"on": "使用",
|
"on": "使用",
|
||||||
"red_eye_reduction": "减轻红眼",
|
"red_eye_reduction": "减轻红眼",
|
||||||
"return_detected": null,
|
"return_detected": "",
|
||||||
"return_not_detected": null
|
"return_not_detected": ""
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "光圈",
|
"aperture": "光圈",
|
||||||
"camera": "相机",
|
"camera": "相机",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "拍摄日期",
|
"date_shot": "拍摄日期",
|
||||||
"exposure": "曝光",
|
"exposure": "曝光",
|
||||||
"exposure_program": "模式",
|
"exposure_program": "模式",
|
||||||
|
@ -288,6 +299,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "新增分享",
|
"add_share": "新增分享",
|
||||||
"copy_link": "复制链接",
|
"copy_link": "复制链接",
|
||||||
|
@ -305,6 +325,13 @@
|
||||||
"places": "地点",
|
"places": "地点",
|
||||||
"settings": "设置"
|
"settings": "设置"
|
||||||
},
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"loading_album": "载入相册",
|
"loading_album": "载入相册",
|
||||||
"login": "登入",
|
"login": "登入",
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"people_page": {
|
||||||
|
"action_label": {
|
||||||
|
"change_label": null,
|
||||||
|
"merge_people": null,
|
||||||
|
"detach_images": null,
|
||||||
|
"move_faces": null
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "所有在此的脸孔群组将会合并到已选择的脸孔群组",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "选择目标脸孔群组"
|
||||||
|
},
|
||||||
|
"title": "合并脸孔群组"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"download": {
|
||||||
|
"filesize": {
|
||||||
|
"byte": null,
|
||||||
|
"giga_byte": null,
|
||||||
|
"kilo_byte": null,
|
||||||
|
"mega_byte": null,
|
||||||
|
"tera_byte": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"exif": {
|
||||||
|
"flash": {
|
||||||
|
"did_not_fire": null,
|
||||||
|
"fired": null,
|
||||||
|
"no_flash": null,
|
||||||
|
"no_flash_function": null,
|
||||||
|
"return_detected": null,
|
||||||
|
"return_not_detected": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -61,17 +61,17 @@
|
||||||
"description": "簡單及易用的照片庫給個人伺服器"
|
"description": "簡單及易用的照片庫給個人伺服器"
|
||||||
},
|
},
|
||||||
"people_page": {
|
"people_page": {
|
||||||
|
"action_label": {
|
||||||
|
"change_label": "",
|
||||||
|
"detach_images": "",
|
||||||
|
"merge_people": "",
|
||||||
|
"move_faces": ""
|
||||||
|
},
|
||||||
"face_group": {
|
"face_group": {
|
||||||
"label_placeholder": "名稱",
|
"label_placeholder": "名稱",
|
||||||
"unlabeled": "未命名",
|
"unlabeled": "未命名",
|
||||||
"unlabeled_person": "未命名人物"
|
"unlabeled_person": "未命名人物"
|
||||||
},
|
},
|
||||||
"action_label": {
|
|
||||||
"change_label": null,
|
|
||||||
"merge_face": null,
|
|
||||||
"detach_face": null,
|
|
||||||
"move_faces": null
|
|
||||||
},
|
|
||||||
"modal": {
|
"modal": {
|
||||||
"action": {
|
"action": {
|
||||||
"merge": "合併"
|
"merge": "合併"
|
||||||
|
@ -215,30 +215,36 @@
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"album": {
|
"album": {
|
||||||
|
"album_cover": "",
|
||||||
|
"download": {
|
||||||
|
"high-resolutions": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"originals": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"thumbnails": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
|
"web-videos": {
|
||||||
|
"description": "",
|
||||||
|
"title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"reset_cover": "",
|
||||||
|
"set_cover": "",
|
||||||
"title_placeholder": "相簿名稱"
|
"title_placeholder": "相簿名稱"
|
||||||
},
|
},
|
||||||
"download": {
|
"download": {
|
||||||
"filesize": {
|
"filesize": {
|
||||||
"byte": null,
|
"byte": "",
|
||||||
"giga_byte": null,
|
"giga_byte": "",
|
||||||
"kilo_byte": null,
|
"kilo_byte": "",
|
||||||
"mega_byte": null,
|
"mega_byte": "",
|
||||||
"tera_byte": null,
|
"tera_byte": ""
|
||||||
"byte_0": null,
|
|
||||||
"byte_1": null,
|
|
||||||
"byte_2": null,
|
|
||||||
"giga_byte_0": null,
|
|
||||||
"giga_byte_1": null,
|
|
||||||
"giga_byte_2": null,
|
|
||||||
"kilo_byte_0": null,
|
|
||||||
"kilo_byte_1": null,
|
|
||||||
"kilo_byte_2": null,
|
|
||||||
"mega_byte_0": null,
|
|
||||||
"mega_byte_1": null,
|
|
||||||
"mega_byte_2": null,
|
|
||||||
"tera_byte_0": null,
|
|
||||||
"tera_byte_1": null,
|
|
||||||
"tera_byte_2": null
|
|
||||||
},
|
},
|
||||||
"table_columns": {
|
"table_columns": {
|
||||||
"dimensions": "尺寸",
|
"dimensions": "尺寸",
|
||||||
|
@ -248,7 +254,11 @@
|
||||||
},
|
},
|
||||||
"title": "下載"
|
"title": "下載"
|
||||||
},
|
},
|
||||||
|
"location": {
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"media": {
|
"media": {
|
||||||
|
"album_path": "",
|
||||||
"exif": {
|
"exif": {
|
||||||
"exposure_program": {
|
"exposure_program": {
|
||||||
"action_program": "動態模式",
|
"action_program": "動態模式",
|
||||||
|
@ -264,19 +274,20 @@
|
||||||
},
|
},
|
||||||
"flash": {
|
"flash": {
|
||||||
"auto": "自動",
|
"auto": "自動",
|
||||||
"did_not_fire": null,
|
"did_not_fire": "",
|
||||||
"fired": null,
|
"fired": "",
|
||||||
"no_flash": null,
|
"no_flash": "",
|
||||||
"no_flash_function": null,
|
"no_flash_function": "",
|
||||||
"off": "關閉",
|
"off": "關閉",
|
||||||
"on": "使用",
|
"on": "使用",
|
||||||
"red_eye_reduction": "減輕紅眼",
|
"red_eye_reduction": "減輕紅眼",
|
||||||
"return_detected": null,
|
"return_detected": "",
|
||||||
"return_not_detected": null
|
"return_not_detected": ""
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"aperture": "光圈",
|
"aperture": "光圈",
|
||||||
"camera": "相機",
|
"camera": "相機",
|
||||||
|
"coordinates": "",
|
||||||
"date_shot": "拍攝日期",
|
"date_shot": "拍攝日期",
|
||||||
"exposure": "曝光",
|
"exposure": "曝光",
|
||||||
"exposure_program": "模式",
|
"exposure_program": "模式",
|
||||||
|
@ -288,6 +299,15 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"people": {
|
||||||
|
"action_label": {
|
||||||
|
"detach_image": "",
|
||||||
|
"merge_face": "",
|
||||||
|
"move_face": ""
|
||||||
|
},
|
||||||
|
"confirm_image_detach": "",
|
||||||
|
"title": ""
|
||||||
|
},
|
||||||
"sharing": {
|
"sharing": {
|
||||||
"add_share": "新增分享",
|
"add_share": "新增分享",
|
||||||
"copy_link": "複製連結",
|
"copy_link": "複製連結",
|
||||||
|
@ -305,6 +325,13 @@
|
||||||
"places": "地點",
|
"places": "地點",
|
||||||
"settings": "設定"
|
"settings": "設定"
|
||||||
},
|
},
|
||||||
|
"timeline_filter": {
|
||||||
|
"date": {
|
||||||
|
"dropdown_all": "",
|
||||||
|
"dropdown_year": "",
|
||||||
|
"label": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"loading_album": "載入相簿",
|
"loading_album": "載入相簿",
|
||||||
"login": "登入",
|
"login": "登入",
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
{
|
||||||
|
"people_page": {
|
||||||
|
"action_label": {
|
||||||
|
"change_label": null,
|
||||||
|
"merge_people": null,
|
||||||
|
"detach_images": null,
|
||||||
|
"move_faces": null
|
||||||
|
},
|
||||||
|
"modal": {
|
||||||
|
"merge_people_groups": {
|
||||||
|
"description": "所有在此的臉孔群組將會合併到已選擇的臉孔群組",
|
||||||
|
"destination_table": {
|
||||||
|
"title": "選擇目標臉孔群組"
|
||||||
|
},
|
||||||
|
"title": "合併臉孔群組"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sidebar": {
|
||||||
|
"download": {
|
||||||
|
"filesize": {
|
||||||
|
"byte": null,
|
||||||
|
"giga_byte": null,
|
||||||
|
"kilo_byte": null,
|
||||||
|
"mega_byte": null,
|
||||||
|
"tera_byte": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"exif": {
|
||||||
|
"flash": {
|
||||||
|
"did_not_fire": null,
|
||||||
|
"fired": null,
|
||||||
|
"no_flash": null,
|
||||||
|
"no_flash_function": null,
|
||||||
|
"return_detected": null,
|
||||||
|
"return_not_detected": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import classNames, { Argument as ClassNamesArg } from 'classnames'
|
||||||
|
// import { overrideTailwindClasses } from 'tailwind-override'
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
export interface DebouncedFn<F extends (...args: any[]) => any> {
|
export interface DebouncedFn<F extends (...args: any[]) => any> {
|
||||||
|
@ -41,3 +43,8 @@ export function isNil(value: any): value is undefined | null {
|
||||||
export function exhaustiveCheck(value: never) {
|
export function exhaustiveCheck(value: never) {
|
||||||
throw new Error(`Exhaustive check failed with value: ${value}`)
|
throw new Error(`Exhaustive check failed with value: ${value}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function tailwindClassNames(...args: ClassNamesArg[]) {
|
||||||
|
// return overrideTailwindClasses(classNames(args))
|
||||||
|
return classNames(args)
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import classNames, { Argument as ClassNamesArg } from 'classnames'
|
||||||
import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg'
|
import { ReactComponent as ActionArrowIcon } from './icons/textboxActionArrow.svg'
|
||||||
import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg'
|
import { ReactComponent as LoadingSpinnerIcon } from './icons/textboxLoadingSpinner.svg'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
import { tailwindClassNames } from '../../helpers/utils'
|
||||||
|
|
||||||
type TextFieldProps = {
|
type TextFieldProps = {
|
||||||
label?: string
|
label?: string
|
||||||
|
@ -164,7 +165,10 @@ export const Submit = ({
|
||||||
...props
|
...props
|
||||||
}: SubmitProps & React.ButtonHTMLAttributes<HTMLInputElement>) => (
|
}: SubmitProps & React.ButtonHTMLAttributes<HTMLInputElement>) => (
|
||||||
<input
|
<input
|
||||||
className={classNames(buttonStyles({ variant, background }), className)}
|
className={tailwindClassNames(
|
||||||
|
buttonStyles({ variant, background }),
|
||||||
|
className
|
||||||
|
)}
|
||||||
type="submit"
|
type="submit"
|
||||||
value={children}
|
value={children}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -179,7 +183,10 @@ export const Button = ({
|
||||||
...props
|
...props
|
||||||
}: ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
}: ButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||||
<button
|
<button
|
||||||
className={classNames(buttonStyles({ variant, background }), className)}
|
className={tailwindClassNames(
|
||||||
|
buttonStyles({ variant, background }),
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
Loading…
Reference in New Issue