diff --git a/api/graphql/generated.go b/api/graphql/generated.go index e0d50de0..1b3171e4 100644 --- a/api/graphql/generated.go +++ b/api/graphql/generated.go @@ -146,6 +146,7 @@ type ComplexityRoot struct { Album 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 MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int MyMedia func(childComplexity int, filter *models.Filter) int MyMediaGeoJSON func(childComplexity int) int @@ -257,6 +258,7 @@ type QueryResolver interface { Album(ctx context.Context, id int) (*models.Album, error) MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error) Media(ctx context.Context, id int) (*models.Media, error) + MediaList(ctx context.Context, ids []int) ([]*models.Media, error) MyMediaGeoJSON(ctx context.Context) (interface{}, error) MapboxToken(ctx context.Context) (*string, error) ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, error) @@ -869,6 +871,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.Media(childComplexity, args["id"].(int)), true + case "Query.mediaList": + if e.complexity.Query.MediaList == nil { + break + } + + args, err := ec.field_Query_mediaList_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.MediaList(childComplexity, args["ids"].([]int)), true + case "Query.myAlbums": if e.complexity.Query.MyAlbums == nil { break @@ -1310,6 +1324,9 @@ type Query { "Get media by id, user must own the media or be admin" media(id: Int!): Media! + "Get a list of media by their ids, user must own the media or be admin" + mediaList(ids: [Int!]!): [Media!]! + "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! "Get the mapbox api token, returns null if mapbox is not enabled" @@ -1584,6 +1601,7 @@ func (ec *executionContext) field_Album_media_args(ctx context.Context, rawArgs args["filter"] = arg0 var arg1 *bool if tmp, ok := rawArgs["onlyFavorites"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyFavorites")) arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) if err != nil { return nil, err @@ -2010,6 +2028,21 @@ func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs 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{}{} + var arg0 []int + if tmp, ok := rawArgs["ids"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("ids")) + arg0, err = ec.unmarshalNInt2ᚕintᚄ(ctx, tmp) + if err != nil { + return nil, err + } + } + args["ids"] = arg0 + return args, nil +} + func (ec *executionContext) field_Query_media_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} @@ -2057,6 +2090,7 @@ func (ec *executionContext) field_Query_myAlbums_args(ctx context.Context, rawAr args["showEmpty"] = arg2 var arg3 *bool if tmp, ok := rawArgs["onlyWithFavorites"]; ok { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyWithFavorites")) arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) if err != nil { return nil, err @@ -4965,6 +4999,48 @@ func (ec *executionContext) _Query_media(ctx context.Context, field graphql.Coll return ec.marshalNMedia2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx, field.Selections, res) } +func (ec *executionContext) _Query_mediaList(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_mediaList_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().MediaList(rctx, args["ids"].([]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.Media) + fc.Result = res + return ec.marshalNMedia2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMediaᚄ(ctx, field.Selections, res) +} + func (ec *executionContext) _Query_myMediaGeoJson(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -8204,6 +8280,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } return res }) + case "mediaList": + 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_mediaList(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&invalids, 1) + } + return res + }) case "myMediaGeoJson": field := field out.Concurrently(i, func() (res graphql.Marshaler) { @@ -8975,6 +9065,36 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti return res } +func (ec *executionContext) unmarshalNInt2ᚕintᚄ(ctx context.Context, v interface{}) ([]int, error) { + var vSlice []interface{} + if v != nil { + if tmp1, ok := v.([]interface{}); ok { + vSlice = tmp1 + } else { + vSlice = []interface{}{v} + } + } + var err error + res := make([]int, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalNInt2int(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalNInt2ᚕintᚄ(ctx context.Context, sel ast.SelectionSet, v []int) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalNInt2int(ctx, sel, v[i]) + } + + return ret +} + func (ec *executionContext) marshalNMedia2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐMedia(ctx context.Context, sel ast.SelectionSet, v models.Media) graphql.Marshaler { return ec._Media(ctx, sel, &v) } diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go index 7d7f5331..07890cc6 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -60,6 +60,44 @@ func (r *queryResolver) Media(ctx context.Context, id int) (*models.Media, error return media, nil } +func (r *queryResolver) MediaList(ctx context.Context, ids []int) ([]*models.Media, error) { + user := auth.UserFromContext(ctx) + if user == nil { + return nil, auth.ErrUnauthorized + } + + if len(ids) == 0 { + return nil, errors.New("no ids provided") + } + + mediaIDQuestions := strings.Repeat("?,", len(ids))[:len(ids)*2-1] + + queryArgs := make([]interface{}, 0) + for _, id := range ids { + queryArgs = append(queryArgs, id) + } + queryArgs = append(queryArgs, user.UserID) + + rows, err := r.Database.Query(` + SELECT media.* FROM media + JOIN album ON media.album_id = album.album_id + WHERE media.media_id IN (`+mediaIDQuestions+`) AND album.owner_id = ? + AND media.media_id IN ( + SELECT media_id FROM media_url WHERE media_url.media_id = media.media_id + ) + `, queryArgs...) + if err != nil { + return nil, errors.Wrap(err, "could not get media list by media_id and user_id from database") + } + + media, err := models.NewMediaFromRows(rows) + if err != nil { + return nil, errors.Wrap(err, "could not convert database rows to media") + } + + return media, nil +} + type mediaResolver struct { *Resolver } diff --git a/api/graphql/schema.graphql b/api/graphql/schema.graphql index 3f0002ce..05544ba7 100644 --- a/api/graphql/schema.graphql +++ b/api/graphql/schema.graphql @@ -41,6 +41,9 @@ type Query { "Get media by id, user must own the media or be admin" media(id: Int!): Media! + "Get a list of media by their ids, user must own the media or be admin" + mediaList(ids: [Int!]!): [Media!]! + "Get media owned by the logged in user, returned in GeoJson format" myMediaGeoJson: Any! "Get the mapbox api token, returns null if mapbox is not enabled" diff --git a/ui/src/Pages/PlacesPage/MapPresentMarker.js b/ui/src/Pages/PlacesPage/MapPresentMarker.js index ef2c992c..0f3e9b49 100644 --- a/ui/src/Pages/PlacesPage/MapPresentMarker.js +++ b/ui/src/Pages/PlacesPage/MapPresentMarker.js @@ -5,8 +5,8 @@ import { useLazyQuery } from 'react-apollo' import PresentView from '../../components/photoGallery/presentView/PresentView' const QUERY_MEDIA = gql` - query placePageQueryMedia($mediaID: Int!) { - media(id: $mediaID) { + query placePageQueryMedia($mediaIDs: [Int!]!) { + mediaList(ids: $mediaIDs) { id title thumbnail { @@ -53,74 +53,51 @@ const getMediaFromMarker = (map, presentMarker) => }) const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => { - const [media, setMedia] = useState(null) + const [mediaMarkers, setMediaMarkers] = useState(null) const [currentIndex, setCurrentIndex] = useState(0) - const previousLoadedMedia = useRef(null) - const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA, { - onCompleted(data) { - previousLoadedMedia.current = data - }, - }) + const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA) useEffect(() => { if (presentMarker == null || map == null) { - setMedia(null) + setMediaMarkers(null) return } - getMediaFromMarker(map, presentMarker).then(setMedia) + getMediaFromMarker(map, presentMarker).then(setMediaMarkers) }, [presentMarker]) useEffect(() => { - if (!media) return + if (!mediaMarkers) return setCurrentIndex(0) loadMedia({ variables: { - mediaID: media[0].media_id, + mediaIDs: mediaMarkers.map(x => x.media_id), }, }) - }, [media]) - - useEffect(() => { - if (!media) return - - console.log('Current index change', currentIndex, media) - - loadMedia({ - variables: { - mediaID: media[currentIndex].media_id, - }, - }) - }, [currentIndex]) + }, [mediaMarkers]) if (presentMarker == null || map == null) { return null } - if (loadedMedia == null && previousLoadedMedia.current == null) { + if (loadedMedia == null) { return null } - const displayMedia = loadedMedia - ? loadedMedia.media - : previousLoadedMedia.current.media - - console.log('diaplay media', displayMedia) - return ( { - setCurrentIndex(i => Math.min(media.length - 1, i + 1)) + setCurrentIndex(i => Math.min(mediaMarkers.length - 1, i + 1)) }} previousImage={() => { setCurrentIndex(i => Math.max(0, i - 1)) }} setPresenting={presenting => { if (!presenting) { - previousLoadedMedia.current = null + setCurrentIndex(0) setPresentMarker(null) } }} diff --git a/ui/src/Pages/PlacesPage/PlacesPage.js b/ui/src/Pages/PlacesPage/PlacesPage.js index 05808fa4..d31e555d 100644 --- a/ui/src/Pages/PlacesPage/PlacesPage.js +++ b/ui/src/Pages/PlacesPage/PlacesPage.js @@ -61,12 +61,10 @@ const MapPage = () => { }) map.current.on('load', () => { - console.log(mapboxData.myMediaGeoJson) map.current.addSource('media', { type: 'geojson', data: mapboxData.myMediaGeoJson, cluster: true, - // clusterMaxZoom: 14, // Max zoom to cluster points on clusterRadius: 50, clusterProperties: { thumbnail: ['coalesce', ['get', 'thumbnail'], false], diff --git a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js index 62c8964b..32e412c3 100644 --- a/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js +++ b/ui/src/Pages/PlacesPage/mapboxHelperFunctions.js @@ -18,7 +18,9 @@ export const makeUpdateMarkers = ({ for (let i = 0; i < features.length; i++) { const coords = features[i].geometry.coordinates const props = features[i].properties - const id = props.cluster ? props.cluster_id : props.media_id + const id = props.cluster + ? `cluster_${props.cluster_id}` + : `media_${props.media_id}` let marker = markers[id] if (!marker) { diff --git a/ui/src/components/photoGallery/PhotoGallery.js b/ui/src/components/photoGallery/PhotoGallery.js index f490c3a9..b064c354 100644 --- a/ui/src/components/photoGallery/PhotoGallery.js +++ b/ui/src/components/photoGallery/PhotoGallery.js @@ -44,32 +44,6 @@ const PhotoGallery = ({ }) => { const { updateSidebar } = useContext(SidebarContext) - useEffect(() => { - const keyDownEvent = e => { - if (!onSelectImage || activeIndex == -1) { - return - } - - if (e.key == 'ArrowRight') { - nextImage && nextImage() - } - - if (e.key == 'ArrowLeft') { - nextImage && previousImage() - } - - if (e.key == 'Escape' && presenting) { - setPresenting(false) - } - } - - document.addEventListener('keydown', keyDownEvent) - - return function cleanup() { - document.removeEventListener('keydown', keyDownEvent) - } - }) - const activeImage = media && activeIndex != -1 && media[activeIndex] const getPhotoElements = updateSidebar => { diff --git a/ui/src/components/photoGallery/presentView/PresentView.js b/ui/src/components/photoGallery/presentView/PresentView.js index dacb8c40..1650188c 100644 --- a/ui/src/components/photoGallery/presentView/PresentView.js +++ b/ui/src/components/photoGallery/presentView/PresentView.js @@ -1,5 +1,5 @@ +import React, { useEffect } from 'react' import PropTypes from 'prop-types' -import React from 'react' import styled, { createGlobalStyle } from 'styled-components' import PresentNavigationOverlay from './PresentNavigationOverlay' import PresentMedia from './PresentMedia' @@ -28,14 +28,43 @@ const PresentView = ({ nextImage, previousImage, setPresenting, -}) => ( - - - - - - -) +}) => { + useEffect(() => { + const keyDownEvent = e => { + if (e.key == 'ArrowRight') { + nextImage && nextImage() + e.stopPropagation() + } + + if (e.key == 'ArrowLeft') { + nextImage && previousImage() + e.stopPropagation() + } + + if (e.key == 'Escape') { + setPresenting(false) + e.stopPropagation() + } + } + + document.addEventListener('keydown', keyDownEvent) + + return function cleanup() { + document.removeEventListener('keydown', keyDownEvent) + } + }) + + return ( + + + + + + + ) +} PresentView.propTypes = { media: PropTypes.object.isRequired,