1
Fork 0

Add pagination for people

This commit is contained in:
viktorstrate 2021-02-25 20:39:24 +01:00
parent 7fab8287a2
commit 8290d51aae
No known key found for this signature in database
GPG Key ID: 3F855605109C1E8A
9 changed files with 248 additions and 43 deletions

View File

@ -175,6 +175,7 @@ type ComplexityRoot struct {
Query struct {
Album func(childComplexity int, id int) int
FaceGroup func(childComplexity int, id int) int
MapboxToken func(childComplexity int) int
Media func(childComplexity int, id int) int
MediaList func(childComplexity int, ids []int) int
@ -322,6 +323,7 @@ type QueryResolver interface {
ShareTokenValidatePassword(ctx context.Context, token string, password *string) (bool, error)
Search(ctx context.Context, query string, limitMedia *int, limitAlbums *int) (*models.SearchResult, error)
MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error)
FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error)
}
type ShareTokenResolver interface {
HasPassword(ctx context.Context, obj *models.ShareToken) (bool, error)
@ -1073,6 +1075,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Query.Album(childComplexity, args["id"].(int)), true
case "Query.faceGroup":
if e.complexity.Query.FaceGroup == nil {
break
}
args, err := ec.field_Query_faceGroup_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Query.FaceGroup(childComplexity, args["id"].(int)), true
case "Query.mapboxToken":
if e.complexity.Query.MapboxToken == nil {
break
@ -1624,6 +1638,7 @@ type Query {
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
myFaceGroups(paginate: Pagination): [FaceGroup!]!
faceGroup(id: ID!): FaceGroup!
}
type Mutation {
@ -2479,6 +2494,21 @@ func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs
return args, nil
}
func (ec *executionContext) field_Query_faceGroup_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["id"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("id"))
arg0, err = ec.unmarshalNID2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["id"] = arg0
return args, nil
}
func (ec *executionContext) field_Query_mediaList_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@ -6567,6 +6597,48 @@ func (ec *executionContext) _Query_myFaceGroups(ctx context.Context, field graph
return ec.marshalNFaceGroup2ᚕᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroupᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Query_faceGroup(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Query",
Field: field,
Args: nil,
IsMethod: true,
IsResolver: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Query_faceGroup_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Query().FaceGroup(rctx, args["id"].(int))
})
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.FaceGroup)
fc.Result = res
return ec.marshalNFaceGroup2ᚖgithubᚗcomᚋphotoviewᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐFaceGroup(ctx, field.Selections, res)
}
func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@ -10093,6 +10165,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
}
return res
})
case "faceGroup":
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._Query_faceGroup(ctx, field)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "__type":
out.Values[i] = ec._Query___type(ctx, field)
case "__schema":

View File

@ -63,6 +63,34 @@ func (r faceGroupResolver) ImageFaceCount(ctx context.Context, obj *models.FaceG
return int(count), nil
}
func (r *queryResolver) FaceGroup(ctx context.Context, id int) (*models.FaceGroup, error) {
user := auth.UserFromContext(ctx)
if user == nil {
return nil, errors.New("unauthorized")
}
if err := user.FillAlbums(r.Database); err != nil {
return nil, err
}
userAlbumIDs := make([]int, len(user.Albums))
for i, album := range user.Albums {
userAlbumIDs[i] = album.ID
}
faceGroupQuery := r.Database.
Joins("LEFT JOIN image_faces ON image_faces.id = face_groups.id").
Where("face_groups.id = ?", id).
Where("image_faces.media_id IN (?)", r.Database.Select("media_id").Table("media").Where("media.album_id IN (?)", userAlbumIDs))
var faceGroup models.FaceGroup
if err := faceGroupQuery.Find(&faceGroup).Error; err != nil {
return nil, err
}
return &faceGroup, nil
}
func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagination) ([]*models.FaceGroup, error) {
user := auth.UserFromContext(ctx)
if user == nil {
@ -80,7 +108,6 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
faceGroupQuery := r.Database.
Joins("LEFT JOIN image_faces ON image_faces.id = face_groups.id").
// Where("face_groups.id IN (?)", faceGroupIDs).
Where("image_faces.media_id IN (?)", r.Database.Select("media_id").Table("media").Where("media.album_id IN (?)", userAlbumIDs)).
Order("CASE WHEN label IS NULL THEN 1 ELSE 0 END")
@ -91,10 +118,6 @@ func (r *queryResolver) MyFaceGroups(ctx context.Context, paginate *models.Pagin
return nil, err
}
// for _, faceGroup := range faceGroups {
// faceGroup.ImageFaces = faceGroupMap[faceGroup.ID]
// }
return faceGroups, nil
}

View File

@ -61,6 +61,7 @@ type Query {
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
myFaceGroups(paginate: Pagination): [FaceGroup!]!
faceGroup(id: ID!): FaceGroup!
}
type Mutation {

View File

@ -6,7 +6,7 @@ import PropTypes from 'prop-types'
import Layout from '../../Layout'
import useURLParameters from '../../hooks/useURLParameters'
import useScrollPagination from '../../hooks/useScrollPagination'
import { Loader } from 'semantic-ui-react'
import PaginateLoader from '../../components/PaginateLoader'
const albumQuery = gql`
query albumQuery(
@ -140,13 +140,10 @@ function AlbumPage({ match }) {
setOrdering={setOrdering}
ordering={{ orderBy, orderDirection }}
/>
<Loader
style={{ margin: '42px 0 24px 0' }}
<PaginateLoader
active={!finishedLoadingMore && !loading}
inline="centered"
>
Loading more media
</Loader>
text="Loading more media"
/>
</Layout>
)
}

View File

@ -7,10 +7,12 @@ import { Link } from 'react-router-dom'
import SingleFaceGroup from './SingleFaceGroup/SingleFaceGroup'
import { Button, Icon, Input } from 'semantic-ui-react'
import FaceCircleImage from './FaceCircleImage'
import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../../components/PaginateLoader'
export const MY_FACES_QUERY = gql`
query myFaces {
myFaceGroups {
query myFaces($limit: Int!, $offset: Int!) {
myFaceGroups(paginate: { limit: $limit, offset: $offset }) {
id
label
imageFaceCount
@ -24,17 +26,11 @@ export const MY_FACES_QUERY = gql`
}
media {
id
type
title
thumbnail {
url
width
height
}
highRes {
url
}
favorite
}
}
}
@ -198,31 +194,33 @@ FaceGroup.propTypes = {
const FaceGroupsWrapper = styled.div`
display: flex;
flex-wrap: wrap;
margin-top: 24px;
`
const PeoplePage = ({ match }) => {
const { data, error } = useQuery(MY_FACES_QUERY)
const PeopleGallery = () => {
const { data, error, loading, fetchMore } = useQuery(MY_FACES_QUERY, {
variables: {
limit: 50,
offset: 0,
},
})
const [
recognizeUnlabeled,
{ loading: recognizeUnlabeledLoading },
] = useMutation(RECOGNIZE_UNLABELED_FACES_MUTATION)
const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
loading,
fetchMore,
data,
getItems: data => data.myFaceGroups,
})
if (error) {
return error.message
}
const faceGroup = match.params.person
if (faceGroup) {
return (
<Layout>
<SingleFaceGroup
faceGroup={data?.myFaceGroups?.find(x => x.id == faceGroup)}
/>
</Layout>
)
}
let faces = null
if (data) {
faces = data.myFaceGroups.map(faceGroup => (
@ -232,7 +230,6 @@ const PeoplePage = ({ match }) => {
return (
<Layout title={'People'}>
<FaceGroupsWrapper>{faces}</FaceGroupsWrapper>
<Button
loading={recognizeUnlabeledLoading}
disabled={recognizeUnlabeledLoading}
@ -243,10 +240,28 @@ const PeoplePage = ({ match }) => {
<Icon name="sync" />
Recognize unlabeled faces
</Button>
<FaceGroupsWrapper ref={containerElem}>{faces}</FaceGroupsWrapper>
<PaginateLoader
active={!finishedLoadingMore && !loading}
text="Loading more people"
/>
</Layout>
)
}
const PeoplePage = ({ match }) => {
const faceGroup = match.params.person
if (faceGroup) {
return (
<Layout>
<SingleFaceGroup faceGroupID={faceGroup} />
</Layout>
)
} else {
return <PeopleGallery />
}
}
PeoplePage.propTypes = {
match: PropTypes.object.isRequired,
}

View File

@ -1,12 +1,67 @@
import { gql, useQuery } from '@apollo/client'
import PropTypes from 'prop-types'
import React, { useState } from 'react'
import PaginateLoader from '../../../components/PaginateLoader'
import PhotoGallery from '../../../components/photoGallery/PhotoGallery'
import useScrollPagination from '../../../hooks/useScrollPagination'
import FaceGroupTitle from './FaceGroupTitle'
const SingleFaceGroup = ({ faceGroup }) => {
export const SINGLE_FACE_GROUP = gql`
query singleFaceGroup($id: ID!, $limit: Int!, $offset: Int!) {
faceGroup(id: $id) {
id
label
imageFaces(paginate: { limit: $limit, offset: $offset }) {
id
rectangle {
minX
maxX
minY
maxY
}
media {
id
type
title
thumbnail {
url
width
height
}
highRes {
url
}
favorite
}
}
}
}
`
const SingleFaceGroup = ({ faceGroupID }) => {
const { data, error, loading, fetchMore } = useQuery(SINGLE_FACE_GROUP, {
variables: {
limit: 2,
offset: 0,
id: faceGroupID,
},
})
const [presenting, setPresenting] = useState(false)
const [activeIndex, setActiveIndex] = useState(-1)
const { containerElem, finished: finishedLoadingMore } = useScrollPagination({
loading,
fetchMore,
data,
getItems: data => data.faceGroup.imageFaces,
})
const faceGroup = data?.faceGroup
if (error) {
return <div>{error.message}</div>
}
let mediaGallery = null
if (faceGroup) {
const media = faceGroup.imageFaces.map(x => x.media)
@ -28,12 +83,16 @@ const SingleFaceGroup = ({ faceGroup }) => {
nextImage={nextImage}
previousImage={previousImage}
/>
<PaginateLoader
active={!finishedLoadingMore && !loading}
text="Loading more photos"
/>
</div>
)
}
return (
<div>
<div ref={containerElem}>
<FaceGroupTitle faceGroup={faceGroup} />
{mediaGallery}
</div>
@ -41,7 +100,7 @@ const SingleFaceGroup = ({ faceGroup }) => {
}
SingleFaceGroup.propTypes = {
faceGroup: PropTypes.object,
faceGroupID: PropTypes.string,
}
export default SingleFaceGroup

View File

@ -142,9 +142,15 @@ const memoryCache = new InMemoryCache({
media: paginateCache(['onlyFavorites', 'order']),
},
},
FaceGroup: {
fields: {
imageFaces: paginateCache([]),
},
},
Query: {
fields: {
myTimeline: paginateCache(['onlyFavorites']),
myFaceGroups: paginateCache([]),
},
},
},

View File

@ -0,0 +1,20 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Loader } from 'semantic-ui-react'
const PaginateLoader = ({ active, text }) => (
<Loader
style={{ margin: '42px 0 24px 0', opacity: active ? '1' : '0' }}
inline="centered"
active={true}
>
{text}
</Loader>
)
PaginateLoader.propTypes = {
active: PropTypes.bool,
text: PropTypes.string,
}
export default PaginateLoader

View File

@ -8,6 +8,7 @@ import { Loader } from 'semantic-ui-react'
import useURLParameters from '../../hooks/useURLParameters'
import { FavoritesCheckbox } from '../AlbumFilter'
import useScrollPagination from '../../hooks/useScrollPagination'
import PaginateLoader from '../PaginateLoader'
const MY_TIMELINE_QUERY = gql`
query myTimeline($onlyFavorites: Boolean, $limit: Int, $offset: Int) {
@ -208,13 +209,10 @@ const TimelineGallery = () => {
setOnlyFavorites={setOnlyFavorites}
/>
<GalleryWrapper ref={containerElem}>{timelineGroups}</GalleryWrapper>
<Loader
style={{ margin: '42px 0 24px 0' }}
<PaginateLoader
active={!finishedLoadingMore && !loading}
inline="centered"
>
Loading more media
</Loader>
text="Loading more media"
/>
{presenting && (
<PresentView
media={