Merge pull request #24 from JamesHawkinss/feature/continue-watching

Continue watching
This commit is contained in:
James Hawkins 2021-08-07 21:20:33 +01:00 committed by GitHub
commit 903cd633cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 255 additions and 151 deletions

View File

@ -1,7 +1,7 @@
import './index.css';
import { SearchView } from './views/Search'; import { SearchView } from './views/Search';
import { MovieView } from './views/Movie'; import { MovieView } from './views/Movie';
import { useMovie, MovieProvider } from './hooks/useMovie'; import { useMovie, MovieProvider } from './hooks/useMovie';
import './index.css';
function Router() { function Router() {
const { streamData } = useMovie(); const { streamData } = useMovie();

View File

@ -1,16 +0,0 @@
.discordBanner {
margin-top: 0.5rem;
border-inline-start: none;
font-size: 16px;
font-weight: normal;
letter-spacing: -.01em;
padding: .5rem 1rem .5rem .75rem;
border-radius: .25rem;
background-color: var(--button);
color: var(--button-text);
}
.discordBanner a {
color: var(--button-text);
/* text-decoration: none; */
}

View File

@ -1,12 +0,0 @@
import React from 'react';
import './DiscordBanner.css';
export function DiscordBanner() {
return (
<div className="discordBanner">
<a href="https://discord.gg/vXsRvye8BS">
Join our Discord server
</a>
</div>
)
}

View File

@ -3,14 +3,15 @@ import { TypeSelector } from './TypeSelector';
import { NumberSelector } from './NumberSelector'; import { NumberSelector } from './NumberSelector';
import './EpisodeSelector.css' import './EpisodeSelector.css'
export function EpisodeSelector({ setSelectedSeason, setEpisode, seasons, selectedSeason, season, episodes, currentSeason, currentEpisode, source }) { export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode, streamData }) {
const choices = episodes ? episodes.map(v => { const choices = episodes ? episodes.map(v => {
let progressData = JSON.parse(localStorage.getItem('video-progress') || "{}") let progressData = JSON.parse(localStorage.getItem('video-progress') || "{}")
let currentlyAt = 0; let currentlyAt = 0;
let totalDuration = 0; let totalDuration = 0;
const progress = progressData?.[source]?.show?.slug?.[`${season}-${v}`] const progress = progressData?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[`${selectedSeason}-${v}`]
if (progress) { if (progress) {
currentlyAt = progress.currentlyAt currentlyAt = progress.currentlyAt
totalDuration = progress.totalDuration totalDuration = progress.totalDuration
@ -27,7 +28,7 @@ export function EpisodeSelector({ setSelectedSeason, setEpisode, seasons, select
return ( return (
<div className="episodeSelector"> <div className="episodeSelector">
<TypeSelector setType={setSelectedSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} selected={selectedSeason}/><br></br> <TypeSelector setType={setSelectedSeason} selected={selectedSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} /><br></br>
<NumberSelector setType={(e) => setEpisode({episode: e, season: selectedSeason})} choices={choices} selected={(selectedSeason.toString() === currentSeason) ? currentEpisode : null} /> <NumberSelector setType={(e) => setEpisode({episode: e, season: selectedSeason})} choices={choices} selected={(selectedSeason.toString() === currentSeason) ? currentEpisode : null} />
</div> </div>
) )

View File

@ -24,6 +24,7 @@
margin-right: 0.5rem; margin-right: 0.5rem;
} }
.movieRow .left .seasonEpisodeSubtitle,
.movieRow .left .year { .movieRow .left .year {
color: var(--text-secondary); color: var(--text-secondary);
} }

View File

@ -1,17 +1,19 @@
import React from 'react' import React from 'react'
import { Arrow } from './Arrow' import { Arrow } from './Arrow'
import './MovieRow.css' // import { Cross } from './Crosss'
import { PercentageOverlay } from './PercentageOverlay' import { PercentageOverlay } from './PercentageOverlay'
import './MovieRow.css'
// title: string // title: string
// onClick: () => void // onClick: () => void
export function MovieRow(props) { export function MovieRow(props) {
const progressData = JSON.parse(localStorage.getItem("video-progress") || "{}") const progressData = JSON.parse(localStorage.getItem("video-progress") || "{}")
let progress; let progress;
let percentage = null; let percentage = null;
if (props.type === "movie") { if (props.type === "movie") {
progress = progressData?.[props.source]?.movie?.[props.slug]?.full progress = progressData?.[props.source]?.movie?.[props.slug]?.full
if (progress) { if (progress) {
percentage = Math.floor((progress.currentlyAt / progress.totalDuration) * 100) percentage = Math.floor((progress.currentlyAt / progress.totalDuration) * 100)
} }
@ -20,14 +22,17 @@ export function MovieRow(props) {
return ( return (
<div className="movieRow" onClick={() => props.onClick && props.onClick()}> <div className="movieRow" onClick={() => props.onClick && props.onClick()}>
<div className="left"> <div className="left">
{props.title}&nbsp; {/* <Cross /> */}
{props.title}<span className="seasonEpisodeSubtitle">{props.place ? ` - S${props.place.season}:E${props.place.episode}` : ''}</span>&nbsp;
<span className="year">({props.year})</span> <span className="year">({props.year})</span>
</div> </div>
<div className="watch"> <div className="watch">
<p>Watch {props.type}</p> <p>Watch {props.type}</p>
<Arrow/> <Arrow/>
</div> </div>
<PercentageOverlay percentage={percentage} />
<PercentageOverlay percentage={props.percentage || percentage} />
</div> </div>
) )
} }

View File

@ -14,6 +14,7 @@ export function Title(props) {
const accentLink = props.accentLink || ""; const accentLink = props.accentLink || "";
const accent = props.accent || ""; const accent = props.accent || "";
return ( return (
<div> <div>
{accent.length > 0 ? ( {accent.length > 0 ? (
@ -26,7 +27,7 @@ export function Title(props) {
{accentLink.length > 0 ? (<Arrow left/>) : null}{accent} {accentLink.length > 0 ? (<Arrow left/>) : null}{accent}
</p> </p>
) : null} ) : null}
<h1 className={"title " + ( size ? 'title-size-' + size : '' )}>{props.children}</h1> <h1 className={"title " + ( size ? `title-size-${size}` : '' )}>{props.children}</h1>
</div> </div>
) )
} }

View File

@ -6,10 +6,17 @@ import './VideoElement.css'
// streamUrl: string // streamUrl: string
// loading: boolean // loading: boolean
export function VideoElement({ streamUrl, loading, setProgress }) { // setProgress: (event: NativeEvent) => void
const videoRef = React.useRef(null); // videoRef: useRef
// startTime: number
export function VideoElement({ streamUrl, loading, setProgress, videoRef, startTime }) {
const [error, setError] = React.useState(false); const [error, setError] = React.useState(false);
function onLoad() {
if (startTime)
videoRef.current.currentTime = startTime;
}
React.useEffect(() => { React.useEffect(() => {
if (!streamUrl.endsWith('.mp4')) { if (!streamUrl.endsWith('.mp4')) {
setError(false) setError(false)
@ -28,7 +35,7 @@ export function VideoElement({ streamUrl, loading, setProgress }) {
hls.attachMedia(videoRef.current); hls.attachMedia(videoRef.current);
hls.loadSource(streamUrl); hls.loadSource(streamUrl);
} }
}, [videoRef, streamUrl, loading]) }, [videoRef, streamUrl, loading]);
if (error) if (error)
return (<VideoPlaceholder>Your browser is not supported</VideoPlaceholder>) return (<VideoPlaceholder>Your browser is not supported</VideoPlaceholder>)
@ -41,11 +48,11 @@ export function VideoElement({ streamUrl, loading, setProgress }) {
if (!streamUrl.endsWith('.mp4')) { if (!streamUrl.endsWith('.mp4')) {
return ( return (
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} /> <video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} onLoadedData={onLoad} />
) )
} else { } else {
return ( return (
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress}> <video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} onLoadedData={onLoad}>
<source src={streamUrl} type="video/mp4" /> <source src={streamUrl} type="video/mp4" />
</video> </video>
) )

View File

@ -14,34 +14,28 @@ export function MovieView(props) {
const baseRouteMatch = useRouteMatch('/:type/:source/:title/:slug'); const baseRouteMatch = useRouteMatch('/:type/:source/:title/:slug');
const showRouteMatch = useRouteMatch('/:type/:source/:title/:slug/season/:season/episode/:episode'); const showRouteMatch = useRouteMatch('/:type/:source/:title/:slug/season/:season/episode/:episode');
const history = useHistory(); const history = useHistory();
const { streamUrl, streamData, setStreamUrl } = useMovie(); const { streamUrl, streamData, setStreamUrl } = useMovie();
const [ seasonList, setSeasonList ] = React.useState([]); const [ seasonList, setSeasonList ] = React.useState([]);
const [ episodeLists, setEpisodeList ] = React.useState([]); const [ episodeLists, setEpisodeList ] = React.useState([]);
const [ loading, setLoading ] = React.useState(false); const [ loading, setLoading ] = React.useState(false);
const [ selectedSeason, setSelectedSeason ] = React.useState("1"); const [ selectedSeason, setSelectedSeason ] = React.useState("1");
const [ startTime, setStartTime ] = React.useState(0);
const videoRef = React.useRef(null);
let isVideoTimeSet = React.useRef(false);
const season = showRouteMatch?.params.season || "1"; const season = showRouteMatch?.params.season || "1";
const episode = showRouteMatch?.params.episode || "1"; const episode = showRouteMatch?.params.episode || "1";
React.useEffect(() => { // eslint-disable-next-line react-hooks/exhaustive-deps
setEpisodeList(streamData.episodes[selectedSeason]); function setEpisode({ season, episode }) {
}, [selectedSeason, streamData.episodes]) history.push(`${baseRouteMatch.url}/season/${season}/episode/${episode}`);
isVideoTimeSet.current = false;
React.useEffect(() => {
if (streamData.type === "show") {
setSeasonList(streamData.seasons);
// TODO load from localstorage last watched
setEpisodeList(streamData.episodes[streamData.seasons[0]]);
} }
}, [streamData])
React.useEffect(() => { React.useEffect(() => {
if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`); if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`);
}, [streamData, showRouteMatch, history, baseRouteMatch.url]); }, [streamData.type, showRouteMatch, history, baseRouteMatch.url]);
React.useEffect(() => {
if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`);
}, [streamData, showRouteMatch, history, baseRouteMatch.url]);
React.useEffect(() => { React.useEffect(() => {
if (streamData.type === "show" && showRouteMatch) setSelectedSeason(showRouteMatch.params.season.toString()); if (streamData.type === "show" && showRouteMatch) setSelectedSeason(showRouteMatch.params.season.toString());
@ -50,15 +44,17 @@ export function MovieView(props) {
React.useEffect(() => { React.useEffect(() => {
let cancel = false; let cancel = false;
// ignore if not a show
if (streamData.type !== "show") return () => { if (streamData.type !== "show") return () => {
cancel = true; cancel = true;
}; };
if (!episode) { if (!episode) {
setLoading(false); setLoading(false);
setStreamUrl(''); setStreamUrl('');
return; return;
} }
setLoading(true); setLoading(true);
getStreamUrl(streamData.slug, streamData.type, streamData.source, season, episode) getStreamUrl(streamData.slug, streamData.type, streamData.source, season, episode)
@ -67,41 +63,54 @@ export function MovieView(props) {
setStreamUrl(url) setStreamUrl(url)
setLoading(false); setLoading(false);
}) })
.catch(e => { .catch((e) => {
if (cancel) return; if (cancel) return;
console.error(e) console.error(e)
}) })
return () => { return () => {
cancel = true; cancel = true;
} }
}, [episode, streamData, setStreamUrl, season]); }, [episode, streamData, setStreamUrl, season]);
function setEpisode({ season, episode }) { React.useEffect(() => {
history.push(`${baseRouteMatch.url}/season/${season}/episode/${episode}`); if (streamData.type === "show") {
setSeasonList(streamData.seasons);
setEpisodeList(streamData.episodes[selectedSeason]);
} }
}, [streamData.seasons, streamData.episodes, streamData.type, selectedSeason])
React.useEffect(() => {
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
let key = streamData.type === "show" ? `${season}-${episode}` : "full"
let time = ls?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[key]?.currentlyAt;
setStartTime(time);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [baseRouteMatch, showRouteMatch]);
const setProgress = (evt) => { const setProgress = (evt) => {
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}") let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
// We're just checking lookmovie for now since there is only one scraper if (!ls[streamData.source])
if(!ls[streamData.source]) ls[streamData.source] = {} ls[streamData.source] = {}
if(!ls[streamData.source][streamData.type]) ls[streamData.source][streamData.type] = {} if (!ls[streamData.source][streamData.type])
if(!ls[streamData.source][streamData.type][streamData.slug]) { ls[streamData.source][streamData.type] = {}
if (!ls[streamData.source][streamData.type][streamData.slug])
ls[streamData.source][streamData.type][streamData.slug] = {} ls[streamData.source][streamData.type][streamData.slug] = {}
}
// Store real data // Store real data
let key = streamData.type === "show" ? `${season}-${episode}` : "full" let key = streamData.type === "show" ? `${season}-${episode}` : "full"
ls[streamData.source][streamData.type][streamData.slug][key] = { ls[streamData.source][streamData.type][streamData.slug][key] = {
currentlyAt: Math.floor(evt.currentTarget.currentTime), currentlyAt: Math.floor(evt.currentTarget.currentTime),
totalDuration: Math.floor(evt.currentTarget.duration), totalDuration: Math.floor(evt.currentTarget.duration),
updatedAt: Date.now() updatedAt: Date.now(),
meta: streamData
} }
if(streamData.type === "show") { if(streamData.type === "show") {
ls[streamData.source][streamData.type][streamData.slug][key].show = { ls[streamData.source][streamData.type][streamData.slug][key].show = {
season, season,
episode: episode episode
} }
} }
@ -121,7 +130,9 @@ export function MovieView(props) {
{streamData.type === "show" ? <Title size="small"> {streamData.type === "show" ? <Title size="small">
Season {season}: Episode {episode} Season {season}: Episode {episode}
</Title> : undefined} </Title> : undefined}
<VideoElement streamUrl={streamUrl} loading={loading} setProgress={setProgress} />
<VideoElement streamUrl={streamUrl} loading={loading} setProgress={setProgress} videoRef={videoRef} startTime={startTime} />
{streamData.type === "show" ? {streamData.type === "show" ?
<EpisodeSelector <EpisodeSelector
setSelectedSeason={setSelectedSeason} setSelectedSeason={setSelectedSeason}
@ -135,7 +146,7 @@ export function MovieView(props) {
currentSeason={season} currentSeason={season}
currentEpisode={episode} currentEpisode={episode}
source={streamData.source} streamData={streamData}
/> />
: ''} : ''}
</Card> </Card>

View File

@ -1,15 +0,0 @@
import React from 'react'
import { Title } from '../components/Title'
import { Card } from '../components/Card'
export function NotFound(props) {
return (
<div className="cardView">
<Card>
<Title accent="How did you end up here?">
Oopsie doopsie
</Title>
</Card>
</div>
)
}

View File

@ -8,11 +8,30 @@
box-sizing: border-box; box-sizing: border-box;
} }
.cardView > div { .cardView nav {
width: 100%;
max-width: 624px;
}
.cardView nav a {
padding: 8px 16px;
margin-right: 10px;
border-radius: 4px;
color: var(--text);
}
.cardView nav a:not(.selected-link) {
cursor: pointer;
}
.cardView nav a.selected-link {
background: var(--card);
color: var(--button-text);
font-weight: bold;
}
.cardView > * {
margin-top: 2rem; margin-top: 2rem;
} }
.cardView > div:first-child { .cardView > *:first-child {
margin-top: 38px; margin-top: 38px;
} }

View File

@ -1,19 +1,18 @@
import React from 'react'; import React from 'react';
import { Redirect, useRouteMatch, useHistory } from 'react-router-dom';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { InputBox } from '../components/InputBox'; import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
import { Title } from '../components/Title'; import { Arrow } from '../components/Arrow';
import { Card } from '../components/Card'; import { Card } from '../components/Card';
import { ErrorBanner } from '../components/ErrorBanner'; import { ErrorBanner } from '../components/ErrorBanner';
import { InputBox } from '../components/InputBox';
import { MovieRow } from '../components/MovieRow'; import { MovieRow } from '../components/MovieRow';
import { Arrow } from '../components/Arrow';
import { Progress } from '../components/Progress'; import { Progress } from '../components/Progress';
import { findContent, getStreamUrl, getEpisodes } from '../lib/index'; import { Title } from '../components/Title';
import { useMovie } from '../hooks/useMovie';
import { TypeSelector } from '../components/TypeSelector'; import { TypeSelector } from '../components/TypeSelector';
import { useMovie } from '../hooks/useMovie';
import { findContent, getEpisodes, getStreamUrl } from '../lib/index';
import './Search.css'; import './Search.css';
import { DiscordBanner } from '../components/DiscordBanner';
export function SearchView() { export function SearchView() {
const { navigate, setStreamUrl, setStreamData } = useMovie(); const { navigate, setStreamUrl, setStreamData } = useMovie();
@ -30,6 +29,8 @@ export function SearchView() {
const [failed, setFailed] = React.useState(false); const [failed, setFailed] = React.useState(false);
const [showingOptions, setShowingOptions] = React.useState(false); const [showingOptions, setShowingOptions] = React.useState(false);
const [errorStatus, setErrorStatus] = React.useState(false); const [errorStatus, setErrorStatus] = React.useState(false);
const [page, setPage] = React.useState('search');
const [continueWatching, setContinueWatching] = React.useState([])
const fail = (str) => { const fail = (str) => {
setProgress(maxSteps); setProgress(maxSteps);
@ -37,7 +38,7 @@ export function SearchView() {
setFailed(true) setFailed(true)
} }
async function getStream(title, slug, type, source) { async function getStream(title, slug, type, source, year) {
setStreamUrl(""); setStreamUrl("");
try { try {
@ -54,7 +55,6 @@ export function SearchView() {
let realUrl = ''; let realUrl = '';
if (type === "movie") { if (type === "movie") {
// getStreamUrl(slug, type, source, season, episode)
const { url } = await getStreamUrl(slug, type, source); const { url } = await getStreamUrl(slug, type, source);
if (url === '') { if (url === '') {
@ -71,7 +71,8 @@ export function SearchView() {
seasons, seasons,
episodes, episodes,
slug, slug,
source source,
year
}) })
setText(`Streaming...`) setText(`Streaming...`)
navigate("movie") navigate("movie")
@ -100,9 +101,9 @@ export function SearchView() {
return; return;
} }
const { title, slug, type, source } = options[0]; const { title, slug, type, source, year } = options[0];
history.push(`${routeMatch.url}/${source}/${title}/${slug}`); history.push(`${routeMatch.url}/${source}/${title}/${slug}`);
getStream(title, slug, type, source); getStream(title, slug, type, source, year);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
fail(`Failed to watch ${contentType}`) fail(`Failed to watch ${contentType}`)
@ -128,16 +129,97 @@ export function SearchView() {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
if (!type || (type !== 'movie' && type !== 'show')) return <Redirect to="/movie" /> React.useEffect(() => {
const progressData = JSON.parse(localStorage.getItem('video-progress') || "{}")
let newContinueWatching = []
Object.keys(progressData).forEach((source) => {
const all = [
...Object.entries(progressData[source]?.show ?? {}),
...Object.entries(progressData[source]?.movie ?? {})
];
for (const [slug, data] of all) {
for (let subselection of Object.values(data)) {
let entry = {
slug,
data: subselection,
type: subselection.show ? 'show' : 'movie',
percentageDone: Math.floor((subselection.currentlyAt / subselection.totalDuration) * 100),
source
}
// due to a constraint with incompatible localStorage data,
// we must quit here if episode and season data is not included
// in the show's data. watching the show will resolve.
if (!subselection.meta) continue;
if (entry.percentageDone < 90) {
newContinueWatching.push(entry)
// begin next episode logic
} else {
// we can't do next episode for movies!
if (!subselection.show) continue;
let newShow = {};
// if the current season has a next episode, load it
if (subselection.meta.episodes[subselection.show.season].includes(`${parseInt(subselection.show.episode) + 1}`)) {
newShow.season = subselection.show.season;
newShow.episode = `${parseInt(subselection.show.episode) + 1}`;
entry.percentageDone = 0;
// if the current season does not have a next epsiode, and the next season has a first episode, load that
} else if (subselection.meta.episodes[`${parseInt(subselection.show.season) + 1}`][0]) {
newShow.season = `${parseInt(subselection.show.season) + 1}`;
newShow.episode = subselection.meta.episodes[`${parseInt(subselection.show.season) + 1}`][0];
entry.percentageDone = 0;
// the next episode does not exist
} else {
continue;
}
// assign the new episode and season data
entry.data.show = { ...newShow };
// if the next episode exists, continue. we don't want to end up with duplicate data.
let nextEpisode = progressData?.[source]?.show?.[slug]?.[`${entry.data.show.season}-${entry.data.show.episode}`];
if (nextEpisode) continue;
newContinueWatching.push(entry);
}
}
}
newContinueWatching = newContinueWatching.sort((a, b) => {
return b.data.updatedAt - a.data.updatedAt
});
setContinueWatching(newContinueWatching)
})
}, []);
if (!type || (type !== 'movie' && type !== 'show')) {
return <Redirect to="/movie" />
}
return ( return (
<div className="cardView"> <div className="cardView">
<Helmet> <Helmet>
<title>{type === 'movie' ? 'Movies' : 'TV Shows'} | movie-web</title> <title>{type === 'movie' ? 'movies' : 'shows'} | movie-web</title>
</Helmet> </Helmet>
{/* Nav */}
<nav>
<a className={page === 'search' ? 'selected-link' : ''} onClick={() => setPage('search')} href>Search</a>
{continueWatching.length > 0 ?
<a className={page === 'watching' ? 'selected-link' : ''} onClick={() => setPage('watching')} href>Continue watching</a>
: ''}
</nav>
{/* Search */}
{page === 'search' ?
<React.Fragment>
<Card> <Card>
<DiscordBanner />
{errorStatus ? <ErrorBanner>{errorStatus}</ErrorBanner> : ''} {errorStatus ? <ErrorBanner>{errorStatus}</ErrorBanner> : ''}
<Title accent="Because watching content legally is boring"> <Title accent="Because watching content legally is boring">
What do you wanna watch? What do you wanna watch?
@ -170,13 +252,33 @@ export function SearchView() {
<MovieRow key={i} title={v.title} slug={v.slug} type={v.type} year={v.year} source={v.source} onClick={() => { <MovieRow key={i} title={v.title} slug={v.slug} type={v.type} year={v.year} source={v.source} onClick={() => {
history.push(`${routeMatch.url}/${v.source}/${v.title}/${v.slug}`); history.push(`${routeMatch.url}/${v.source}/${v.title}/${v.slug}`);
setShowingOptions(false) setShowingOptions(false)
getStream(v.title, v.slug, v.type, v.source) getStream(v.title, v.slug, v.type, v.source, v.year)
}} /> }} />
))} ))}
</div> </div>
)) ))}
}
</Card> </Card>
</React.Fragment> : <React.Fragment />}
{/* Continue watching */}
{continueWatching.length > 0 && page === 'watching' ? <Card>
<Title>Continue watching</Title>
{continueWatching?.map((v, i) => (
// <div>
<MovieRow key={i} title={v.data.meta.title} slug={v.data.meta.slug} type={v.type} year={v.data.meta.year} source={v.source} place={v.data.show} percentage={v.percentageDone} deletable onClick={() => {
if (v.type === 'show') {
history.push(`${routeMatch.url}/${v.source}/${v.data.meta.title}/${v.slug}/season/${v.data.show.season}/episode/${v.data.show.episode}`)
} else {
history.push(`${routeMatch.url}/${v.source}/${v.data.meta.title}/${v.slug}`)
}
setShowingOptions(false)
getStream(v.data.meta.title, v.data.meta.slug, v.type, v.source, v.data.meta.year)
}} />
// </div>
))}
</Card> : <React.Fragment></React.Fragment>}
<div className="topRightCredits"> <div className="topRightCredits">
<a href="https://github.com/JamesHawkinss/movie-web" target="_blank" rel="noreferrer">Check it out on GitHub <Arrow /></a> <a href="https://github.com/JamesHawkinss/movie-web" target="_blank" rel="noreferrer">Check it out on GitHub <Arrow /></a>
<br /> <br />