mirror of
https://git.vectorsigma.ru/public/photoview.git
synced 2026-08-03 20:49:02 +00:00
Show face circles in sidebar
This commit is contained in:
@@ -32,6 +32,8 @@ models:
|
||||
fields:
|
||||
exif:
|
||||
resolver: true
|
||||
faces:
|
||||
resolver: true
|
||||
MediaURL:
|
||||
model: github.com/photoview/photoview/api/graphql/models.MediaURL
|
||||
MediaEXIF:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:"-"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -264,6 +264,8 @@ type Media {
|
||||
|
||||
shares: [ShareToken!]!
|
||||
downloads: [MediaDownload!]!
|
||||
|
||||
faces: [ImageFace!]!
|
||||
}
|
||||
|
||||
"EXIF metadata from the camera"
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
61
ui/src/components/facesOverlay/FacesOverlay.js
Normal file
61
ui/src/components/facesOverlay/FacesOverlay.js
Normal 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,
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user