Compare commits
5 Commits
669a129f21
...
e3182d4cf2
| Author | SHA1 | Date | |
|---|---|---|---|
| e3182d4cf2 | |||
| 9f9c3cc00c | |||
| 0dc3c5edef | |||
| 62ae230f70 | |||
| 49695dd393 |
@ -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>
|
||||||
|
|||||||
@ -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": ".",
|
||||||
|
|||||||
@ -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%;
|
||||||
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user