Add present animations
This commit is contained in:
parent
6d998a392f
commit
52d59b965b
|
@ -16,9 +16,10 @@
|
|||
"ecmaVersion": 2018,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react"],
|
||||
"plugins": ["react", "react-hooks"],
|
||||
"rules": {
|
||||
"no-unused-vars": "off"
|
||||
"no-unused-vars": "off",
|
||||
"react/display-name": "off"
|
||||
},
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
|
|
|
@ -3129,6 +3129,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"eslint-plugin-react-hooks": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz",
|
||||
"integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "3.7.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"babel-eslint": "^10.0.2",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-plugin-react": "^7.14.3",
|
||||
"eslint-plugin-react-hooks": "^1.7.0",
|
||||
"husky": "^3.0.2",
|
||||
"isarray": "^2.0.5",
|
||||
"lint-staged": "^9.2.1",
|
||||
|
|
|
@ -52,7 +52,7 @@ const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => {
|
|||
}
|
||||
|
||||
AuthorizedRoute.propTypes = {
|
||||
component: PropTypes.element.isRequired,
|
||||
component: PropTypes.object.isRequired,
|
||||
admin: PropTypes.bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ const SideButton = props => {
|
|||
}
|
||||
|
||||
SideButton.propTypes = {
|
||||
children: PropTypes.element,
|
||||
children: PropTypes.any,
|
||||
}
|
||||
|
||||
const SideButtonLabel = styled.div`
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { animated } from 'react-spring'
|
||||
import { Transition } from 'react-spring/renderprops'
|
||||
import styled from 'styled-components'
|
||||
import { Loader } from 'semantic-ui-react'
|
||||
import { Photo } from './Photo'
|
||||
import PresentView from './PresentView'
|
||||
import { PresentContainer, PresentPhoto } from './PresentView'
|
||||
import PropTypes from 'prop-types'
|
||||
import { fetchProtectedImage } from './ProtectedImage'
|
||||
import { SidebarConsumer } from '../sidebar/Sidebar'
|
||||
import PhotoSidebar from '../sidebar/PhotoSidebar'
|
||||
|
||||
|
@ -24,130 +25,138 @@ export const presentIndexFromHash = hash => {
|
|||
return match && parseInt(match[1])
|
||||
}
|
||||
|
||||
class PhotoGallery extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
|
||||
this.keyDownEvent = e => {
|
||||
if (!this.props.onSelectImage || this.props.activeIndex == -1) {
|
||||
const PhotoGallery = ({
|
||||
activeIndex = -1,
|
||||
photos,
|
||||
loading,
|
||||
onSelectImage,
|
||||
presenting,
|
||||
setPresenting,
|
||||
nextImage,
|
||||
previousImage,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const keyDownEvent = e => {
|
||||
if (!onSelectImage || activeIndex == -1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key == 'ArrowRight') {
|
||||
this.props.nextImage && this.props.nextImage()
|
||||
setMoveDirection('right')
|
||||
nextImage && nextImage()
|
||||
}
|
||||
|
||||
if (e.key == 'ArrowLeft') {
|
||||
this.props.nextImage && this.props.previousImage()
|
||||
setMoveDirection('left')
|
||||
nextImage && previousImage()
|
||||
}
|
||||
|
||||
if (e.key == 'Escape') {
|
||||
this.props.setPresenting(false)
|
||||
setMoveDirection(null)
|
||||
setPresenting(false)
|
||||
}
|
||||
}
|
||||
|
||||
this.preloadImages = this.preloadImages.bind(this)
|
||||
}
|
||||
document.addEventListener('keydown', keyDownEvent)
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.keyDownEvent)
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.keyDownEvent)
|
||||
}
|
||||
|
||||
preloadImages() {
|
||||
async function preloadImage(url) {
|
||||
var img = new Image()
|
||||
img.src = await fetchProtectedImage(url)
|
||||
return function cleanup() {
|
||||
document.removeEventListener('keydown', keyDownEvent)
|
||||
}
|
||||
})
|
||||
|
||||
const { activeIndex = -1, photos } = this.props
|
||||
const [moveDirection, setMoveDirection] = useState(null)
|
||||
|
||||
if (activeIndex != -1 && photos) {
|
||||
let previousIndex = null
|
||||
let nextIndex = null
|
||||
const activeImage = photos && activeIndex != -1 && photos[activeIndex]
|
||||
|
||||
if (activeIndex > 0) {
|
||||
previousIndex = activeIndex - 1
|
||||
} else {
|
||||
previousIndex = photos.length - 1
|
||||
}
|
||||
const getPhotoElements = updateSidebar => {
|
||||
let photoElements = null
|
||||
if (photos) {
|
||||
photos.filter(photo => photo.thumbnail)
|
||||
|
||||
nextIndex = (activeIndex + 1) % photos.length
|
||||
photoElements = photos.map((photo, index) => {
|
||||
const active = activeIndex == index
|
||||
|
||||
preloadImage(photos[nextIndex].original.url)
|
||||
preloadImage(photos[previousIndex].original.url)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activeIndex = -1,
|
||||
photos,
|
||||
loading,
|
||||
onSelectImage,
|
||||
presenting,
|
||||
} = this.props
|
||||
|
||||
const activeImage = photos && activeIndex != -1 && photos[activeIndex]
|
||||
|
||||
const getPhotoElements = updateSidebar => {
|
||||
let photoElements = null
|
||||
if (photos) {
|
||||
photos.filter(photo => photo.thumbnail)
|
||||
|
||||
photoElements = photos.map((photo, index) => {
|
||||
const active = activeIndex == index
|
||||
|
||||
let minWidth = 100
|
||||
if (photo.thumbnail) {
|
||||
minWidth = Math.floor(
|
||||
(photo.thumbnail.width / photo.thumbnail.height) * 200
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Photo
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onSelectImage={index => {
|
||||
updateSidebar(<PhotoSidebar imageId={photo.id} />)
|
||||
onSelectImage(index)
|
||||
}}
|
||||
setPresenting={this.props.setPresenting}
|
||||
minWidth={minWidth}
|
||||
index={index}
|
||||
active={active}
|
||||
/>
|
||||
let minWidth = 100
|
||||
if (photo.thumbnail) {
|
||||
minWidth = Math.floor(
|
||||
(photo.thumbnail.width / photo.thumbnail.height) * 200
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return photoElements
|
||||
return (
|
||||
<Photo
|
||||
key={photo.id}
|
||||
photo={photo}
|
||||
onSelectImage={index => {
|
||||
updateSidebar(<PhotoSidebar imageId={photo.id} />)
|
||||
onSelectImage(index)
|
||||
}}
|
||||
setPresenting={setPresenting}
|
||||
minWidth={minWidth}
|
||||
index={index}
|
||||
active={active}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<SidebarConsumer>
|
||||
{({ updateSidebar }) => (
|
||||
<div>
|
||||
return photoElements
|
||||
}
|
||||
|
||||
let transformDirectionIndex = 0
|
||||
if (moveDirection == 'right') transformDirectionIndex = 1
|
||||
if (moveDirection == 'left') transformDirectionIndex = 2
|
||||
|
||||
const presentViewTransitionConfig = {
|
||||
items: activeImage,
|
||||
keys: x => x,
|
||||
config: {
|
||||
tension: 220,
|
||||
},
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: [
|
||||
'translate(0%, 0)',
|
||||
'translate(12%, 0)',
|
||||
'translate(-12%, 0)',
|
||||
][transformDirectionIndex],
|
||||
},
|
||||
enter: {
|
||||
opacity: 1,
|
||||
transform: 'translate(0%, 0)',
|
||||
},
|
||||
}
|
||||
|
||||
const AnimatedPresentPhoto = animated(PresentPhoto)
|
||||
|
||||
return (
|
||||
<SidebarConsumer>
|
||||
{({ updateSidebar }) => (
|
||||
<div>
|
||||
{!presenting ? (
|
||||
<Gallery>
|
||||
<Loader active={loading}>Loading images</Loader>
|
||||
{getPhotoElements(updateSidebar)}
|
||||
<PhotoFiller />
|
||||
</Gallery>
|
||||
<PresentView
|
||||
presenting={presenting}
|
||||
image={activeImage && activeImage.id}
|
||||
thumbnail={activeImage && activeImage.thumbnail.url}
|
||||
imageLoaded={this.preloadImages()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</SidebarConsumer>
|
||||
)
|
||||
}
|
||||
) : (
|
||||
<PresentContainer>
|
||||
<Transition {...presentViewTransitionConfig}>
|
||||
{item => props => (
|
||||
<PresentPhoto
|
||||
thumbnail={item.thumbnail.url}
|
||||
photoId={item.id}
|
||||
style={props}
|
||||
/>
|
||||
)}
|
||||
</Transition>
|
||||
</PresentContainer>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SidebarConsumer>
|
||||
)
|
||||
// }
|
||||
}
|
||||
|
||||
PhotoGallery.propTypes = {
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import styled, { createGlobalStyle } from 'styled-components'
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import ProtectedImage from './ProtectedImage'
|
||||
|
||||
const PresentContainer = styled.div`
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
color: white;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
`
|
||||
export const PresentContainer = ({ children, ...otherProps }) => {
|
||||
const StyledContainer = styled.div`
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
color: white;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
`
|
||||
|
||||
return (
|
||||
<StyledContainer {...otherProps}>
|
||||
<PreventScroll />
|
||||
{children}
|
||||
</StyledContainer>
|
||||
)
|
||||
}
|
||||
|
||||
PresentContainer.propTypes = {
|
||||
children: PropTypes.element,
|
||||
}
|
||||
|
||||
const PreventScroll = createGlobalStyle`
|
||||
body {
|
||||
|
@ -37,65 +50,74 @@ const imageQuery = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const PresentImage = styled(ProtectedImage)`
|
||||
const StyledPhoto = styled(ProtectedImage)`
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
`
|
||||
|
||||
const PresentView = ({
|
||||
image,
|
||||
presenting,
|
||||
export const PresentPhoto = ({
|
||||
photo,
|
||||
thumbnail,
|
||||
imageLoaded: imageLoadedCallback,
|
||||
imageLoaded,
|
||||
photoId,
|
||||
...otherProps
|
||||
}) => {
|
||||
if (!image || !presenting) {
|
||||
return null
|
||||
}
|
||||
let [originalPhoto, setOriginalPhoto] = useState(null)
|
||||
useEffect(() => {
|
||||
if (!photoId) return
|
||||
|
||||
function loadOriginalPhoto() {
|
||||
const originalPhoto = (
|
||||
<Query query={imageQuery} variables={{ id: photoId }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (error) {
|
||||
alert(error)
|
||||
return null
|
||||
}
|
||||
|
||||
if (data && data.photo) {
|
||||
const photo = data.photo
|
||||
|
||||
return (
|
||||
<StyledPhoto
|
||||
style={{ display: 'none' }}
|
||||
src={photo.original.url}
|
||||
onLoad={e => {
|
||||
e.target.style.display = 'initial'
|
||||
imageLoaded && imageLoaded()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
|
||||
setOriginalPhoto(originalPhoto)
|
||||
}
|
||||
|
||||
const timeoutHandle = setTimeout(loadOriginalPhoto, 500)
|
||||
|
||||
return function cleanup() {
|
||||
clearTimeout(timeoutHandle)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<PresentContainer>
|
||||
<PreventScroll />
|
||||
<Query query={imageQuery} variables={{ id: image }}>
|
||||
{({ loading, error, data }) => {
|
||||
if (error) {
|
||||
alert(error)
|
||||
return <div>{error.message}</div>
|
||||
}
|
||||
|
||||
let original = null
|
||||
if (!loading) {
|
||||
const { photo } = data
|
||||
original = (
|
||||
<PresentImage
|
||||
// style={{ display: 'none' }}
|
||||
src={photo && photo.original.url}
|
||||
onLoad={e => {
|
||||
// e.target.style.display = 'initial'
|
||||
imageLoadedCallback && imageLoadedCallback()
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{original}
|
||||
<PresentImage src={thumbnail} />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
</Query>
|
||||
</PresentContainer>
|
||||
<div {...otherProps}>
|
||||
{originalPhoto}
|
||||
<StyledPhoto src={thumbnail} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
PresentView.propTypes = {
|
||||
image: PropTypes.string.isRequired,
|
||||
presenting: PropTypes.bool,
|
||||
PresentPhoto.propTypes = {
|
||||
photo: PropTypes.object,
|
||||
thumbnail: PropTypes.string.isRequired,
|
||||
imageLoaded: PropTypes.func.isRequired,
|
||||
imageLoaded: PropTypes.func,
|
||||
photoId: PropTypes.string,
|
||||
}
|
||||
|
||||
export default PresentView
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import React from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
let imageCache = {}
|
||||
|
||||
export async function fetchProtectedImage(src) {
|
||||
export async function fetchProtectedImage(src, { signal } = { signal: null }) {
|
||||
if (src) {
|
||||
if (imageCache[src]) {
|
||||
return imageCache[src]
|
||||
|
@ -13,11 +13,13 @@ export async function fetchProtectedImage(src) {
|
|||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
signal,
|
||||
})
|
||||
|
||||
image = await image.blob()
|
||||
const url = URL.createObjectURL(image)
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
imageCache[src] = url
|
||||
|
||||
return url
|
||||
|
@ -27,40 +29,36 @@ export async function fetchProtectedImage(src) {
|
|||
/**
|
||||
* An image that needs a authorization header to load
|
||||
*/
|
||||
class ProtectedImage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const ProtectedImage = ({ src, ...props }) => {
|
||||
const [imgSrc, setImgSrc] = useState(null)
|
||||
|
||||
this.state = {
|
||||
imgSrc: null,
|
||||
}
|
||||
useEffect(() => {
|
||||
if (imageCache[src]) return
|
||||
|
||||
this.shouldRefresh = true
|
||||
}
|
||||
const fetchController = new AbortController()
|
||||
let canceled = false
|
||||
|
||||
shouldComponentUpdate(newProps) {
|
||||
if (newProps.src != this.props.src) this.shouldRefresh = true
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.shouldRefresh) {
|
||||
this.shouldRefresh = false
|
||||
|
||||
fetchProtectedImage(this.props.src).then(imgSrc => {
|
||||
this.setState({
|
||||
imgSrc,
|
||||
})
|
||||
fetchProtectedImage(src, { signal: fetchController.signal })
|
||||
.then(newSrc => {
|
||||
if (!canceled) {
|
||||
setImgSrc(newSrc)
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Fetch image error', error.message)
|
||||
})
|
||||
}
|
||||
|
||||
return <img {...this.props} src={this.state.imgSrc} />
|
||||
}
|
||||
return function cleanup() {
|
||||
canceled = true
|
||||
fetchController.abort()
|
||||
}
|
||||
}, [src])
|
||||
|
||||
return <img {...props} src={imageCache[src] || imgSrc} />
|
||||
}
|
||||
|
||||
ProtectedImage.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
src: PropTypes.string,
|
||||
}
|
||||
|
||||
export default ProtectedImage
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
|
|||
import styled from 'styled-components'
|
||||
import { Query } from 'react-apollo'
|
||||
import gql from 'graphql-tag'
|
||||
import { SidebarItem } from './SidebarItem'
|
||||
import SidebarItem from './SidebarItem'
|
||||
import { Loader } from 'semantic-ui-react'
|
||||
import ProtectedImage from '../photoGallery/ProtectedImage'
|
||||
import SidebarShare from './Sharing'
|
||||
|
@ -78,32 +78,28 @@ class PhotoSidebar extends Component {
|
|||
const { photo } = data
|
||||
let exifItems = []
|
||||
|
||||
if (data.photo) {
|
||||
if (photo.exif) {
|
||||
let exifKeys = Object.keys(photo.exif).filter(
|
||||
x => !!photo.exif[x] && x != '__typename'
|
||||
)
|
||||
if (photo && photo.exif) {
|
||||
let exifKeys = Object.keys(photo.exif).filter(
|
||||
x => !!photo.exif[x] && x != '__typename'
|
||||
)
|
||||
|
||||
let exif = exifKeys.reduce(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr]: photo.exif[curr],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
let exif = exifKeys.reduce(
|
||||
(prev, curr) => ({
|
||||
...prev,
|
||||
[curr]: photo.exif[curr],
|
||||
}),
|
||||
{}
|
||||
)
|
||||
|
||||
exif.dateShot = new Date(
|
||||
exif.dateShot.formatted
|
||||
).toLocaleString()
|
||||
exif.dateShot = new Date(exif.dateShot.formatted).toLocaleString()
|
||||
|
||||
exifItems = exifKeys.map(key => (
|
||||
<SidebarItem
|
||||
key={key}
|
||||
name={exifNameLookup[key]}
|
||||
value={exif[key]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
exifItems = exifKeys.map(key => (
|
||||
<SidebarItem
|
||||
key={key}
|
||||
name={exifNameLookup[key]}
|
||||
value={exif[key]}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -125,7 +121,7 @@ class PhotoSidebar extends Component {
|
|||
}
|
||||
|
||||
PhotoSidebar.propTypes = {
|
||||
imageId: PropTypes.string,
|
||||
imageId: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
export default PhotoSidebar
|
||||
|
|
|
@ -25,7 +25,7 @@ const SidebarItem = ({ name, value }) => (
|
|||
|
||||
SidebarItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
value: PropTypes.any.isRequired,
|
||||
}
|
||||
|
||||
export default SidebarItem
|
||||
|
|
Loading…
Reference in New Issue