1
Fork 0

Improve integration with ui

This commit is contained in:
viktorstrate 2020-02-10 12:05:58 +01:00
parent a3a4dda286
commit 6e72caf2f0
13 changed files with 356 additions and 91 deletions

View File

@ -78,15 +78,21 @@ type ComplexityRoot struct {
Photo struct {
Album func(childComplexity int) int
Downloads func(childComplexity int) int
Exif func(childComplexity int) int
HighRes func(childComplexity int) int
ID func(childComplexity int) int
Original func(childComplexity int) int
Path func(childComplexity int) int
Shares func(childComplexity int) int
Thumbnail func(childComplexity int) int
Title func(childComplexity int) int
}
PhotoDownload struct {
Title func(childComplexity int) int
URL func(childComplexity int) int
}
PhotoExif struct {
Aperture func(childComplexity int) int
Camera func(childComplexity int) int
@ -165,11 +171,12 @@ type MutationResolver interface {
SharePhoto(ctx context.Context, photoID int, expire *time.Time, password *string) (*models.ShareToken, error)
}
type PhotoResolver interface {
Original(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error)
Thumbnail(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error)
HighRes(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error)
Album(ctx context.Context, obj *models.Photo) (*models.Album, error)
Exif(ctx context.Context, obj *models.Photo) (*models.PhotoExif, error)
Shares(ctx context.Context, obj *models.Photo) ([]*models.ShareToken, error)
Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error)
}
type QueryResolver interface {
SiteInfo(ctx context.Context) (*models.SiteInfo, error)
@ -377,6 +384,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Photo.Album(childComplexity), true
case "Photo.downloads":
if e.complexity.Photo.Downloads == nil {
break
}
return e.complexity.Photo.Downloads(childComplexity), true
case "Photo.exif":
if e.complexity.Photo.Exif == nil {
break
@ -398,13 +412,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Photo.ID(childComplexity), true
case "Photo.original":
if e.complexity.Photo.Original == nil {
break
}
return e.complexity.Photo.Original(childComplexity), true
case "Photo.path":
if e.complexity.Photo.Path == nil {
break
@ -412,6 +419,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Photo.Path(childComplexity), true
case "Photo.shares":
if e.complexity.Photo.Shares == nil {
break
}
return e.complexity.Photo.Shares(childComplexity), true
case "Photo.thumbnail":
if e.complexity.Photo.Thumbnail == nil {
break
@ -426,6 +440,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Photo.Title(childComplexity), true
case "PhotoDownload.title":
if e.complexity.PhotoDownload.Title == nil {
break
}
return e.complexity.PhotoDownload.Title(childComplexity), true
case "PhotoDownload.url":
if e.complexity.PhotoDownload.URL == nil {
break
}
return e.complexity.PhotoDownload.URL(childComplexity), true
case "PhotoEXIF.aperture":
if e.complexity.PhotoExif.Aperture == nil {
break
@ -926,22 +954,26 @@ type PhotoURL {
height: Int!
}
type PhotoDownload {
title: String!
url: String!
}
type Photo {
id: Int!
title: String!
"Local filepath for the photo"
path: String!
"URL to display the photo in full resolution"
original: PhotoURL!
"URL to display the photo in a smaller resolution"
thumbnail: PhotoURL!
"URL to display the photo in full resolution"
highRes: PhotoURL!
"The album that holds the photo"
album: Album!
exif: PhotoEXIF
# shares: [ShareToken]
# downloads: [PhotoDownload]
shares: [ShareToken!]!
downloads: [PhotoDownload!]!
}
"EXIF metadata from the camera"
@ -2137,43 +2169,6 @@ func (ec *executionContext) _Photo_path(ctx context.Context, field graphql.Colle
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _Photo_original(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Photo",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Photo().Original(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(*models.PhotoURL)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNPhotoURL2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoURL(ctx, field.Selections, res)
}
func (ec *executionContext) _Photo_thumbnail(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
@ -2319,6 +2314,154 @@ func (ec *executionContext) _Photo_exif(ctx context.Context, field graphql.Colle
return ec.marshalOPhotoEXIF2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoExif(ctx, field.Selections, res)
}
func (ec *executionContext) _Photo_shares(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Photo",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Photo().Shares(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*models.ShareToken)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNShareToken2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐShareTokenᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _Photo_downloads(ctx context.Context, field graphql.CollectedField, obj *models.Photo) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "Photo",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Photo().Downloads(rctx, obj)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.([]*models.PhotoDownload)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNPhotoDownload2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoDownloadᚄ(ctx, field.Selections, res)
}
func (ec *executionContext) _PhotoDownload_title(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "PhotoDownload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.Title, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _PhotoDownload_url(ctx context.Context, field graphql.CollectedField, obj *models.PhotoDownload) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
ec.Tracer.EndFieldExecution(ctx)
}()
rctx := &graphql.ResolverContext{
Object: "PhotoDownload",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithResolverContext(ctx, rctx)
ctx = ec.Tracer.StartFieldResolverExecution(ctx, rctx)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.URL, nil
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !ec.HasError(rctx) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(string)
rctx.Result = res
ctx = ec.Tracer.StartFieldChildExecution(ctx)
return ec.marshalNString2string(ctx, field.Selections, res)
}
func (ec *executionContext) _PhotoEXIF_photo(ctx context.Context, field graphql.CollectedField, obj *models.PhotoExif) (ret graphql.Marshaler) {
ctx = ec.Tracer.StartFieldExecution(ctx, field)
defer func() {
@ -5249,20 +5392,6 @@ func (ec *executionContext) _Photo(ctx context.Context, sel ast.SelectionSet, ob
if out.Values[i] == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
case "original":
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._Photo_original(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "thumbnail":
field := field
out.Concurrently(i, func() (res graphql.Marshaler) {
@ -5316,6 +5445,66 @@ func (ec *executionContext) _Photo(ctx context.Context, sel ast.SelectionSet, ob
res = ec._Photo_exif(ctx, field, obj)
return res
})
case "shares":
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._Photo_shares(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
case "downloads":
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._Photo_downloads(ctx, field, obj)
if res == graphql.Null {
atomic.AddUint32(&invalids, 1)
}
return res
})
default:
panic("unknown field " + strconv.Quote(field.Name))
}
}
out.Dispatch()
if invalids > 0 {
return graphql.Null
}
return out
}
var photoDownloadImplementors = []string{"PhotoDownload"}
func (ec *executionContext) _PhotoDownload(ctx context.Context, sel ast.SelectionSet, obj *models.PhotoDownload) graphql.Marshaler {
fields := graphql.CollectFields(ec.RequestContext, sel, photoDownloadImplementors)
out := graphql.NewFieldSet(fields)
var invalids uint32
for i, field := range fields {
switch field.Name {
case "__typename":
out.Values[i] = graphql.MarshalString("PhotoDownload")
case "title":
out.Values[i] = ec._PhotoDownload_title(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
case "url":
out.Values[i] = ec._PhotoDownload_url(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@ -6128,6 +6317,57 @@ func (ec *executionContext) marshalNPhoto2ᚖgithubᚗcomᚋviktorstrateᚋphoto
return ec._Photo(ctx, sel, v)
}
func (ec *executionContext) marshalNPhotoDownload2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoDownload(ctx context.Context, sel ast.SelectionSet, v models.PhotoDownload) graphql.Marshaler {
return ec._PhotoDownload(ctx, sel, &v)
}
func (ec *executionContext) marshalNPhotoDownload2ᚕᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoDownloadᚄ(ctx context.Context, sel ast.SelectionSet, v []*models.PhotoDownload) graphql.Marshaler {
ret := make(graphql.Array, len(v))
var wg sync.WaitGroup
isLen1 := len(v) == 1
if !isLen1 {
wg.Add(len(v))
}
for i := range v {
i := i
rctx := &graphql.ResolverContext{
Index: &i,
Result: &v[i],
}
ctx := graphql.WithResolverContext(ctx, rctx)
f := func(i int) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = nil
}
}()
if !isLen1 {
defer wg.Done()
}
ret[i] = ec.marshalNPhotoDownload2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoDownload(ctx, sel, v[i])
}
if isLen1 {
f(i)
} else {
go f(i)
}
}
wg.Wait()
return ret
}
func (ec *executionContext) marshalNPhotoDownload2ᚖgithubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoDownload(ctx context.Context, sel ast.SelectionSet, v *models.PhotoDownload) graphql.Marshaler {
if v == nil {
if !ec.HasError(graphql.GetResolverContext(ctx)) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
return ec._PhotoDownload(ctx, sel, v)
}
func (ec *executionContext) marshalNPhotoURL2githubᚗcomᚋviktorstrateᚋphotoviewᚋapiᚋgraphqlᚋmodelsᚐPhotoURL(ctx context.Context, sel ast.SelectionSet, v models.PhotoURL) graphql.Marshaler {
return ec._PhotoURL(ctx, sel, &v)
}

View File

@ -22,6 +22,11 @@ type Filter struct {
Offset *int `json:"offset"`
}
type PhotoDownload struct {
Title string `json:"title"`
URL string `json:"url"`
}
// EXIF metadata from the camera
type PhotoExif struct {
Photo *Photo `json:"photo"`

View File

@ -24,7 +24,6 @@ type PhotoPurpose string
const (
PhotoThumbnail PhotoPurpose = "thumbnail"
PhotoHighRes PhotoPurpose = "high-res"
PhotoOriginal PhotoPurpose = "original"
)
type PhotoURL struct {

View File

@ -3,6 +3,7 @@ package resolvers
import (
"context"
"errors"
"log"
api "github.com/viktorstrate/photoview/api/graphql"
"github.com/viktorstrate/photoview/api/graphql/auth"
@ -56,12 +57,20 @@ func (r *Resolver) Photo() api.PhotoResolver {
return &photoResolver{r}
}
func (r *photoResolver) HighRes(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
panic("not implemented")
func (r *photoResolver) Shares(ctx context.Context, obj *models.Photo) ([]*models.ShareToken, error) {
log.Println("Photo: shares not implemented")
shares := make([]*models.ShareToken, 0)
return shares, nil
}
func (r *photoResolver) Original(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
row := r.Database.QueryRow("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?", obj.PhotoID, models.PhotoOriginal)
func (r *photoResolver) Downloads(ctx context.Context, obj *models.Photo) ([]*models.PhotoDownload, error) {
log.Println("Photo: downloads not implemented")
downloads := make([]*models.PhotoDownload, 0)
return downloads, nil
}
func (r *photoResolver) HighRes(ctx context.Context, obj *models.Photo) (*models.PhotoURL, error) {
row := r.Database.QueryRow("SELECT * FROM photo_url WHERE photo_id = ? AND purpose = ?", obj.PhotoID, models.PhotoHighRes)
url, err := models.NewPhotoURLFromRow(row)
if err != nil {
@ -87,5 +96,6 @@ func (r *photoResolver) Album(ctx context.Context, obj *models.Photo) (*models.A
}
func (r *photoResolver) Exif(ctx context.Context, obj *models.Photo) (*models.PhotoExif, error) {
panic("not implemented")
log.Println("Photo: EXIF not implemented")
return nil, nil
}

View File

@ -135,22 +135,26 @@ type PhotoURL {
height: Int!
}
type PhotoDownload {
title: String!
url: String!
}
type Photo {
id: Int!
title: String!
"Local filepath for the photo"
path: String!
"URL to display the photo in full resolution"
original: PhotoURL!
"URL to display the photo in a smaller resolution"
thumbnail: PhotoURL!
"URL to display the photo in full resolution"
highRes: PhotoURL!
"The album that holds the photo"
album: Album!
exif: PhotoEXIF
# shares: [ShareToken]
# downloads: [PhotoDownload]
shares: [ShareToken!]!
downloads: [PhotoDownload!]!
}
"EXIF metadata from the camera"

View File

@ -42,7 +42,7 @@ func PhotoRoutes(db *sql.DB) chi.Router {
}
}
if purpose == models.PhotoOriginal {
if purpose == models.PhotoHighRes {
var err error
file, err = os.Open(path)
if err != nil {

View File

@ -68,15 +68,17 @@ func ProcessImage(tx *sql.Tx, photoPath string, albumId int, content_type string
photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))]
photoBaseExt := path.Ext(photoName)
original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
original_image_name = strings.ReplaceAll(original_image_name, " ", "_") + photoBaseExt
// high res
highres_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken())
highres_image_name = strings.ReplaceAll(highres_image_name, " ", "_") + photoBaseExt
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo_id, original_image_name, image.Bounds().Max.X, image.Bounds().Max.Y, models.PhotoOriginal, content_type)
_, err = tx.Exec("INSERT INTO photo_url (photo_id, photo_name, width, height, purpose, content_type) VALUES (?, ?, ?, ?, ?, ?)", photo_id, highres_image_name, image.Bounds().Max.X, image.Bounds().Max.Y, models.PhotoHighRes, content_type)
if err != nil {
log.Printf("Could not insert original photo url: %d, %s\n", photo_id, photoName)
log.Printf("Could not insert high-res photo url: %d, %s\n", photo_id, photoName)
return err
}
// Thumbnail
thumbnailImage := resize.Thumbnail(1024, 1024, image, resize.Bilinear)
if _, err := os.Stat("image-cache"); os.IsNotExist(err) {

View File

@ -177,7 +177,7 @@ func directoryContainsPhotos(rootPath string, cache *scanner_cache) bool {
return false
}
var supported_mimetypes = [...]string{
var SupportedMimetypes = [...]string{
"image/jpeg",
"image/png",
"image/tiff",
@ -186,6 +186,13 @@ var supported_mimetypes = [...]string{
"image/bmp",
}
var WebMimetypes = [...]string{
"image/jpeg",
"image/png",
"image/webp",
"image/bmp",
}
func isPathImage(path string, cache *scanner_cache) bool {
if cache.get_photo_type(path) != nil {
log.Printf("Image cache hit: %s\n", path)
@ -209,7 +216,7 @@ func isPathImage(path string, cache *scanner_cache) bool {
return false
}
for _, supported_mime := range supported_mimetypes {
for _, supported_mime := range SupportedMimetypes {
if supported_mime == imgType.MIME.Value {
cache.insert_photo_type(path, supported_mime)
return true

View File

@ -30,7 +30,7 @@ const albumQuery = gql`
width
height
}
original {
highRes {
url
}
}

View File

@ -22,7 +22,7 @@ const photoQuery = gql`
width
height
}
original {
highRes {
url
}
}

View File

@ -50,7 +50,7 @@ export const PresentPhoto = ({ photo, imageLoaded, ...otherProps }) => {
<StyledPhoto src={photo.thumbnail.url} />
<StyledPhoto
style={{ display: 'none' }}
src={photo.original.url}
src={photo.highRes.url}
onLoad={e => {
e.target.style.display = 'initial'
imageLoaded && imageLoaded()

View File

@ -13,7 +13,7 @@ const photoQuery = gql`
photo(id: $id) {
id
title
original {
highRes {
url
width
height
@ -22,9 +22,7 @@ const photoQuery = gql`
camera
maker
lens
dateShot {
formatted
}
dateShot
fileSize
exposure
aperture
@ -77,7 +75,7 @@ const SidebarContent = ({ photo, hidePreview }) => {
{}
)
exif.dateShot = new Date(exif.dateShot.formatted).toLocaleString()
exif.dateShot = new Date(exif.dateShot).toLocaleString()
exifItems = exifKeys.map(key => (
<SidebarItem key={key} name={exifNameLookup[key]} value={exif[key]} />
@ -86,7 +84,7 @@ const SidebarContent = ({ photo, hidePreview }) => {
let previewUrl = null
if (photo) {
if (photo.original) previewUrl = photo.original.url
if (photo.highRes) previewUrl = photo.highRes.url
else if (photo.thumbnail) previewUrl = photo.thumbnail.url
}

View File

@ -54,7 +54,7 @@ const SidebarDownload = ({ photoId }) => {
}
SidebarDownload.propTypes = {
photoId: PropTypes.string,
photoId: PropTypes.number,
}
export default SidebarDownload