Merge pull request #25 from ismaelpadilla/feat_24

Feat 24
This commit is contained in:
Ismael Padilla 2022-03-14 21:31:08 -03:00 committed by GitHub
commit db18936172
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 17202 additions and 219 deletions

16951
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "reddit-slideshow",
"version": "0.1.4",
"version": "0.1.5",
"private": true,
"dependencies": {
"axios": "^0.19.2",

View File

@ -1,15 +1,16 @@
import React from 'react';
import React from "react";
import Slide from '../Slide/Slide';
import './Layout.css';
import { ReactComponent as PrevSlideSVG } from '../../assets/prevSlide-24px.svg';
import { ReactComponent as NextSlideSVG } from '../../assets/nextSlide-24px.svg';
import { ReactComponent as LeftArrowSVG } from '../../assets/leftArrow-24px.svg';
import { ReactComponent as RightArrowSVG } from '../../assets/rightArrow-24px.svg';
import { ReactComponent as GithubLogo } from '../../assets/githubLogo.svg';
import Slide from "../Slide/Slide";
import "./Layout.css";
import { ReactComponent as PrevSlideSVG } from "../../assets/prevSlide-24px.svg";
import { ReactComponent as NextSlideSVG } from "../../assets/nextSlide-24px.svg";
import { ReactComponent as LeftArrowSVG } from "../../assets/leftArrow-24px.svg";
import { ReactComponent as RightArrowSVG } from "../../assets/rightArrow-24px.svg";
import { ReactComponent as GithubLogo } from "../../assets/githubLogo.svg";
const layout = (props) => {
const link = props.post ? props.post.link : null;
const extension = props.post ? props.post.extension : null;
const id = props.post ? props.post.id : null;
const title = props.post ? props.post.title : null;
const urlToComments = props.post ? props.post.urlToComments : null;
@ -19,67 +20,119 @@ const layout = (props) => {
// console.log('link', link);
// console.log('post', props.post);
const headerClasses = [ props.showTitle ? 'showTitle' : 'hideTitle'];
const headerClasses = [props.showTitle ? "showTitle" : "hideTitle"];
const infoClasses = ['info'];
infoClasses.push( props.showInfo ? 'showInfo' : 'hideInfo' );
const infoClasses = ["info"];
infoClasses.push(props.showInfo ? "showInfo" : "hideInfo");
const uiClasses = ['ui'];
uiClasses.push( props.hideUI ? 'hideUI' : 'showUI' );
const uiClasses = ["ui"];
uiClasses.push(props.hideUI ? "hideUI" : "showUI");
const slideClasses = ['slide'];
if ( props.hideUI ) {
slideClasses.push('hideCursor');
const slideClasses = ["slide"];
if (props.hideUI) {
slideClasses.push("hideCursor");
}
const currentEndedPlayingHandler = () => {
props.currentEndedPlaying();
};
return (
<div className="layout" onTouchStart={props.touchStart} onTouchEnd={props.touchEnd}>
<div className={ uiClasses.join(' ') }>
<header className={headerClasses.join(' ')}>
<h1 className="title">
{title}
</h1>
{ props.showTitle
? <LeftArrowSVG aria-label="Hide title" className="toggleTitleButton toggleButton" onClick={props.titleClick}/>
: <RightArrowSVG aria-label="Show title" className="toggleTitleButton toggleButton" onClick={props.titleClick}/> }
<div className={uiClasses.join(" ")}>
<header className={headerClasses.join(" ")}>
<h1 className="title">{title}</h1>
{props.showTitle ? (
<LeftArrowSVG
aria-label="Hide title"
className="toggleTitleButton toggleButton"
onClick={props.titleClick}
/>
) : (
<RightArrowSVG
aria-label="Show title"
className="toggleTitleButton toggleButton"
onClick={props.titleClick}
/>
)}
</header>
<NextSlideSVG aria-label="Next" className="navButton nextButton" onClick={ props.nextHandler }/>
<PrevSlideSVG aria-label="Previous" className="navButton prevButton" onClick={ props.prevHandler }/>
<NextSlideSVG aria-label="Next" className="navButton nextButton" onClick={props.nextHandler} />
<PrevSlideSVG aria-label="Previous" className="navButton prevButton" onClick={props.prevHandler} />
<div className={infoClasses.join(' ')}>
<div className={infoClasses.join(" ")}>
<div className="infoButtons">
<div className="infoRow">
<a target="_blank" className="infoElement" href= { urlToComments } rel="noopener noreferrer">Comments</a>
<a target="_blank" className="infoElement" href= { link } rel="noopener noreferrer">Direct link</a>
<a target="_blank" className="infoElement" href={urlToComments} rel="noopener noreferrer">
Comments
</a>
<a target="_blank" className="infoElement" href={link} rel="noopener noreferrer">
Direct link
</a>
{/*<div className="infoElement">
<input type="checkbox" id="showNSFW" name="showNSFW" onChange={props.nsfwCheckboxHandler} checked={props.nsfwChecked}/><label htmlFor="showNSFW">NSFW</label>
</div>*/}
</div>
<div className="infoRow">
<div className="infoElement">
<input type="checkbox" id="auto" name="auto" onChange={props.autoCheckboxHandler} checked={props.autoPlay}/>
<input
type="checkbox"
id="auto"
name="auto"
onChange={props.autoCheckboxHandler}
checked={props.autoPlay}
/>
<label htmlFor="auto">Auto</label>
</div>
<div className="infoElement">
<input type="checkbox" id="hideUI" name="hideUI" onChange={props.hideUICheckboxHandler} checked={props.hideUIChecked}/>
<input
type="checkbox"
id="hideUI"
name="hideUI"
onChange={props.hideUICheckboxHandler}
checked={props.hideUIChecked}
/>
<label htmlFor="hideUI">Hide UI</label>
</div>
</div>
</div>
{ props.showInfo
? <LeftArrowSVG aria-label="Hide info panel" className="toggleInfoButton toggleButton" onClick={props.infoClick}/>
: <RightArrowSVG aria-label="Show info panel" className="toggleInfoButton toggleButton" onClick={props.infoClick}/> }
{props.showInfo ? (
<LeftArrowSVG
aria-label="Hide info panel"
className="toggleInfoButton toggleButton"
onClick={props.infoClick}
/>
) : (
<RightArrowSVG
aria-label="Show info panel"
className="toggleInfoButton toggleButton"
onClick={props.infoClick}
/>
)}
</div>
<a href="https://github.com/ismaelpadilla/reddit-slideshow" aria-label="Source code" target="_blank" rel="noopener noreferrer">
<GithubLogo className="githubLogo"/>
<a
href="https://github.com/ismaelpadilla/reddit-slideshow"
aria-label="Source code"
target="_blank"
rel="noopener noreferrer"
>
<GithubLogo className="githubLogo" />
</a>
</div>
{ link ? <Slide url={ link } classes={ slideClasses.concat(['current']) } key={ id }/> : null }
{ props.prev !== props.post ? <Slide url={ prevLink } classes={ slideClasses.concat(['prev']) } key={ prevId } /> : null }
{link ? (
<Slide
url={link}
extension={extension}
classes={slideClasses.concat(["current"])}
key={id}
currentEndedPLaying={currentEndedPlayingHandler}
/>
) : null}
{props.prev !== props.post ? (
<Slide url={prevLink} extension={extension} classes={slideClasses.concat(["prev"])} key={prevId} />
) : null}
</div>
);
};

View File

@ -1,49 +1,48 @@
import React from 'react';
import React, { useState } from "react";
import './Slide.css'
import "./Slide.css";
const slide = (props) => {
const Slide = (props) => {
// how many time has the video played?
const [loopCount, setLoopCount] = useState(0);
// get file extension
const extPattern = /\.[0-9a-z]+$/i;
const match = props.url.match(extPattern);
const fileExt = match ? match[0] : null;
if (props.classes.includes('current')) {
const fileExt = props.extension;
if (props.classes.includes("current")) {
console.log("Attempting to show", props.url);
}
// console.log (props.classes);
// console.log('extension', fileExt);
// console.log('domain', domain);
// imgur urls are case sensitive!
// const afterDomain = props.url.match(/[.0-9a-zA-Z]+$/)[0];
// console.log('afterdomain', afterDomain);
const endedHandler = () => {
if (props.classes.includes("current")) {
if (loopCount === 1 && props.currentEndedPLaying) {
props.currentEndedPLaying();
}
setLoopCount(loopCount + 1);
}
};
if (fileExt) {
// Imgur videos can be linked by using .mp4 extension instead of .gifv
if ( fileExt === ".gifv" ) {
if (fileExt === ".gifv") {
return (
<video autoPlay muted loop className= { props.classes.join(' ') + ' video' }>
<source src={ props.url.replace(".gifv", ".mp4") } type="video/mp4"/>
<video autoPlay loop muted className={props.classes.join(" ") + " video"} onPlaying={endedHandler}>
<source src={props.url.replace(".gifv", ".mp4")} type="video/mp4" />
</video>
);
} else if ( fileExt === ".mp4" ) {
} else if (fileExt === ".mp4") {
return (
<video autoPlay muted loop className= { props.classes.join(' ') + ' video' }>
<source src={ props.url.replace('.mp4', '.webm') } type="video/webm"/>
<source src={ props.url } type="video/mp4"/>
<video autoPlay loop muted className={props.classes.join(" ") + " video"} onPlaying={endedHandler}>
<source src={props.url.replace(".mp4", ".webm")} type="video/webm" />
<source src={props.url} type="video/mp4" />
</video>
);
}
// Regular picture
const mystyle= { backgroundImage:`url(${props.url})` };
return <div style={ mystyle } className= { props.classes.join(' ') } />;
const mystyle = { backgroundImage: `url(${props.url})` };
return <div style={mystyle} className={props.classes.join(" ")} />;
}
return(
<div>Something went wrong.</div>
);
return <div>Something went wrong.</div>;
};
export default slide;
export default Slide;

View File

@ -1,18 +1,19 @@
import React, { Component } from 'react';
import axios from 'axios';
import React, { Component } from "react";
import axios from "axios";
import Layout from '../components/Layout/Layout';
import './App.css';
import Layout from "../components/Layout/Layout";
import "./App.css";
class App extends Component {
hideUI = false;
state = {
// request: '/r/pics.json',
request: window.location.pathname + '.json',
request: window.location.pathname + ".json",
posts: [],
currentPost: -1,
prevPost: 0,
after: '', // to be sent as a part of a request, see reddit api docs
currentEndedPlaying: false,
after: "", // to be sent as a part of a request, see reddit api docs
awaitingResponse: false,
showTitle: true, // show image title at top left
showInfo: true, // show info and buttons at bottom right
@ -20,39 +21,39 @@ class App extends Component {
hideUI: this.hideUI,
hideUIChecked: this.hideUI,
showNSWF: true,
touchStartX: 0 // used for swipe gestures in mobile
touchStartX: 0, // used for swipe gestures in mobile
};
/**
* This makes sure we access the right reddit url based on how the user
* accesses our website.
*/
componentDidMount = () => {
// Get initial posts via http, set state
if ( !this.state.awaitingResponse ) {
this.setState( { awaitingResponse: true } );
axios.get( this.state.request )
.then(
response => {
if (!this.state.awaitingResponse) {
this.setState({ awaitingResponse: true });
axios
.get(this.state.request)
.then((response) => {
// posts are in response.data.data.children
const after = response.data.data.after;
this.getPosts(response.data.data.children).then ( (posts) =>
{
this.getPosts(response.data.data.children)
.then((posts) => {
// console.log('after', after);
// console.log('reduced posts', posts);
this.setState( {
this.setState({
posts: [...posts],
after: after,
currentPost: 0,
awaitingResponse: false
awaitingResponse: false,
// interval: setInterval(this.nextSlideHandler, 2000)
});
if (this.state.auto) {
this.setState({ interval: setInterval(this.nextSlideHandler, 5000) });
this.setState({ interval: setInterval(() => this.nextSlideHandler(false), 5000) });
}
// console.log("initialized state", this.state);
}).catch(function (error) {
})
.catch(function (error) {
console.log(error);
});
})
@ -60,9 +61,9 @@ class App extends Component {
console.log(error);
});
}
document.addEventListener('mousemove', this.mouseMoveHandler);
document.addEventListener('keydown', this.keyDownHandler);
}
document.addEventListener("mousemove", this.mouseMoveHandler);
document.addEventListener("keydown", this.keyDownHandler);
};
/**
* Prevent unnecessary rerenders.
@ -73,11 +74,11 @@ class App extends Component {
* the touch start event modifies the state but it doesn't have an impact
* on the elements being shown.
*/
if (this.state.touchStartX !== nextState.touchStartX){
if (this.state.touchStartX !== nextState.touchStartX) {
return false;
}
return true;
}
};
/**
* Handle key presses.
@ -87,9 +88,9 @@ class App extends Component {
if (event.key === "ArrowLeft" || event.code === "KeyA") {
this.prevSlideHandler();
} else if (event.key === "ArrowRight" || event.code === "KeyD") {
this.nextSlideHandler();
}
this.nextSlideHandler(true);
}
};
/**
* Get posts from a server response
@ -104,7 +105,7 @@ class App extends Component {
*/
getPosts = (children) => {
// Filter and map at the same time
const posts = children.reduce( this.filterPosts, [] );
const posts = children.reduce(this.filterPosts, []);
// Update the posts, get urls to gfycat mp4s (see updatePosts fore more info).
const updatedPosts = this.updatePosts(posts);
// console.log('updated posts', updatedPosts);
@ -125,8 +126,9 @@ class App extends Component {
title: child.data.title,
id: child.data.id,
nsfw: child.data.over_18,
urlToComments: 'https://www.reddit.com' + child.data.permalink,
link: filteredUrl
urlToComments: "https://www.reddit.com" + child.data.permalink,
link: filteredUrl.url,
extension: filteredUrl.extension,
});
}
}
@ -147,12 +149,12 @@ class App extends Component {
const extPattern = /\.[0-9a-z]+$/i;
const match = url.match(extPattern);
const fileExt = match ? match[0] : null;
const supportedExtensions = [".jpg", ".jpeg", ".png", ".bmp",".gif", ".gifv"];
if ( domain === "gfycat.com" || supportedExtensions.includes(fileExt) ) {
return url;
const supportedExtensions = [".jpg", ".jpeg", ".png", ".bmp", ".gif", ".gifv"];
if (domain === "gfycat.com" || supportedExtensions.includes(fileExt)) {
return { url, extension: fileExt };
}
if ( domain === "imgur.com" ) {
return url + ".jpg";
if (domain === "imgur.com") {
return { url: url + ".jpg", extension: fileExt };
}
return null;
};
@ -174,7 +176,7 @@ class App extends Component {
const domain = post.link.match(/:\/\/(.+)\//)[1];
// In order to play gfycat videos we must get the mp4 url from the video id
if (domain === 'gfycat.com') {
if (domain === "gfycat.com") {
const gfyId = post.link.match(/([.0-9a-zA-Z]+)(-|$)/)[1];
/*
* gfyId regex clarification: sometimes gfys have dashes ('-') in their url.
@ -182,20 +184,31 @@ class App extends Component {
*/
// Wait until some images are correct so we can render something
if ( counter < 3) {
const response = await axios.get( "https://api.gfycat.com/v1/gfycats/" + gfyId );
if (counter < 3) {
const response = await axios.get("https://api.gfycat.com/v1/gfycats/" + gfyId);
post.link = response.data.gfyItem.mp4Url;
} else {
axios.get( "https://api.gfycat.com/v1/gfycats/" + gfyId ).then( (response) =>
{
axios
.get("https://api.gfycat.com/v1/gfycats/" + gfyId)
.then((response) => {
post.link = response.data.gfyItem.mp4Url;
}).catch(function (error) {
})
.catch(function (error) {
console.log(error);
});
}
const extPattern = /\.[0-9a-z]+$/i;
const match = post.link.match(extPattern);
const fileExt = match ? match[0] : null;
post.extension = fileExt;
if (fileExt === ".gifv" || fileExt === ".mp4") {
post.isVideo = true;
} else {
post.isVideo = false;
}
counter++;
}
};
}
return posts;
};
@ -203,11 +216,11 @@ class App extends Component {
* Go to previous slide.
*/
prevSlideHandler = () => {
if ( this.state.currentPost > 0 ) {
this.setState( (prevState, props) => {
if (this.state.currentPost > 0) {
this.setState((prevState, props) => {
return {
prevPost: prevState.currentPost,
currentPost: prevState.currentPost -1
currentPost: prevState.currentPost - 1,
};
});
}
@ -215,40 +228,50 @@ class App extends Component {
};
/**
* Go to next slide.
* Handle nextSlide event. If it was fired manually, always go to next slide.
* If it was fired automatically and we are playing a video, go to next slide only
* if current video has finished playing.
*/
nextSlideHandler = () => {
if ( this.state.currentPost < this.state.posts.length -1 ) {
this.setState( (prevState, props) => {
nextSlideHandler = (manual, e) => {
// do nothing if current video didn't end playing
const currentIsVideo = this.state.posts[this.state.currentPost].isVideo;
if (!manual && currentIsVideo && !this.state.currentEndedPlaying) {
console.log("Current video still playing!");
return;
}
if (this.state.currentPost < this.state.posts.length - 1) {
this.setState((prevState, props) => {
return {
prevPost: prevState.currentPost,
currentPost: prevState.currentPost +1
currentPost: prevState.currentPost + 1,
};
});
};
}
// Load more posts when close to reaching the end
if ( this.state.currentPost > this.state.posts.length -4 ) {
if ( !this.state.awaitingResponse ) {
this.setState( { awaitingResponse: true } );
console.log('fetching aditional posts');
axios.get(this.state.request + '?after=' + this.state.after)
.then(
response => {
if (this.state.currentPost > this.state.posts.length - 4) {
if (!this.state.awaitingResponse) {
this.setState({ awaitingResponse: true });
console.log("fetching aditional posts");
axios
.get(this.state.request + "?after=" + this.state.after)
.then((response) => {
// console.log(response); // posts are in response.data.data.children
const after = response.data.data.after;
this.getPosts(response.data.data.children).then ( (posts) =>
{
this.getPosts(response.data.data.children)
.then((posts) => {
// console.log('after', after);
// console.log('reduced posts', posts);
this.setState( (state, props) => {
this.setState((state, props) => {
return {
posts: [...state.posts, ...posts],
after: after,
awaitingResponse: false
}
awaitingResponse: false,
};
});
}).catch(function (error) {
})
.catch(function (error) {
console.log(error);
});
})
@ -257,6 +280,7 @@ class App extends Component {
});
}
}
this.setState({ currentEndedPlaying: false });
console.log("Next");
};
@ -264,23 +288,23 @@ class App extends Component {
* Hide/show title at the top right.
*/
toggleTitleHandler = () => {
this.setState( (state,props) => {
this.setState((state, props) => {
return {
showTitle: ! state.showTitle
}
} );
showTitle: !state.showTitle,
};
});
};
/**
* Hide/show info panel at the bottom left.
*/
toggleInfoHandler = () => {
console.log('toggle info');
this.setState( (state,props) => {
console.log("toggle info");
this.setState((state, props) => {
return {
showInfo: ! state.showInfo
}
} );
showInfo: !state.showInfo,
};
});
};
/**
@ -290,11 +314,11 @@ class App extends Component {
if (event.target.checked) {
this.setState({
auto: true,
interval: setInterval(this.nextSlideHandler, 5000)
interval: setInterval(() => this.nextSlideHandler(false), 5000),
});
} else {
this.setState({ auto: false });
clearInterval( this.state.interval );
clearInterval(this.state.interval);
}
};
@ -305,14 +329,14 @@ class App extends Component {
if (event.target.checked) {
this.setState({
hideUI: true,
hideUIChecked: true
hideUIChecked: true,
});
} else {
this.setState({
hideUI: false,
hideUIChecked: false
hideUIChecked: false,
});
clearInterval( this.state.interval );
clearInterval(this.state.interval);
}
};
@ -321,7 +345,7 @@ class App extends Component {
*/
nsfwCheckboxHandler = (event) => {
this.setState({ showNSWF: event.target.checked });
}
};
/**
* Handle mouse movement.
@ -329,10 +353,10 @@ class App extends Component {
*/
mouseMoveHandler = () => {
if (this.state.hideUI) {
this.setState({ hideUI:false });
setTimeout( this.hideUI, 3000);
}
this.setState({ hideUI: false });
setTimeout(this.hideUI, 3000);
}
};
/**
* Hide UI, this method should be called from the timer created in
@ -341,7 +365,7 @@ class App extends Component {
* after moving the mouse so as not to hide the UI in that case.
*/
hideUI = () => {
if(this.state.hideUIChecked) {
if (this.state.hideUIChecked) {
this.setState({ hideUI: true });
}
};
@ -352,45 +376,51 @@ class App extends Component {
*/
touchStartHandler = (event) => {
this.setState({ touchStartX: event.targetTouches[0].clientX });
}
};
/**
* On touch end, compare touch position to touch start (saved in state).
* Change slides if both points are far enough.
*/
touchEndHandler = (event) => {
if ( event.changedTouches[0].clientX < this.state.touchStartX - 50 ) {
this.nextSlideHandler();
} else if ( event.changedTouches[0].clientX > this.state.touchStartX + 50) {
if (event.changedTouches[0].clientX < this.state.touchStartX - 50) {
this.nextSlideHandler(true);
} else if (event.changedTouches[0].clientX > this.state.touchStartX + 50) {
this.prevSlideHandler();
}
}
};
currentEndedPlayingHandler = () => {
this.setState({ currentEndedPlaying: true });
};
render() {
return (
<div className="App">
<Layout className="App"
post={ this.state.posts.length ? (this.state.posts[ this.state.currentPost ]) : null }
prev={ this.state.posts.length ? (this.state.posts[ this.state.prevPost ]) : null }
prevHandler={ this.prevSlideHandler }
nextHandler={ this.nextSlideHandler }
showTitle={ this.state.showTitle }
showInfo={ this.state.showInfo }
titleClick={ this.toggleTitleHandler }
infoClick={ this.toggleInfoHandler }
autoPlay={ this.state.auto }
autoCheckboxHandler={ this.autoCheckboxHandler }
hideUI={ this.state.hideUI }
hideUIChecked={ this.state.hideUIChecked }
hideUICheckboxHandler={ this.hideUICheckboxHandler }
nsfwCheckboxHandler={ this.nsfwCheckboxHandler }
nsfwChecked={ this.state.showNSWF }
touchStart={ this.touchStartHandler }
touchEnd={ this.touchEndHandler }
<Layout
className="App"
post={this.state.posts.length ? this.state.posts[this.state.currentPost] : null}
prev={this.state.posts.length ? this.state.posts[this.state.prevPost] : null}
prevHandler={this.prevSlideHandler}
nextHandler={(e) => this.nextSlideHandler(true, e)}
showTitle={this.state.showTitle}
showInfo={this.state.showInfo}
titleClick={this.toggleTitleHandler}
infoClick={this.toggleInfoHandler}
autoPlay={this.state.auto}
autoCheckboxHandler={this.autoCheckboxHandler}
hideUI={this.state.hideUI}
hideUIChecked={this.state.hideUIChecked}
hideUICheckboxHandler={this.hideUICheckboxHandler}
nsfwCheckboxHandler={this.nsfwCheckboxHandler}
nsfwChecked={this.state.showNSWF}
touchStart={this.touchStartHandler}
touchEnd={this.touchEndHandler}
currentEndedPlaying={this.currentEndedPlayingHandler}
/>
</div>
);
};
}
}
export default App;