diff --git a/api/graphql/generated.go b/api/graphql/generated.go index 8c325750..8d130f6c 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -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": diff --git a/api/graphql/resolvers/faces.go b/api/graphql/resolvers/faces.go index 6f863b6e..a712c023 100644 --- a/api/graphql/resolvers/faces.go +++ b/api/graphql/resolvers/faces.go @@ -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 } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 7a3d3816..6a845225 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -61,6 +61,7 @@ type Query { search(query: String!, limitMedia: Int, limitAlbums: Int): SearchResult! myFaceGroups(paginate: Pagination): [FaceGroup!]! + faceGroup(id: ID!): FaceGroup! } type Mutation { diff --git a/ui/src/Pages/AlbumPage/AlbumPage.js b/ui/src/Pages/AlbumPage/AlbumPage.js index 81a0279f..70f1d6ee 100644 --- a/ui/src/Pages/AlbumPage/AlbumPage.js +++ b/ui/src/Pages/AlbumPage/AlbumPage.js @@ -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 }} /> - - Loading more media - + text="Loading more media" + /> ) } diff --git a/ui/src/Pages/PeoplePage/PeoplePage.js b/ui/src/Pages/PeoplePage/PeoplePage.js index ef2ffbe8..c6bad273 100644 --- a/ui/src/Pages/PeoplePage/PeoplePage.js +++ b/ui/src/Pages/PeoplePage/PeoplePage.js @@ -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 ( - - x.id == faceGroup)} - /> - - ) - } - let faces = null if (data) { faces = data.myFaceGroups.map(faceGroup => ( @@ -232,7 +230,6 @@ const PeoplePage = ({ match }) => { return ( - {faces} + {faces} + ) } +const PeoplePage = ({ match }) => { + const faceGroup = match.params.person + if (faceGroup) { + return ( + + + + ) + } else { + return + } +} + PeoplePage.propTypes = { match: PropTypes.object.isRequired, } diff --git a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js index c2846e2d..b51823fb 100644 --- a/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js +++ b/ui/src/Pages/PeoplePage/SingleFaceGroup/SingleFaceGroup.js @@ -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
{error.message}
+ } + let mediaGallery = null if (faceGroup) { const media = faceGroup.imageFaces.map(x => x.media) @@ -28,12 +83,16 @@ const SingleFaceGroup = ({ faceGroup }) => { nextImage={nextImage} previousImage={previousImage} /> + ) } return ( -
+
{mediaGallery}
@@ -41,7 +100,7 @@ const SingleFaceGroup = ({ faceGroup }) => { } SingleFaceGroup.propTypes = { - faceGroup: PropTypes.object, + faceGroupID: PropTypes.string, } export default SingleFaceGroup diff --git a/ui/src/apolloClient.js b/ui/src/apolloClient.js index 53ab78c0..bc0a6598 100644 --- a/ui/src/apolloClient.js +++ b/ui/src/apolloClient.js @@ -142,9 +142,15 @@ const memoryCache = new InMemoryCache({ media: paginateCache(['onlyFavorites', 'order']), }, }, + FaceGroup: { + fields: { + imageFaces: paginateCache([]), + }, + }, Query: { fields: { myTimeline: paginateCache(['onlyFavorites']), + myFaceGroups: paginateCache([]), }, }, }, diff --git a/ui/src/components/PaginateLoader.js b/ui/src/components/PaginateLoader.js new file mode 100644 index 00000000..19e66152 --- /dev/null +++ b/ui/src/components/PaginateLoader.js @@ -0,0 +1,20 @@ +import React from 'react' +import PropTypes from 'prop-types' +import { Loader } from 'semantic-ui-react' + +const PaginateLoader = ({ active, text }) => ( + + {text} + +) + +PaginateLoader.propTypes = { + active: PropTypes.bool, + text: PropTypes.string, +} + +export default PaginateLoader diff --git a/ui/src/components/timelineGallery/TimelineGallery.js b/ui/src/components/timelineGallery/TimelineGallery.js index 8f66f022..38d8299d 100644 --- a/ui/src/components/timelineGallery/TimelineGallery.js +++ b/ui/src/components/timelineGallery/TimelineGallery.js @@ -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} /> {timelineGroups} - - Loading more media - + text="Loading more media" + /> {presenting && (