diff --git a/Dockerfile b/Dockerfile index a50b8fb..80a7490 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,3 +21,4 @@ ENV AUTH_ENABLED "true" ENV AUTH_SECRET "\$2a\$14\$qRW8no8UDmSwIWM6KHwdRe1j/LMrxoP4NSM756RVodqeUq5HzG6t." ENV PUBLIC_URL "https://localhost" ENV APP_TITLE "Erin - TikTok feed for your own clips" +ENV AUTOPLAY_ENABLED "false" diff --git a/README.md b/README.md index b485110..8c76a28 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Erin has all these features implemented : - Display your own videos using TikTok's swipe feed - Mask the videos you don't want to see in your feed\* - Choose which feed you want to play\*\* +- Autoplay your feed without even swiping - Simple lazy-loading mechanism for your videos - Automatic clip naming based on file name - Simple and optional security using a master password @@ -101,6 +102,7 @@ To run Erin, you will need to set the following environment variables in a `.env | `AUTH_ENABLED` | `string` | Whether Basic Authentication should be enabled. (This parameter is case sensitive) (Possible values : true, false) | true | | `AUTH_SECRET` | `string` | The secure hash of the password used to protect your instance of Erin. | Hash of `secure-password` | | `APP_TITLE` | `string` | The custom title that you would like to display in the browser's tab. (Tip: You can use `[VIDEO_TITLE]` here if you want Erin to dynamically display the title of the current video.) | Erin - TikTok feed for your own clips | +| `AUTOPLAY_ENABLED` | `boolean` | Whether autoplay should be enabled. (This parameter is case sensitive) (Possible values : true, false) | false | > **Tip :** To generate a secure hash for your instance, use the following command : diff --git a/docker/Caddyfile b/docker/Caddyfile index f02d91b..ef3d20b 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -72,6 +72,7 @@ replace __PUBLIC_URL__ {$PUBLIC_URL} replace __USE_SECRET__ {$AUTH_ENABLED} replace __APP_TITLE__ "{$APP_TITLE}" + replace __AUTOPLAY_ENABLED__ {$AUTOPLAY_ENABLED} } # 404 Redirect diff --git a/public/index.html b/public/index.html index 3db8271..79e3b7d 100644 --- a/public/index.html +++ b/public/index.html @@ -25,6 +25,7 @@ window.USE_SECRET = __USE_SECRET__; if (window.location.protocol === "https:") window.PUBLIC_URL = window.PUBLIC_URL.replace("http:", "https:"); + window.AUTOPLAY_ENABLED = __AUTOPLAY_ENABLED__; diff --git a/src/App.js b/src/App.js index 41c6a65..68d42e9 100644 --- a/src/App.js +++ b/src/App.js @@ -123,6 +123,10 @@ const App = () => { _updatePageTitle(v); setCurrentVideoIndex(i); }, []); // eslint-disable-line react-hooks/exhaustive-deps + const handleVideoFinish = () => { + if (!window.AUTOPLAY_ENABLED) return; + document.querySelector(".feed").scrollBy({ top: 1, left: 0, behavior: "smooth" }); + }; // Member - Trick to trigger state updates on localStorage updates const [blackListUpdater, setBlacklistUpdater] = useState(0); @@ -331,6 +335,7 @@ const App = () => { jumpBackForward={previousVideoIndex !== visibleVideos.length && currentVideoIndex > 1} videos={visibleVideos} onFocusVideo={handleVideoFocus} + onFinishVideo={handleVideoFinish} /> ); }, diff --git a/src/components/VideoFeed/index.jsx b/src/components/VideoFeed/index.jsx index f73fea1..9605db6 100644 --- a/src/components/VideoFeed/index.jsx +++ b/src/components/VideoFeed/index.jsx @@ -2,7 +2,14 @@ import { useEffect, useRef } from "react"; import VideoCard from "../VideoCard"; import "./index.css"; -const VideoFeed = ({ videos, initialIndex, jumpToEnd, jumpBackForward, onFocusVideo }) => { +const VideoFeed = ({ + videos, + initialIndex, + jumpToEnd, + jumpBackForward, + onFocusVideo, + onFinishVideo, +}) => { // Member - Track the currently-visible video (0-indexed) const currentVideoIndex = useRef(0); @@ -21,8 +28,21 @@ const VideoFeed = ({ videos, initialIndex, jumpToEnd, jumpBackForward, onFocusVi // Member - Number of videos to load at a time const _bufferSize = 3; + // Member - Prevent double jump forward const hasJumpedForward = useRef(false); + // Mechanism - On video end, call a listener to trigger autoscroll + autoplay if enabled + const throttle = useRef(false); + const handleVideoTimeUpdate = (e) => { + if (e.target.currentTime === 0 && throttle.current === true) { + throttle.current = false; + onFinishVideo(); + setTimeout(() => { + throttle.current = true; + }, 500); + } + }; + // Hook - On mount - Set the current scroll useEffect(() => { if (jumpToEnd) return feedRef.current.scrollBy({ top: 1, left: 0 }); @@ -58,9 +78,13 @@ const VideoFeed = ({ videos, initialIndex, jumpToEnd, jumpBackForward, onFocusVi visibleIndex = currentIndex; videoElement.play().catch((_) => {}); onFocusVideo(videos[currentIndex], currentIndex); + videoElement.addEventListener("timeupdate", handleVideoTimeUpdate, true); } // Case when a video is off-screen or being scrolled in / out of the screen - else videoElement.pause(); + else { + videoElement.pause(); + videoElement.removeEventListener("timeupdate", handleVideoTimeUpdate, true); + } }); if (visibleIndex === false) return; @@ -135,6 +159,13 @@ const VideoFeed = ({ videos, initialIndex, jumpToEnd, jumpBackForward, onFocusVi }; }, [videos]); // eslint-disable-line react-hooks/exhaustive-deps + // Hook - On mount - Set our autoplay flag to ready + useEffect(() => { + setTimeout(() => { + throttle.current = true; + }, 500); + }, []); + initialIndex = _bufferSize === videos.length ? 0 : initialIndex; return (