Compare commits

..

5 Commits

5 changed files with 110 additions and 98 deletions

View File

@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="https://static.xinu.tv/favicon/gallery.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Photo gallery @ xinu.tv"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="https://static.xinu.tv/favicon/gallery.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Xinu Slideshow</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View File

@ -3,19 +3,8 @@
"name": "Create React App Sample", "name": "Create React App Sample",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "https://static.xinu.tv/favicon/gallery.png",
"sizes": "64x64 32x32 24x24 16x16", "type": "image/png"
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
} }
], ],
"start_url": ".", "start_url": ".",

View File

@ -3,7 +3,7 @@ body, html, #root {
} }
#ui { #ui {
background-color: white; background-color: rgba(255, 255, 255, 0.5);
bottom: 0; bottom: 0;
line-height: 3em; line-height: 3em;
position: 'absolute'; position: 'absolute';
@ -13,3 +13,7 @@ body, html, #root {
#ui .meta { #ui .meta {
text-align: center; text-align: center;
} }
#slide {
height: 100%;
}

View File

@ -51,6 +51,43 @@ function shuffle(a: Array<MediaItem>) {
return a; 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<MediaItem>;
nextSlide?: Slide;
prevSlide?: Slide;
constructor(items: Array<MediaItem>) {
this.items = items;
}
render() {
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}`);
let style: React.CSSProperties = {
height: '100%',
width: '100%',
backgroundColor: 'black',
// TODO(wathiede): make this handle multiple items.
backgroundImage: `url(/api/image/${this.items[0].id}?w=${w}&h=${h})`,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center center',
backgroundSize: 'cover',
};
return <div style={style}></div>
}
};
type MediaMetadata = { type MediaMetadata = {
width: number; width: number;
height: number; height: number;
@ -67,9 +104,8 @@ type AlbumProps = {
}; };
type AlbumState = { type AlbumState = {
error: any; error: any;
// TODO(wathiede): define a MediaItem type.
mediaItems: Array<MediaItem> | null; mediaItems: Array<MediaItem> | null;
idx: number; curSlide?: Slide,
showUI: boolean; showUI: boolean;
timerID: any | null; timerID: any | null;
}; };
@ -77,7 +113,6 @@ class Album extends React.Component<AlbumProps, AlbumState> {
state: AlbumState = { state: AlbumState = {
error: null, error: null,
mediaItems: null, mediaItems: null,
idx: 0,
showUI: this.props.showUI, showUI: this.props.showUI,
timerID: null, timerID: null,
}; };
@ -89,49 +124,10 @@ class Album extends React.Component<AlbumProps, AlbumState> {
fetch(process.env.PUBLIC_URL + `/api/album/${album}`) fetch(process.env.PUBLIC_URL + `/api/album/${album}`)
.then(res => res.json()) .then(res => res.json())
.then( .then(
(result) => { (mediaItems: Array<MediaItem>) => {
this.setState({mediaItems: result});
let {sleepTimeSeconds} = this.props;
let timerID = setInterval(()=>{
let {idx} = this.state;
this.setState({idx: idx+1})
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.
// TODO(wathiede): fetch an image that maintains the originals aspect ratio
let w = window.innerWidth * window.devicePixelRatio; let w = window.innerWidth * window.devicePixelRatio;
let h = window.innerHeight * window.devicePixelRatio; let h = window.innerHeight * window.devicePixelRatio;
let ratio = w/h; 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}`);
//let w = roundup(window.innerWidth*window.devicePixelRatio, IMAGE_CHUNK);
//let h = roundup(window.innerHeight*window.devicePixelRatio, IMAGE_CHUNK);
let {idx, error, mediaItems, showUI} = this.state;
if (error !== null) {
return <h2>Error: {JSON.stringify(error)}</h2>;
} else if (mediaItems !== null) {
let landscapes = mediaItems.filter((mi) => { let landscapes = mediaItems.filter((mi) => {
let md = mi.mediaMetadata; let md = mi.mediaMetadata;
let ratio = md.width/md.height; let ratio = md.width/md.height;
@ -155,28 +151,51 @@ class Album extends React.Component<AlbumProps, AlbumState> {
photos = portraits; photos = portraits;
} }
photos = shuffle(photos); 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];
})
let numImages = photos.length; this.setState({curSlide: slides[0]});
idx = idx % numImages; let {sleepTimeSeconds} = this.props;
let nextIdx = (idx+1)%numImages; let timerID = setInterval(()=>{
let prevIdx = (numImages+idx-1)%numImages; let {curSlide} = this.state;
let image = photos[idx]; this.setState({curSlide: curSlide?.nextSlide})
let nextImage = photos[nextIdx]; console.log('timer fired');
let prevImage = photos[prevIdx]; }, sleepTimeSeconds*1000);
let style: React.CSSProperties = { this.setState({timerID});
height: '100%', },
width: '100%', (error) => this.setState({error}),
backgroundColor: 'black', );
backgroundImage: `url(/api/image/${image.id}?w=${w}&h=${h})`, }
backgroundRepeat: 'no-repeat', componentWillUnmount() {
backgroundPosition: 'center center', let {timerID} = this.state;
backgroundSize: 'cover', clearInterval(timerID);
}; }
nextPhoto() {
}
render() {
// TODO(wathiede): fade transition.
// TODO(wathiede): pair-up portrait orientation images.
let {curSlide, error, mediaItems, showUI} = this.state;
if (error !== null) {
return <h2>Error: {JSON.stringify(error)}</h2>;
} else if (curSlide) {
let nextSlide = curSlide?.nextSlide;
let prevSlide = curSlide?.prevSlide;
let prefetchStyle: React.CSSProperties = { let prefetchStyle: React.CSSProperties = {
backgroundColor: 'rgba(127, 127, 127, 0.5)',
backgroundPosition: 'center center',
bottom: 0,
height: '25%',
position: 'absolute', position: 'absolute',
width: '25%', width: '25%',
height: '25%',
bottom: 0,
}; };
let leftPrefetchStyle: React.CSSProperties = { let leftPrefetchStyle: React.CSSProperties = {
left: 0, left: 0,
@ -189,27 +208,27 @@ class Album extends React.Component<AlbumProps, AlbumState> {
let ui; let ui;
if (showUI) { if (showUI) {
ui = <div id="ui"> ui = <div id="ui">
<img <div
style={leftPrefetchStyle} style={leftPrefetchStyle}
onClick={(e)=>{ onClick={(e)=>{
e.stopPropagation(); e.stopPropagation();
this.setState({idx: prevIdx}) this.setState({curSlide: curSlide?.prevSlide})
}} }}>{ prevSlide?.render() }</div>
src={`/api/image/${prevImage.id}?w=${w}&h=${h}`} alt="prefetch prev" /> {/* TODO(wathiede): make this work with multiple items. */}
<div className="meta">{image.filename}</div> <div className="meta">{curSlide?.items[0].filename}</div>
<img <div
style={rightPrefetchStyle} style={rightPrefetchStyle}
onClick={(e)=>{ onClick={(e)=>{
e.stopPropagation(); e.stopPropagation();
this.setState({idx: nextIdx}) this.setState({curSlide: curSlide?.nextSlide})
}} }}>{ nextSlide?.render() }</div>
src={`/api/image/${nextImage.id}?w=${w}&h=${h}`} alt="prefetch next" />
</div>; </div>;
} }
return <div style={style} onClick={(e)=>{ return <div id="slide" onClick={(e)=>{
e.stopPropagation(); e.stopPropagation();
this.setState({showUI: !showUI}) this.setState({showUI: !showUI})
}}> }}>
{ curSlide?.render() }
{ ui } { ui }
</div>; </div>;
} else { } else {

View File

@ -1,6 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es5", "target": "es6",
"lib": [ "lib": [
"dom", "dom",
"dom.iterable", "dom.iterable",