1
Fork 0

Add present animations

This commit is contained in:
viktorstrate 2019-08-16 22:31:49 +02:00
parent 6d998a392f
commit 52d59b965b
10 changed files with 247 additions and 214 deletions

View File

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

6
ui/package-lock.json generated
View File

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

View File

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

View File

@ -52,7 +52,7 @@ const AuthorizedRoute = ({ component: Component, admin = false, ...props }) => {
}
AuthorizedRoute.propTypes = {
component: PropTypes.element.isRequired,
component: PropTypes.object.isRequired,
admin: PropTypes.bool,
}

View File

@ -67,7 +67,7 @@ const SideButton = props => {
}
SideButton.propTypes = {
children: PropTypes.element,
children: PropTypes.any,
}
const SideButtonLabel = styled.div`

View File

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

View File

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

View File

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

View File

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

View File

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