Add pagination for people
This commit is contained in:
parent
7fab8287a2
commit
8290d51aae
|
@ -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":
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ type Query {
|
|||
search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult!
|
||||
|
||||
myFaceGroups(paginate: Pagination): [FaceGroup!]!
|
||||
faceGroup(id: ID!): FaceGroup!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -142,9 +142,15 @@ const memoryCache = new InMemoryCache({
|
|||
media: paginateCache(['onlyFavorites', 'order']),
|
||||
},
|
||||
},
|
||||
FaceGroup: {
|
||||
fields: {
|
||||
imageFaces: paginateCache([]),
|
||||
},
|
||||
},
|
||||
Query: {
|
||||
fields: {
|
||||
myTimeline: paginateCache(['onlyFavorites']),
|
||||
myFaceGroups: paginateCache([]),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
|
@ -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={
|
||||
|
|
Loading…
Reference in New Issue