Show face circles in sidebar

This commit is contained in:
viktorstrate
2021-02-16 17:13:08 +01:00
parent c0d289eb41
commit de46f89f5d
10 changed files with 222 additions and 4 deletions

View File

@@ -32,6 +32,8 @@ models:
fields:
exif:
resolver: true
faces:
resolver: true
MediaURL:
model: github.com/photoview/photoview/api/graphql/models.MediaURL
MediaEXIF:

View File

@@ -94,6 +94,7 @@ type ComplexityRoot struct {
Album func(childComplexity int) int
Downloads func(childComplexity int) int
Exif func(childComplexity int) int
Faces func(childComplexity int) int
Favorite func(childComplexity int) int
HighRes func(childComplexity int) int
ID func(childComplexity int) int
@@ -264,6 +265,7 @@ type MediaResolver interface {
Shares(ctx context.Context, obj *models.Media) ([]*models.ShareToken, error)
Downloads(ctx context.Context, obj *models.Media) ([]*models.MediaDownload, error)
Faces(ctx context.Context, obj *models.Media) ([]*models.ImageFace, error)
}
type MutationResolver interface {
AuthorizeUser(ctx context.Context, username string, password string) (*models.AuthorizeResult, error)
@@ -518,6 +520,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Media.Exif(childComplexity), true
case "Media.faces":
if e.complexity.Media.Faces == nil {
break
}
return e.complexity.Media.Faces(childComplexity), true
case "Media.favorite":
if e.complexity.Media.Favorite == nil {
break
@@ -1723,6 +1732,8 @@ type Media {
shares: [ShareToken!]!
downloads: [MediaDownload!]!
faces: [ImageFace!]!
}
"EXIF metadata from the camera"
@@ -3786,6 +3797,41 @@ func (ec *executionContext) _Media_downloads(ctx context.Context, field graphql.
return ec.marshalNMediaDownload2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaDownloadᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Media_faces(ctx context.Context, field graphql.CollectedField, obj *models.Media) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Media",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
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 ec.resolvers.Media().Faces(rctx, obj)
})
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.([]*models.ImageFace)
fc.Result = res
return ec.marshalNImageFace2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFaceᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _MediaDownload_title(ctx context.Context, field graphql.CollectedField, obj *models.MediaDownload) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -8993,6 +9039,20 @@ func (ec *executionContext) _Media(ctx context.Context, sel ast.SelectionSet, ob
}
return res
})
case "faces":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
}
}()
res = ec._Media_faces(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -10341,6 +10401,53 @@ func (ec *executionContext) marshalNImageFace2ᚕgithubᚗcomᚋphotoviewᚋphot
return ret
}
func (ec *executionContext) marshalNImageFace2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFaceᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.ImageFace) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
fc := &graphql.FieldContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithFieldContext(ctx, fc)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNImageFace2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFace(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNImageFace2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐImageFace(ctx context.Context, sel ast.SelectionSet, v *models.ImageFace) graphql.Marshaler {
if v == nil {
if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._ImageFace(ctx, sel, v)
}
func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
res, err := graphql.UnmarshalInt(v)
return res, graphql.ErrorOnPath(ctx, err)

View File

@@ -27,7 +27,8 @@ type Media struct {
VideoMetadataID *int `gorm:"index"`
VideoMetadata *VideoMetadata `gorm:"constraint:OnDelete:CASCADE;"`
SideCarPath *string
SideCarHash *string `gorm:"unique"`
SideCarHash *string `gorm:"unique"`
Faces []*ImageFace `gorm:"constraint:OnDelete:CASCADE;"`
// Only used internally
CounterpartPath *string `gorm:"-"`

View File

@@ -23,8 +23,12 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
userAlbumIDs[i] = album.ID
}
imageFaceQuery := r.Database.
Joins("Media").
Where("media.album_id IN (?)", userAlbumIDs)
var imageFaces []*models.ImageFace
if err := r.Database.Joins("Media").Where("media.album_id IN (?)", userAlbumIDs).Find(&imageFaces).Error; err != nil {
if err := imageFaceQuery.Find(&imageFaces).Error; err != nil {
return nil, err
}
@@ -47,8 +51,12 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
i++
}
faceGroupQuery := r.Database.
Where("id IN (?)", faceGroupIDs).
Order("CASE WHEN label IS NULL THEN 1 ELSE 0 END")
var faceGroups []*models.FaceGroup
if err := r.Database.Where("id IN (?)", faceGroupIDs).Find(&faceGroups).Error; err != nil {
if err := faceGroupQuery.Find(&faceGroups).Error; err != nil {
return nil, err
}

View File

@@ -208,3 +208,16 @@ func (r *mutationResolver) FavoriteMedia(ctx context.Context, mediaID int, favor
return &media, nil
}
func (r *mediaResolver) Faces(ctx context.Context, media *models.Media) ([]*models.ImageFace, error) {
if media.Faces != nil {
return media.Faces, nil
}
var faces []*models.ImageFace
if err := r.Database.Model(&media).Association("Faces").Find(&faces); err != nil {
return nil, err
}
return faces, nil
}

View File

@@ -264,6 +264,8 @@ type Media {
shares: [ShareToken!]!
downloads: [MediaDownload!]!
faces: [ImageFace!]!
}
"EXIF metadata from the camera"

View File

@@ -48,11 +48,18 @@ const FaceImage = styled(ProtectedImage)`
object-fit: cover;
`
const FaceLabel = styled.div`
color: ${({ labeled }) => (labeled ? 'black' : '#aaa')};
margin: 12px 12px 24px;
text-align: center;
`
const FaceGroup = ({ group }) => (
<Link to={`/people/${group.id}`}>
<CircleImageWrapper>
<FaceImage src={group.imageFaces[0].media.thumbnail.url} />
</CircleImageWrapper>
<FaceLabel labeled={!!group.label}>{group.label ?? 'Unlabeled'}</FaceLabel>
</Link>
)

View File

@@ -31,7 +31,12 @@ const SingleFaceGroup = ({ faceGroup }) => {
<div>
Face group: {faceGroup.id}
<div>
<PhotoGallery media={media} loading={false} setPresenting={() => {}} />
<PhotoGallery
media={media}
loading={false}
setPresenting={() => {}}
onSelectImage={() => {}}
/>
</div>
</div>
)

View File

@@ -0,0 +1,61 @@
import PropTypes from 'prop-types'
import React from 'react'
import styled from 'styled-components'
const FaceBoxStyle = styled.div`
box-shadow: inset 0 0 2px 1px rgba(0, 0, 0, 0.3), 0 0 0 1px rgb(255, 255, 255);
border-radius: 50%;
position: absolute;
top: ${({ minY }) => minY * 100}%;
bottom: ${({ maxY }) => (1 - maxY) * 100}%;
left: ${({ minX }) => minX * 100}%;
right: ${({ maxX }) => (1 - maxX) * 100}%;
`
const FaceBox = ({ face /*media*/ }) => {
return <FaceBoxStyle {...face.rectangle}></FaceBoxStyle>
}
FaceBox.propTypes = {
face: PropTypes.object.isRequired,
media: PropTypes.object.isRequired,
}
const SidebarFacesOverlayWrapper = styled.div`
position: absolute;
width: ${({ width }) => width * 100}%;
left: ${({ width }) => (100 - width * 100) / 2}%;
height: 100%;
top: 0;
opacity: 0;
user-select: none;
transition: opacity ease 200ms;
&:hover {
opacity: 1;
}
`
export const SidebarFacesOverlay = ({ media }) => {
if (media.type != 'photo') return null
const faceBoxes = media.faces?.map(face => (
<FaceBox key={face.id} face={face} media={media} />
))
let wrapperWidth = 1
if (media.thumbnail.width * 0.75 < media.thumbnail.height) {
wrapperWidth = (media.thumbnail.width * 0.75) / media.thumbnail.height
}
return (
<SidebarFacesOverlayWrapper width={wrapperWidth}>
{faceBoxes}
</SidebarFacesOverlayWrapper>
)
}
SidebarFacesOverlay.propTypes = {
media: PropTypes.object.isRequired,
}

View File

@@ -7,6 +7,7 @@ import { ProtectedImage, ProtectedVideo } from '../photoGallery/ProtectedMedia'
import SidebarShare from './Sharing'
import SidebarDownload from './SidebarDownload'
import SidebarItem from './SidebarItem'
import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
const mediaQuery = gql`
query sidebarPhoto($id: ID!) {
@@ -41,6 +42,7 @@ const mediaQuery = gql`
audio
}
exif {
id
camera
maker
lens
@@ -52,6 +54,15 @@ const mediaQuery = gql`
flash
exposureProgram
}
faces {
id
rectangle {
minX
maxX
minY
maxY
}
}
}
}
`
@@ -203,6 +214,7 @@ const SidebarContent = ({ media, hidePreview }) => {
{!hidePreview && (
<PreviewImageWrapper imageAspect={imageAspect}>
<PreviewMedia previewImage={previewImage} media={media} />
<SidebarFacesOverlay media={media} />
</PreviewImageWrapper>
)}
<Name>{media && media.title}</Name>