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
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)
}

View File

@@ -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
}

View File

@@ -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"

View File

@@ -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 (
<PresentView
media={displayMedia}
media={loadedMedia.mediaList[currentIndex]}
nextImage={() => {
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)
}
}}

View File

@@ -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],

View File

@@ -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) {

View File

@@ -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 => {

View File

@@ -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,
}) => (
<StyledContainer {...className}>
<PreventScroll />
<PresentNavigationOverlay {...{ nextImage, previousImage, setPresenting }}>
<PresentMedia media={media} imageLoaded={imageLoaded} />
</PresentNavigationOverlay>
</StyledContainer>
)
}) => {
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}>
<PreventScroll />
<PresentNavigationOverlay
{...{ nextImage, previousImage, setPresenting }}
>
<PresentMedia media={media} imageLoaded={imageLoaded} />
</PresentNavigationOverlay>
</StyledContainer>
)
}
PresentView.propTypes = {
media: PropTypes.object.isRequired,