import React from 'react'; import { HashRouter as Router, Switch, Route, useParams } from "react-router-dom"; import 'animate.css'; import Random from './rand'; import './App.css'; type Config = { sleepTimeSeconds: number; showUI: boolean; } let CONFIG: Config; if (process.env.NODE_ENV === 'production') { CONFIG = { sleepTimeSeconds: 60, showUI: false, } } else { CONFIG = { sleepTimeSeconds: 10, showUI: true, } } const IMAGE_CHUNK = 256; const roundup = (v: number, mod: number) => { let r = v % mod; if (r === 0) { return v; } else { return v + (mod - r); } }; /** * Shuffles array in place. ES6 version * From https://stackoverflow.com/questions/6274339/how-can-i-shuffle-an-array * @param {Array} a items An array containing the items. */ function shuffle(a: Array) { let rng = new Random(new Date().getDay()); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rng.nextFloat() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } class Slide { // One or two items. For example if display is landscape we'll try to fit // two portrait images and only one landscape. items: Array; nextSlide?: Slide; prevSlide?: Slide; constructor(items: Array) { this.items = items; } prefetchImages() { console.log(`prefetchImages, I have ${this.imageUrls.length} images`); this.imageUrls.map(url => new Image().src = url); } get imageUrls(): Array { let w = window.innerWidth * window.devicePixelRatio; let h = window.innerHeight * window.devicePixelRatio; let ratio = w/h; if (ratio > 1) { // Landscape image w = roundup(w, IMAGE_CHUNK); h = Math.round(w/ratio); } else { // Portrait image h = roundup(h, IMAGE_CHUNK); w = Math.round(h/ratio); } //console.log(`Window size ${window.innerWidth}x${window.innerHeight} with a devicePixelRatio of ${window.devicePixelRatio} for a total size of ${w}x${h}`); return this.items.map(img => `/api/image/${img.id}?w=${w}&h=${h}`); } render() { const imgs = this.imageUrls.map(url => { let style: React.CSSProperties = { height: '100%', width: '100%', backgroundColor: 'black', backgroundImage: `url(${url})`, backgroundRepeat: 'no-repeat', backgroundPosition: 'center center', backgroundSize: 'cover', }; return
; }); // TODO(wathiede): make sure the style handles multiple items. return
{imgs}
; } }; type MediaMetadata = { width: number; height: number; }; type MediaItem = { id: string; mediaMetadata: MediaMetadata; filename: string; }; type AlbumProps = { album: string; showUI: boolean; sleepTimeSeconds: number; }; type AlbumState = { error: any; mediaItems: Array | null; curSlide?: Slide, showUI: boolean; timerID: any | null; }; class Album extends React.Component { state: AlbumState = { error: null, mediaItems: null, showUI: this.props.showUI, timerID: null, }; componentDidMount() { this.loadAlbum() } loadAlbum() { let {album} = this.props; fetch(process.env.PUBLIC_URL + `/api/album/${album}`) .then(res => res.json()) .then( (mediaItems: Array) => { let w = window.innerWidth * window.devicePixelRatio; let h = window.innerHeight * window.devicePixelRatio; let ratio = w/h; let landscapes = mediaItems.filter((mi) => { let md = mi.mediaMetadata; let ratio = md.width/md.height; return ratio > 1; }); let portraits = mediaItems.filter((mi) => { let md = mi.mediaMetadata; let ratio = md.width/md.height; return ratio <= 1; }); console.log(`${landscapes.length} landscape photos`); console.log(`${portraits.length} portraits photos`); let photos; if (ratio > 1) { console.log('display in landscape mode'); photos = landscapes; } else { console.log('display in portrait mode'); photos = portraits; } photos = shuffle(photos); let slides = photos.map((p)=>{ return new Slide([p]); }); let numSlides = slides.length; slides.forEach((p, idx)=>{ let nextIdx = (idx+1)%numSlides; let prevIdx = (numSlides+idx-1)%numSlides; p.nextSlide = slides[nextIdx]; p.prevSlide = slides[prevIdx]; }) this.setState({curSlide: slides[0]}); let {sleepTimeSeconds} = this.props; let timerID = setInterval(()=>{ let {curSlide} = this.state; this.setState({curSlide: curSlide?.nextSlide}) console.log('timer fired'); }, sleepTimeSeconds*1000); this.setState({timerID}); }, (error) => this.setState({error}), ); } componentWillUnmount() { let {timerID} = this.state; clearInterval(timerID); } nextPhoto() { } render() { // TODO(wathiede): fade transition. // TODO(wathiede): pair-up portrait orientation images. let {curSlide, error, showUI} = this.state; if (error !== null) { return

Error: {JSON.stringify(error)}

; } else if (curSlide) { let nextSlide = curSlide?.nextSlide; let prevSlide = curSlide?.prevSlide; let prefetchStyle: React.CSSProperties = { backgroundColor: 'rgba(127, 127, 127, 0.5)', backgroundPosition: 'center center', bottom: 0, height: '25%', position: 'absolute', width: '25%', }; let leftPrefetchStyle: React.CSSProperties = { left: 0, ...prefetchStyle }; let rightPrefetchStyle: React.CSSProperties = { right: 0, ...prefetchStyle }; let ui; if (showUI) { ui =
{ e.stopPropagation(); this.setState({curSlide: curSlide?.prevSlide}) }}>{ prevSlide?.render() }
{/* TODO(wathiede): make this work with multiple items. */}
{curSlide?.items[0].filename}
{ e.stopPropagation(); this.setState({curSlide: curSlide?.nextSlide}) }}>{ nextSlide?.render() }
; } nextSlide?.prefetchImages(); return
{ e.stopPropagation(); this.setState({showUI: !showUI}) }}> { curSlide?.render() } { ui }
; } else { return

Loading...

; } } } type AlbumIndexProps = { }; type AlbumIndexState = { error: any | null, albums: Array | null, }; class AlbumIndex extends React.Component { state: AlbumIndexState = { error: null, albums: null, } componentDidMount() { fetch(process.env.PUBLIC_URL + "/api/albums") .then(res => res.json()) .then( (result) => this.setState({albums: result}), (error) => this.setState({error}), ); } render() { let {error, albums} = this.state; if (error !== null) { return

Error: {JSON.stringify(error)}

; } else if (albums !== null) { return albums.map((a) => { let img = unset; if (a.coverPhotoMediaItemId !== undefined) { img = { } let figure =
{img}
{ a.title || "No title" } - { a.mediaItemsCount || 0 } photos
; return { figure } }); } else { return

Loading...

; } } } type AlbumRouteProps = { sleepTimeSeconds: number, showUI: boolean }; const AlbumRoute = ({sleepTimeSeconds, showUI}: AlbumRouteProps) => { // We can use the `useParams` hook here to access // the dynamic pieces of the URL. let { albumId } = useParams(); albumId = albumId || ''; return ; } const App = () => { let {showUI, sleepTimeSeconds} = CONFIG; return
} export default App;