Add mapbox to sidebar
This commit is contained in:
parent
57d408cb52
commit
c357532613
|
@ -6,7 +6,7 @@ import {
|
|||
ProtectedVideo,
|
||||
} from '../../components/photoGallery/ProtectedMedia'
|
||||
import { SidebarContext } from '../../components/sidebar/Sidebar'
|
||||
import MediaSidebar from '../../components/sidebar/MediaSidebar'
|
||||
import MediaSidebar from '../../components/sidebar/MediaSidebar/MediaSidebar'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SharePageToken_shareToken_media } from './__generated__/SharePageToken'
|
||||
import { MediaType } from '../../__generated__/globalTypes'
|
||||
|
|
|
@ -2,7 +2,7 @@ import React from 'react'
|
|||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import { MediaType } from '../../__generated__/globalTypes'
|
||||
import { MediaSidebarMedia } from '../sidebar/MediaSidebar'
|
||||
import { MediaSidebarMedia } from '../sidebar/MediaSidebar/MediaSidebar'
|
||||
import { sidebarPhoto_media_faces } from '../sidebar/__generated__/sidebarPhoto'
|
||||
|
||||
interface FaceBoxStyleProps {
|
||||
|
|
|
@ -20,10 +20,13 @@ const MapContainer = styled.div`
|
|||
|
||||
type MapboxMapProps = {
|
||||
configureMapbox(map: mapboxgl.Map, mapboxLibrary: typeof mapboxgl): void
|
||||
readonly initialZoom?: number
|
||||
mapboxOptions?: Partial<mapboxgl.MapboxOptions>
|
||||
}
|
||||
|
||||
const useMapboxMap = ({ configureMapbox, initialZoom = 1 }: MapboxMapProps) => {
|
||||
const useMapboxMap = ({
|
||||
configureMapbox,
|
||||
mapboxOptions = undefined,
|
||||
}: MapboxMapProps) => {
|
||||
const [mapboxLibrary, setMapboxLibrary] = useState<typeof mapboxgl>()
|
||||
const mapContainer = useRef<HTMLDivElement | null>(null)
|
||||
const map = useRef<mapboxgl.Map | null>(null)
|
||||
|
@ -57,12 +60,15 @@ const useMapboxMap = ({ configureMapbox, initialZoom = 1 }: MapboxMapProps) => {
|
|||
map.current = new mapboxLibrary.Map({
|
||||
container: mapContainer.current,
|
||||
style: 'mapbox://styles/mapbox/streets-v11',
|
||||
zoom: initialZoom,
|
||||
...mapboxOptions,
|
||||
})
|
||||
|
||||
configureMapbox(map.current, mapboxLibrary)
|
||||
map.current?.resize()
|
||||
}, [mapContainer, mapboxLibrary, mapboxData])
|
||||
|
||||
map.current?.resize()
|
||||
|
||||
return {
|
||||
mapContainer: <MapContainer ref={mapContainer}></MapContainer>,
|
||||
mapboxMap: map.current,
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
toggleFavoriteAction,
|
||||
useMarkFavoriteMutation,
|
||||
} from './photoGalleryMutations'
|
||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
||||
import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar'
|
||||
import { SidebarContext } from '../sidebar/Sidebar'
|
||||
|
||||
const Gallery = styled.div`
|
||||
|
|
|
@ -0,0 +1,242 @@
|
|||
import { gql, useLazyQuery } from '@apollo/client'
|
||||
import React, { useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { authToken } from '../../../helpers/authentication'
|
||||
import { MediaType } from '../../../__generated__/globalTypes'
|
||||
import { SidebarFacesOverlay } from '../../facesOverlay/FacesOverlay'
|
||||
import {
|
||||
ProtectedImage,
|
||||
ProtectedVideo,
|
||||
ProtectedVideoProps_Media,
|
||||
} from '../../photoGallery/ProtectedMedia'
|
||||
import { SidebarPhotoCover } from '../AlbumCovers'
|
||||
import { SidebarPhotoShare } from '../Sharing'
|
||||
import SidebarMediaDownload from '../SidebarDownloadMedia'
|
||||
import SidebarHeader from '../SidebarHeader'
|
||||
import { sidebarDownloadQuery_media_downloads } from '../__generated__/sidebarDownloadQuery'
|
||||
import {
|
||||
sidebarPhoto,
|
||||
sidebarPhotoVariables,
|
||||
sidebarPhoto_media_exif,
|
||||
sidebarPhoto_media_faces,
|
||||
sidebarPhoto_media_thumbnail,
|
||||
sidebarPhoto_media_videoMetadata,
|
||||
} from '../__generated__/sidebarPhoto'
|
||||
import ExifDetails from './MediaSidebarExif'
|
||||
import MediaSidebarMap from './MediaSidebarMap'
|
||||
|
||||
const SIDEBAR_MEDIA_QUERY = gql`
|
||||
query sidebarPhoto($id: ID!) {
|
||||
media(id: $id) {
|
||||
id
|
||||
title
|
||||
type
|
||||
highRes {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
thumbnail {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
videoWeb {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
videoMetadata {
|
||||
id
|
||||
width
|
||||
height
|
||||
duration
|
||||
codec
|
||||
framerate
|
||||
bitrate
|
||||
colorProfile
|
||||
audio
|
||||
}
|
||||
exif {
|
||||
id
|
||||
camera
|
||||
maker
|
||||
lens
|
||||
dateShot
|
||||
exposure
|
||||
aperture
|
||||
iso
|
||||
focalLength
|
||||
flash
|
||||
exposureProgram
|
||||
coordinates {
|
||||
latitude
|
||||
longitude
|
||||
}
|
||||
}
|
||||
faces {
|
||||
id
|
||||
rectangle {
|
||||
minX
|
||||
maxX
|
||||
minY
|
||||
maxY
|
||||
}
|
||||
faceGroup {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PreviewImage = styled(ProtectedImage)`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const PreviewVideo = styled(ProtectedVideo)`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media {
|
||||
type: MediaType
|
||||
}
|
||||
|
||||
type PreviewMediaProps = {
|
||||
media: PreviewMediaPropsMedia
|
||||
previewImage?: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => {
|
||||
if (media.type === MediaType.Photo) {
|
||||
return <PreviewImage src={previewImage?.url} />
|
||||
}
|
||||
|
||||
if (media.type === MediaType.Video) {
|
||||
return <PreviewVideo media={media} />
|
||||
}
|
||||
|
||||
return <div>ERROR: Unknown media type: {media.type}</div>
|
||||
}
|
||||
|
||||
type SidebarContentProps = {
|
||||
media: MediaSidebarMedia
|
||||
hidePreview?: boolean
|
||||
}
|
||||
|
||||
const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
|
||||
let previewImage = null
|
||||
if (media.highRes) previewImage = media.highRes
|
||||
else if (media.thumbnail) previewImage = media.thumbnail
|
||||
|
||||
const imageAspect =
|
||||
previewImage?.width && previewImage?.height
|
||||
? previewImage.height / previewImage.width
|
||||
: 3 / 2
|
||||
|
||||
let sidebarMap = null
|
||||
const mediaCoordinates = media.exif?.coordinates
|
||||
if (mediaCoordinates) {
|
||||
sidebarMap = <MediaSidebarMap coordinates={mediaCoordinates} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SidebarHeader title={media.title ?? 'Loading...'} />
|
||||
<div className="lg:mx-4">
|
||||
{!hidePreview && (
|
||||
<div
|
||||
className="w-full h-0 relative"
|
||||
style={{ paddingTop: `${Math.min(imageAspect, 0.75) * 100}%` }}
|
||||
>
|
||||
<PreviewMedia
|
||||
previewImage={previewImage || undefined}
|
||||
media={media}
|
||||
/>
|
||||
<SidebarFacesOverlay media={media} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ExifDetails media={media} />
|
||||
{sidebarMap}
|
||||
<SidebarMediaDownload media={media} />
|
||||
<SidebarPhotoShare id={media.id} />
|
||||
<div className="mt-8">
|
||||
<SidebarPhotoCover cover_id={media.id} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MediaSidebarMedia {
|
||||
__typename: 'Media'
|
||||
id: string
|
||||
title?: string
|
||||
type: MediaType
|
||||
highRes?: null | {
|
||||
__typename: 'MediaURL'
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
thumbnail?: sidebarPhoto_media_thumbnail | null
|
||||
videoWeb?: null | {
|
||||
__typename: 'MediaURL'
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
videoMetadata?: sidebarPhoto_media_videoMetadata | null
|
||||
exif?: sidebarPhoto_media_exif | null
|
||||
faces?: sidebarPhoto_media_faces[]
|
||||
downloads?: sidebarDownloadQuery_media_downloads[]
|
||||
}
|
||||
|
||||
type MediaSidebarType = {
|
||||
media: MediaSidebarMedia
|
||||
hidePreview?: boolean
|
||||
}
|
||||
|
||||
const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => {
|
||||
const [loadMedia, { loading, error, data }] = useLazyQuery<
|
||||
sidebarPhoto,
|
||||
sidebarPhotoVariables
|
||||
>(SIDEBAR_MEDIA_QUERY)
|
||||
|
||||
useEffect(() => {
|
||||
if (media != null && authToken()) {
|
||||
loadMedia({
|
||||
variables: {
|
||||
id: media.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [media])
|
||||
|
||||
if (!media) return null
|
||||
|
||||
if (!authToken()) {
|
||||
return <SidebarContent media={media} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
if (error) return <div>{error.message}</div>
|
||||
|
||||
if (loading || data == null) {
|
||||
return <SidebarContent media={media} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
return <SidebarContent media={data.media} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
export default MediaSidebar
|
|
@ -1,147 +1,20 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { useLazyQuery, gql } from '@apollo/client'
|
||||
import styled from 'styled-components'
|
||||
import { authToken } from '../../helpers/authentication'
|
||||
import {
|
||||
ProtectedImage,
|
||||
ProtectedVideo,
|
||||
ProtectedVideoProps_Media,
|
||||
} from '../photoGallery/ProtectedMedia'
|
||||
import { SidebarPhotoShare } from './Sharing'
|
||||
import SidebarMediaDownload from './SidebarDownloadMedia'
|
||||
import SidebarItem from './SidebarItem'
|
||||
import { SidebarFacesOverlay } from '../facesOverlay/FacesOverlay'
|
||||
import { isNil } from '../../helpers/utils'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MediaType } from '../../__generated__/globalTypes'
|
||||
import { TranslationFn } from '../../localization'
|
||||
import {
|
||||
sidebarPhoto,
|
||||
sidebarPhotoVariables,
|
||||
sidebarPhoto_media_exif,
|
||||
sidebarPhoto_media_faces,
|
||||
sidebarPhoto_media_thumbnail,
|
||||
sidebarPhoto_media_videoMetadata,
|
||||
} from './__generated__/sidebarPhoto'
|
||||
|
||||
import { sidebarDownloadQuery_media_downloads } from './__generated__/sidebarDownloadQuery'
|
||||
import SidebarHeader from './SidebarHeader'
|
||||
import { SidebarPhotoCover } from './AlbumCovers'
|
||||
|
||||
const SIDEBAR_MEDIA_QUERY = gql`
|
||||
query sidebarPhoto($id: ID!) {
|
||||
media(id: $id) {
|
||||
id
|
||||
title
|
||||
type
|
||||
highRes {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
thumbnail {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
videoWeb {
|
||||
url
|
||||
width
|
||||
height
|
||||
}
|
||||
videoMetadata {
|
||||
id
|
||||
width
|
||||
height
|
||||
duration
|
||||
codec
|
||||
framerate
|
||||
bitrate
|
||||
colorProfile
|
||||
audio
|
||||
}
|
||||
exif {
|
||||
id
|
||||
camera
|
||||
maker
|
||||
lens
|
||||
dateShot
|
||||
exposure
|
||||
aperture
|
||||
iso
|
||||
focalLength
|
||||
flash
|
||||
exposureProgram
|
||||
coordinates {
|
||||
latitude
|
||||
longitude
|
||||
}
|
||||
}
|
||||
faces {
|
||||
id
|
||||
rectangle {
|
||||
minX
|
||||
maxX
|
||||
minY
|
||||
maxY
|
||||
}
|
||||
faceGroup {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PreviewImage = styled(ProtectedImage)`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
object-fit: contain;
|
||||
`
|
||||
|
||||
const PreviewVideo = styled(ProtectedVideo)`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
`
|
||||
|
||||
interface PreviewMediaPropsMedia extends ProtectedVideoProps_Media {
|
||||
type: MediaType
|
||||
}
|
||||
|
||||
type PreviewMediaProps = {
|
||||
media: PreviewMediaPropsMedia
|
||||
previewImage?: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
const PreviewMedia = ({ media, previewImage }: PreviewMediaProps) => {
|
||||
if (media.type === MediaType.Photo) {
|
||||
return <PreviewImage src={previewImage?.url} />
|
||||
}
|
||||
|
||||
if (media.type === MediaType.Video) {
|
||||
return <PreviewVideo media={media} />
|
||||
}
|
||||
|
||||
return <div>ERROR: Unknown media type: {media.type}</div>
|
||||
}
|
||||
import styled from 'styled-components'
|
||||
import { isNil } from '../../../helpers/utils'
|
||||
import { TranslationFn } from '../../../localization'
|
||||
import SidebarItem from '../SidebarItem'
|
||||
import { MediaSidebarMedia } from './MediaSidebar'
|
||||
|
||||
const MetadataInfoContainer = styled.div`
|
||||
margin-bottom: 1.5rem;
|
||||
`
|
||||
|
||||
type MediaInfoProps = {
|
||||
type ExifDetailsProps = {
|
||||
media?: MediaSidebarMedia
|
||||
}
|
||||
|
||||
export const MetadataInfo = ({ media }: MediaInfoProps) => {
|
||||
const ExifDetails = ({ media }: ExifDetailsProps) => {
|
||||
const { t } = useTranslation()
|
||||
let exifItems: JSX.Element[] = []
|
||||
|
||||
|
@ -343,106 +216,4 @@ const flashLookup = (t: TranslationFn): { [key: number]: string } => {
|
|||
}
|
||||
}
|
||||
|
||||
type SidebarContentProps = {
|
||||
media: MediaSidebarMedia
|
||||
hidePreview?: boolean
|
||||
}
|
||||
|
||||
const SidebarContent = ({ media, hidePreview }: SidebarContentProps) => {
|
||||
let previewImage = null
|
||||
if (media.highRes) previewImage = media.highRes
|
||||
else if (media.thumbnail) previewImage = media.thumbnail
|
||||
|
||||
const imageAspect =
|
||||
previewImage?.width && previewImage?.height
|
||||
? previewImage.height / previewImage.width
|
||||
: 3 / 2
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SidebarHeader title={media.title ?? 'Loading...'} />
|
||||
<div className="lg:mx-4">
|
||||
{!hidePreview && (
|
||||
<div
|
||||
className="w-full h-0 relative"
|
||||
style={{ paddingTop: `${Math.min(imageAspect, 0.75) * 100}%` }}
|
||||
>
|
||||
<PreviewMedia
|
||||
previewImage={previewImage || undefined}
|
||||
media={media}
|
||||
/>
|
||||
<SidebarFacesOverlay media={media} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<MetadataInfo media={media} />
|
||||
<SidebarMediaDownload media={media} />
|
||||
<SidebarPhotoShare id={media.id} />
|
||||
<div className="mt-8">
|
||||
<SidebarPhotoCover cover_id={media.id} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export interface MediaSidebarMedia {
|
||||
__typename: 'Media'
|
||||
id: string
|
||||
title?: string
|
||||
type: MediaType
|
||||
highRes?: null | {
|
||||
__typename: 'MediaURL'
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
thumbnail?: sidebarPhoto_media_thumbnail | null
|
||||
videoWeb?: null | {
|
||||
__typename: 'MediaURL'
|
||||
url: string
|
||||
width?: number
|
||||
height?: number
|
||||
}
|
||||
videoMetadata?: sidebarPhoto_media_videoMetadata | null
|
||||
exif?: sidebarPhoto_media_exif | null
|
||||
faces?: sidebarPhoto_media_faces[]
|
||||
downloads?: sidebarDownloadQuery_media_downloads[]
|
||||
}
|
||||
|
||||
type MediaSidebarType = {
|
||||
media: MediaSidebarMedia
|
||||
hidePreview?: boolean
|
||||
}
|
||||
|
||||
const MediaSidebar = ({ media, hidePreview }: MediaSidebarType) => {
|
||||
const [loadMedia, { loading, error, data }] = useLazyQuery<
|
||||
sidebarPhoto,
|
||||
sidebarPhotoVariables
|
||||
>(SIDEBAR_MEDIA_QUERY)
|
||||
|
||||
useEffect(() => {
|
||||
if (media != null && authToken()) {
|
||||
loadMedia({
|
||||
variables: {
|
||||
id: media.id,
|
||||
},
|
||||
})
|
||||
}
|
||||
}, [media])
|
||||
|
||||
if (!media) return null
|
||||
|
||||
if (!authToken()) {
|
||||
return <SidebarContent media={media} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
if (error) return <div>{error.message}</div>
|
||||
|
||||
if (loading || data == null) {
|
||||
return <SidebarContent media={media} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
return <SidebarContent media={data.media} hidePreview={hidePreview} />
|
||||
}
|
||||
|
||||
export default MediaSidebar
|
||||
export default ExifDetails
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { isNil } from '../../../helpers/utils'
|
||||
import useMapboxMap from '../../mapbox/MapboxMap'
|
||||
import { SidebarSection, SidebarSectionTitle } from '../SidebarComponents'
|
||||
import { sidebarPhoto_media_exif_coordinates } from '../__generated__/sidebarPhoto'
|
||||
|
||||
type MediaSidebarMapProps = {
|
||||
coordinates: sidebarPhoto_media_exif_coordinates
|
||||
}
|
||||
|
||||
const MediaSidebarMap = ({ coordinates }: MediaSidebarMapProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { mapContainer, mapboxToken } = useMapboxMap({
|
||||
mapboxOptions: {
|
||||
interactive: false,
|
||||
zoom: 12,
|
||||
center: {
|
||||
lat: coordinates.latitude,
|
||||
lng: coordinates.longitude,
|
||||
},
|
||||
},
|
||||
configureMapbox: (map, mapboxLibrary) => {
|
||||
// todo
|
||||
map.addControl(
|
||||
new mapboxLibrary.NavigationControl({ showCompass: false })
|
||||
)
|
||||
|
||||
const centerMarker = new mapboxLibrary.Marker({
|
||||
color: 'red',
|
||||
scale: 0.8,
|
||||
})
|
||||
centerMarker.setLngLat({
|
||||
lat: coordinates.latitude,
|
||||
lng: coordinates.longitude,
|
||||
})
|
||||
centerMarker.addTo(map)
|
||||
},
|
||||
})
|
||||
|
||||
if (isNil(mapboxToken)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarSection>
|
||||
<SidebarSectionTitle>
|
||||
{t('sidebar.location.title', 'Location')}
|
||||
</SidebarSectionTitle>
|
||||
<div className="w-full h-64">{mapContainer}</div>
|
||||
</SidebarSection>
|
||||
)
|
||||
}
|
||||
|
||||
export default MediaSidebarMap
|
|
@ -4,7 +4,7 @@ import { useLazyQuery, gql } from '@apollo/client'
|
|||
import { authToken } from '../../helpers/authentication'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { TranslationFn } from '../../localization'
|
||||
import { MediaSidebarMedia } from './MediaSidebar'
|
||||
import { MediaSidebarMedia } from './MediaSidebar/MediaSidebar'
|
||||
import {
|
||||
sidebarDownloadQuery,
|
||||
sidebarDownloadQueryVariables,
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
toggleFavoriteAction,
|
||||
useMarkFavoriteMutation,
|
||||
} from '../photoGallery/photoGalleryMutations'
|
||||
import MediaSidebar from '../sidebar/MediaSidebar'
|
||||
import MediaSidebar from '../sidebar/MediaSidebar/MediaSidebar'
|
||||
import { SidebarContext } from '../sidebar/Sidebar'
|
||||
import {
|
||||
getActiveTimelineImage,
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Download"
|
||||
},
|
||||
"location": {
|
||||
"title": "Lokation"
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Download"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Download"
|
||||
},
|
||||
"location": {
|
||||
"title": "Location"
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Descargar"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Télécharger"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Download"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -264,6 +264,9 @@
|
|||
},
|
||||
"title": "Pobierz"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -264,6 +264,9 @@
|
|||
},
|
||||
"title": "Скачать"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
|
@ -259,6 +259,9 @@
|
|||
},
|
||||
"title": "Ladda ner"
|
||||
},
|
||||
"location": {
|
||||
"title": ""
|
||||
},
|
||||
"media": {
|
||||
"exif": {
|
||||
"exposure_program": {
|
||||
|
|
Loading…
Reference in New Issue