Add graphql query for getting media from an array of ids

- Add keyboard navigation when presenting images in map view
This commit is contained in:
viktorstrate
2020-09-27 20:52:09 +02:00
parent db64d3eb1b
commit b5e640c1bd
8 changed files with 215 additions and 74 deletions

View File

@@ -146,6 +146,7 @@ type ComplexityRoot struct {
Album func(childComplexity int, id int) int Album func(childComplexity int, id int) int
MapboxToken func(childComplexity int) int MapboxToken func(childComplexity int) int
Media func(childComplexity int, id 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 MyAlbums func(childComplexity int, filter *models.Filter, onlyRoot *bool, showEmpty *bool, onlyWithFavorites *bool) int
MyMedia func(childComplexity int, filter *models.Filter) int MyMedia func(childComplexity int, filter *models.Filter) int
MyMediaGeoJSON func(childComplexity int) int MyMediaGeoJSON func(childComplexity int) int
@@ -257,6 +258,7 @@ type QueryResolver interface {
Album(ctx context.Context, id int) (*models.Album, error) Album(ctx context.Context, id int) (*models.Album, error)
MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error) MyMedia(ctx context.Context, filter *models.Filter) ([]*models.Media, error)
Media(ctx context.Context, id int) (*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) MyMediaGeoJSON(ctx context.Context) (interface{}, error)
MapboxToken(ctx context.Context) (*string, error) MapboxToken(ctx context.Context) (*string, error)
ShareToken(ctx context.Context, token string, password *string) (*models.ShareToken, 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 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": case "Query.myAlbums":
if e.complexity.Query.MyAlbums == nil { if e.complexity.Query.MyAlbums == nil {
break break
@@ -1310,6 +1324,9 @@ type Query {
"Get media by id, user must own the media or be admin" "Get media by id, user must own the media or be admin"
media(id: Int!): Media! 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" "Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! myMediaGeoJson: Any!
"Get the mapbox api token, returns null if mapbox is not enabled" "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 args["filter"] = arg0
var arg1 *bool var arg1 *bool
if tmp, ok := rawArgs["onlyFavorites"]; ok { if tmp, ok := rawArgs["onlyFavorites"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyFavorites"))
arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) arg1, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2010,6 +2028,21 @@ func (ec *executionContext) field_Query_album_args(ctx context.Context, rawArgs
return args, nil 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) { func (ec *executionContext) field_Query_media_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error var err error
args := map[string]interface{}{} args := map[string]interface{}{}
@@ -2057,6 +2090,7 @@ func (ec *executionContext) field_Query_myAlbums_args(ctx context.Context, rawAr
args["showEmpty"] = arg2 args["showEmpty"] = arg2
var arg3 *bool var arg3 *bool
if tmp, ok := rawArgs["onlyWithFavorites"]; ok { if tmp, ok := rawArgs["onlyWithFavorites"]; ok {
ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("onlyWithFavorites"))
arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp) arg3, err = ec.unmarshalOBoolean2ᚖbool(ctx, tmp)
if err != nil { if err != nil {
return nil, err 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) 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) { func (ec *executionContext) _Query_myMediaGeoJson(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -8204,6 +8280,20 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
} }
return res 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": case "myMediaGeoJson":
field := field field := field
out.Concurrently(i, func() (res graphql.Marshaler) { out.Concurrently(i, func() (res graphql.Marshaler) {
@@ -8975,6 +9065,36 @@ func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.Selecti
return res 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 { 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) return ec._Media(ctx, sel, &v)
} }

View File

@@ -60,6 +60,44 @@ func (r *queryResolver) Media(ctx context.Context, id int) (*models.Media, error
return media, nil 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 { type mediaResolver struct {
*Resolver *Resolver
} }

View File

@@ -41,6 +41,9 @@ type Query {
"Get media by id, user must own the media or be admin" "Get media by id, user must own the media or be admin"
media(id: Int!): Media! 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" "Get media owned by the logged in user, returned in GeoJson format"
myMediaGeoJson: Any! myMediaGeoJson: Any!
"Get the mapbox api token, returns null if mapbox is not enabled" "Get the mapbox api token, returns null if mapbox is not enabled"

View File

@@ -5,8 +5,8 @@ import { useLazyQuery } from 'react-apollo'
import PresentView from '../../components/photoGallery/presentView/PresentView' import PresentView from '../../components/photoGallery/presentView/PresentView'
const QUERY_MEDIA = gql` const QUERY_MEDIA = gql`
query placePageQueryMedia($mediaID: Int!) { query placePageQueryMedia($mediaIDs: [Int!]!) {
media(id: $mediaID) { mediaList(ids: $mediaIDs) {
id id
title title
thumbnail { thumbnail {
@@ -53,74 +53,51 @@ const getMediaFromMarker = (map, presentMarker) =>
}) })
const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => { const MapPresentMarker = ({ map, presentMarker, setPresentMarker }) => {
const [media, setMedia] = useState(null) const [mediaMarkers, setMediaMarkers] = useState(null)
const [currentIndex, setCurrentIndex] = useState(0) const [currentIndex, setCurrentIndex] = useState(0)
const previousLoadedMedia = useRef(null) const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA)
const [loadMedia, { data: loadedMedia }] = useLazyQuery(QUERY_MEDIA, {
onCompleted(data) {
previousLoadedMedia.current = data
},
})
useEffect(() => { useEffect(() => {
if (presentMarker == null || map == null) { if (presentMarker == null || map == null) {
setMedia(null) setMediaMarkers(null)
return return
} }
getMediaFromMarker(map, presentMarker).then(setMedia) getMediaFromMarker(map, presentMarker).then(setMediaMarkers)
}, [presentMarker]) }, [presentMarker])
useEffect(() => { useEffect(() => {
if (!media) return if (!mediaMarkers) return
setCurrentIndex(0) setCurrentIndex(0)
loadMedia({ loadMedia({
variables: { variables: {
mediaID: media[0].media_id, mediaIDs: mediaMarkers.map(x => x.media_id),
}, },
}) })
}, [media]) }, [mediaMarkers])
useEffect(() => {
if (!media) return
console.log('Current index change', currentIndex, media)
loadMedia({
variables: {
mediaID: media[currentIndex].media_id,
},
})
}, [currentIndex])
if (presentMarker == null || map == null) { if (presentMarker == null || map == null) {
return null return null
} }
if (loadedMedia == null && previousLoadedMedia.current == null) { if (loadedMedia == null) {
return null return null
} }
const displayMedia = loadedMedia
? loadedMedia.media
: previousLoadedMedia.current.media
console.log('diaplay media', displayMedia)
return ( return (
<PresentView <PresentView
media={displayMedia} media={loadedMedia.mediaList[currentIndex]}
nextImage={() => { nextImage={() => {
setCurrentIndex(i => Math.min(media.length - 1, i + 1)) setCurrentIndex(i => Math.min(mediaMarkers.length - 1, i + 1))
}} }}
previousImage={() => { previousImage={() => {
setCurrentIndex(i => Math.max(0, i - 1)) setCurrentIndex(i => Math.max(0, i - 1))
}} }}
setPresenting={presenting => { setPresenting={presenting => {
if (!presenting) { if (!presenting) {
previousLoadedMedia.current = null setCurrentIndex(0)
setPresentMarker(null) setPresentMarker(null)
} }
}} }}

View File

@@ -61,12 +61,10 @@ const MapPage = () => {
}) })
map.current.on('load', () => { map.current.on('load', () => {
console.log(mapboxData.myMediaGeoJson)
map.current.addSource('media', { map.current.addSource('media', {
type: 'geojson', type: 'geojson',
data: mapboxData.myMediaGeoJson, data: mapboxData.myMediaGeoJson,
cluster: true, cluster: true,
// clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50, clusterRadius: 50,
clusterProperties: { clusterProperties: {
thumbnail: ['coalesce', ['get', 'thumbnail'], false], thumbnail: ['coalesce', ['get', 'thumbnail'], false],

View File

@@ -18,7 +18,9 @@ export const makeUpdateMarkers = ({
for (let i = 0; i < features.length; i++) { for (let i = 0; i < features.length; i++) {
const coords = features[i].geometry.coordinates const coords = features[i].geometry.coordinates
const props = features[i].properties 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] let marker = markers[id]
if (!marker) { if (!marker) {

View File

@@ -44,32 +44,6 @@ const PhotoGallery = ({
}) => { }) => {
const { updateSidebar } = useContext(SidebarContext) 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 activeImage = media && activeIndex != -1 && media[activeIndex]
const getPhotoElements = updateSidebar => { const getPhotoElements = updateSidebar => {

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react'
import styled, { createGlobalStyle } from 'styled-components' import styled, { createGlobalStyle } from 'styled-components'
import PresentNavigationOverlay from './PresentNavigationOverlay' import PresentNavigationOverlay from './PresentNavigationOverlay'
import PresentMedia from './PresentMedia' import PresentMedia from './PresentMedia'
@@ -28,14 +28,43 @@ const PresentView = ({
nextImage, nextImage,
previousImage, previousImage,
setPresenting, 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 (
<StyledContainer {...className}> <StyledContainer {...className}>
<PreventScroll /> <PreventScroll />
<PresentNavigationOverlay {...{ nextImage, previousImage, setPresenting }}> <PresentNavigationOverlay
{...{ nextImage, previousImage, setPresenting }}
>
<PresentMedia media={media} imageLoaded={imageLoaded} /> <PresentMedia media={media} imageLoaded={imageLoaded} />
</PresentNavigationOverlay> </PresentNavigationOverlay>
</StyledContainer> </StyledContainer>
) )
}
PresentView.propTypes = { PresentView.propTypes = {
media: PropTypes.object.isRequired, media: PropTypes.object.isRequired,