1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

Better show support

This commit is contained in:
James Hawkins 2021-07-14 23:09:42 +01:00
parent c10807808a
commit 57e9bc2dc1
25 changed files with 495 additions and 205 deletions

View file

@ -20,7 +20,7 @@
<title>movie-web</title>
</head>
<body>
<noscript style="color: white">You need to enable JavaScript to run this app.</noscript>
<noscript style="color: var(--text)">You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -1,5 +1,5 @@
.card {
background-color: #22232A;
background-color: var(--card);
padding: 3rem 4rem;
margin: 0 3rem;
border-radius: 10px;

View file

View file

@ -0,0 +1,13 @@
import React from 'react';
import { TypeSelector } from './TypeSelector';
import { NumberSelector } from './NumberSelector';
import './EpisodeSelector.css'
export function EpisodeSelector({ setSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode }) {
return (
<div>
<TypeSelector setType={setSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} selected={currentSeason}/><br></br>
<NumberSelector setType={(e) => setEpisode({episode: e, season: currentSeason})} choices={episodes.map(v=>({ value: v.toString(), label: v}))} selected={currentEpisode.season === currentSeason?currentEpisode.episode:null}/>
</div>
)
}

View file

@ -19,8 +19,8 @@
.inputTextBox {
border-width: 0;
outline: none;
background-color: #36363e;
color: white;
background-color: var(--content);
color: var(--text);
padding: .7rem 1.5rem;
height: auto;
flex: 1;
@ -28,52 +28,21 @@
}
.inputSearchButton {
background-color: #A73B83;
background-color: var(--button);
border-width: 0;
color: white;
color: var(--text);
padding: .5rem 2.1rem;
font-weight: bold;
cursor: pointer;
}
.inputDropdown {
border-width: 0;
outline: none;
background-color: #36363e;
color: white;
padding: .7rem 1rem;
height: auto;
width: 25%;
color: white;
font-weight: bold;
cursor: pointer;
}
.inputOptionBox {
border-width: 0;
outline: none;
background-color: #36363e;
color: white;
height: auto;
width: 10%;
box-sizing: border-box;
}
.inputDropdown:hover {
background-color: #3C3D44;
}
.inputSearchButton:hover {
background-color: #9C3179;
background-color: var(--button-hover);
}
.inputTextBox:hover {
background-color: #3C3D44;
}
.inputOptionBox:hover {
background-color: #3C3D44;
background-color: var(--content-hover);
}
.inputSearchButton .text > .arrow {
@ -83,11 +52,13 @@
right: -0.8rem;
bottom: -0.2rem;
}
.inputSearchButton .text {
display: flex;
position: relative;
transition: transform 0.2s ease-in-out;
}
.inputSearchButton:hover .text > .arrow {
transform: translateX(8px);
opacity: 1;
@ -98,7 +69,7 @@
}
.inputSearchButton:active {
background-color: #8b286a;
background-color: var(--button-active);
}
@media screen and (max-width: 700px) {
@ -121,17 +92,4 @@
margin-top: .5rem;
width: 100%;
}
.inputDropdown {
width: 100%;
padding: .7rem 1.5rem;
}
.inputOptionBox {
margin-top: .5rem;
width: 50%;
/* align-items:stretch; */
align-self: center;
padding: .7rem 1.5rem;
}
}

View file

@ -5,22 +5,13 @@ import './InputBox.css'
// props = { onSubmit: (str) => {}, placeholder: string}
export function InputBox({ onSubmit, placeholder }) {
const [searchTerm, setSearchTerm] = React.useState("");
const [type, setType] = React.useState("movie");
const [season, setSeason] = React.useState("");
const [episode, setEpisode] = React.useState("");
const showContentType = type === "show" ? false : true;
return (
<form className="inputBar" onSubmit={(e) => {
e.preventDefault();
onSubmit(searchTerm, type, season, episode)
onSubmit(searchTerm)
return false;
}}>
<select name="type" id="type" className="inputDropdown" onChange={(e) => setType(e.target.value)} required>
<option value="movie">Movie</option>
<option value="show">TV Show</option>
</select>
<input
type='text'
className="inputTextBox"
@ -30,26 +21,9 @@ export function InputBox({ onSubmit, placeholder }) {
onChange={(e) => setSearchTerm(e.target.value)}
required
/>
<input
type='text'
className='inputOptionBox'
id='inputOptionBoxSeason'
placeholder='Season'
value={season}
onChange={(e) => setSeason(e.target.value)}
hidden={showContentType}
required={!showContentType}
/>
<input
type='text'
className='inputOptionBox'
id='inputOptionBoxEpisode'
placeholder='Episode'
value={episode}
onChange={(e) => setEpisode(e.target.value)}
hidden={showContentType}
required={!showContentType} />
<button className="inputSearchButton"><span className="text">Search<span className="arrow"><Arrow /></span></span></button>
<button className="inputSearchButton">
<span className="text">Search<span className="arrow"><Arrow /></span></span>
</button>
</form>
)
}

View file

@ -1,8 +1,8 @@
.movieRow {
display: flex;
border-radius: 5px;
background-color: #35363D;
color: white;
background-color: var(--content);
color: var(--text);
padding: .8rem 1.5rem;
margin-top: .5rem;
cursor: pointer;
@ -23,11 +23,11 @@
}
.movieRow .left .year {
color: #BCBECB;
color: var(--text-secondary);
}
.movieRow .watch {
color: #D678B7;
color: var(--theme-color-text);
display: flex;
align-items: center;
}
@ -43,7 +43,7 @@
}
.movieRow:hover {
background-color: #3A3B40;
background-color: var(--content-hover);
}
.movieRow:hover .watch .arrow {
@ -51,8 +51,8 @@
}
.attribute {
color: white;
background-color: #D678B7;
color: var(--text);
background-color: var(--theme-color);
font-size: .75rem;
padding: .25rem;
border-radius: 10px;

View file

@ -12,7 +12,6 @@ export function MovieRow(props) {
<span className="year">({props.year})</span>
</div>
<div className="watch">
<span className='attribute' hidden={props.type === 'show' ? false : true }>{props.season}x{props.episode}</span>
<p>Watch {props.type}</p>
<Arrow/>
</div>

View file

@ -0,0 +1,48 @@
.numberSelector {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr));
gap: 5px;
position: relative;
margin-bottom: 1.5rem;
}
.numberSelector .choiceWrapper {
position: relative;
}
.numberSelector .choiceWrapper::before {
content: '';
display: block;
width: 100%;
padding-bottom: 100%;
}
.numberSelector .choice {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--choice);
margin-right: 5px;
padding: .2rem;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
color: var(--text);
font-weight: bold;
cursor: pointer;
user-select: none;
border-radius: 10%;
box-sizing: border-box;
}
.numberSelector .choice:hover {
background-color: var(--choice-hover);
}
.numberSelector .choice.selected {
color: var(--text);
background-color: var(--choice-hover);
}

View file

@ -0,0 +1,20 @@
import React from 'react';
// import { Arrow } from './Arrow';
import './NumberSelector.css'
// setType: (txt: string) => void
// choices: { label: string, value: string }[]
// selected: string
export function NumberSelector({ setType, choices, selected }) {
return (
<div className="numberSelector">
{choices.map(v=>(
<div key={v.value} className="choiceWrapper">
<div className={`choice ${selected&&selected===v.value?'selected':''}`} onClick={() => setType(v.value)}>
{v.label}
</div>
</div>
))}
</div>
)
}

View file

@ -1,6 +1,6 @@
.progress {
text-align: center;
color: #BCBECB;
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
@ -32,12 +32,12 @@
.progress .bar .bar-inner {
transition: width 400ms ease-in-out, background-color 100ms ease-in-out;
background-color: #D463AE;
background-color: var(--theme-color);
border-radius: 10px;
height: 100%;
width: 0%;
}
.progress.failed .bar .bar-inner {
background-color: #d85b66;
background-color: var(--failed);
}

View file

@ -1,6 +1,6 @@
.title {
font-size: 2rem;
color: white;
color: var(--text);
/* max-width: 20rem; */
margin: 0;
padding: 0;
@ -10,9 +10,13 @@
.title-size-medium {
font-size: 1.5rem;
}
.title-size-small {
font-size: 1.1rem;
color: #afb1b8;
}
.title-accent {
color: #E880C5;
color: var(--theme-color);
font-weight: 600;
margin: 0;
padding: 0;
@ -34,3 +38,4 @@
.title-accent.title-accent-link:hover .arrow {
transform: translateY(.1rem) translateX(-.5rem);
}

View file

@ -0,0 +1,59 @@
/* TODO better responsiveness, use dropdown if more than 5 options */
.typeSelector {
display: inline-flex;
position: relative;
margin-bottom: 1.5rem;
max-width: 100%;
flex-wrap: wrap;
}
.typeSelector::before {
content: "";
position: absolute;
width: 100%;
bottom: 0;
background-color: #3a3c46;
height: 4px;
border-radius: 2px;
}
.typeSelector .choice {
width: 7rem;
height: 3rem;
padding: .3rem .2rem;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
box-sizing: border-box;
color: #585A67;
font-weight: bold;
cursor: pointer;
user-select: none;
}
.typeSelector .choice:hover {
color: #afb1b8;
}
.typeSelector .choice.selected {
color: var(--text);
}
.typeSelector .selectedBar {
position: absolute;
height: 4px;
width: 7rem;
background-color: var(--theme-color);
border-radius: 2px;
bottom: 0;
transition: transform 150ms ease-in-out;
}
@media screen and (max-width: 700px) {
.typeSelector {
width: 80%;
display: block;
}
}

View file

@ -0,0 +1,24 @@
import React from 'react';
// import { Arrow } from './Arrow';
import './TypeSelector.css'
// setType: (txt: string) => void
// choices: { label: string, value: string }[]
// selected: string
export function TypeSelector({ setType, choices, selected }) {
const selectedIndex = choices.findIndex(v=>v.value===selected);
const transformStyles = {
opacity: selectedIndex!==-1?1:0,
transform: `translateX(${selectedIndex!==-1?selectedIndex*7:0}rem)`
}
return (
<div className="typeSelector">
{choices.map(v=>(
<div key={v.value} className={`choice ${selected===v.value?'selected':''}`} onClick={() => setType(v.value)}>
{v.label}
</div>
))}
<div className="selectedBar" style={transformStyles}/>
</div>
)
}

View file

@ -5,6 +5,6 @@
}
.videoElementText {
color: white;
color: var(--text);
margin: 0;
}

View file

@ -3,13 +3,14 @@ import Hls from 'hls.js'
import './VideoElement.css'
// streamUrl: string
export function VideoElement({ streamUrl }) {
// loading: boolean
export function VideoElement({ streamUrl, loading }) {
const videoRef = React.useRef(null);
const [error, setError] = React.useState(false);
React.useEffect(() => {
setError(false)
if (!videoRef || !videoRef.current) return;
if (!videoRef || !videoRef.current || !streamUrl || streamUrl.length === 0 || loading) return;
const hls = new Hls();
@ -23,11 +24,19 @@ export function VideoElement({ streamUrl }) {
hls.attachMedia(videoRef.current);
hls.loadSource(streamUrl);
}, [videoRef, streamUrl])
}, [videoRef, streamUrl, loading])
// TODO make better loading/error/empty state
if (error)
return (<p className="videoElementText">Your browser is not supported</p>)
if (loading)
return <p className="videoElementText">Loading episode</p>
if (!streamUrl || streamUrl.length === 0)
return <p className="videoElementText">No video selected</p>
return (
<video className="videoElement" ref={videoRef} controls autoPlay />
)

View file

@ -4,7 +4,7 @@ const MovieContext = React.createContext(null)
export function MovieProvider(props) {
const [page, setPage] = React.useState("search");
const [stream, setStream] = React.useState("");
const [streamData, setStreamData] = React.useState({ title: "", type: "", episode: "", season: "" })
const [streamData, setStreamData] = React.useState({ title: "", slug: "", type: "", episodes: [], seasons: [] })
return (
<MovieContext.Provider value={{

View file

@ -1,6 +1,33 @@
:root {
/* TODO finish theming for entire css */
--theme-color: #E880C5;
--theme-color-text: var(--theme-color);
--failed: #d85b66;
--body: #16171D;
--card: #22232A;
--text: white;
--text-secondary: #BCBECB;
--content: #36363e;
--content-hover: #3C3D44;
--button: #A73B83;
--button-hover: #9C3179;
--button-active: #8b286a;
--choice: #2E2F37;
--choice-hover: #45464D;
--choice-active: #45464D;
}
body, html {
margin: 0;
background-color: #16171D;
background-color: var(--body);
min-height: 100vh;
}

View file

@ -53,6 +53,22 @@ async function getAccessToken(config) {
return "Invalid type provided in config";
}
async function getEpisodes(slug) {
const url = getCorsUrl(`https://lookmovie.io/shows/view/${slug}`);
const pageReq = await fetch(url).then((d) => d.text());
const data = JSON5.parse("{" +
pageReq
.slice(pageReq.indexOf(`show_storage`))
.split("};")[0]
.split("= {")[1]
.trim() +
"}"
);
return data.seasons
}
async function getStreamUrl(slug, type, season, episode) {
const url = getCorsUrl(`https://lookmovie.io/${type}s/view/${slug}`);
const pageReq = await fetch(url).then((d) => d.text());
@ -72,7 +88,7 @@ async function getStreamUrl(slug, type, season, episode) {
id = data.id_movie;
} else if (type === "show") {
const episodeObj = data.seasons.find((v) => { return v.season === season && v.episode === episode; });
if (episodeObj) {
id = episodeObj.id_episode;
}
@ -92,9 +108,22 @@ async function getStreamUrl(slug, type, season, episode) {
}
async function findContent(searchTerm, type) {
const searchUrl = getCorsUrl(`https://lookmovie.io/api/v1/${type}s/search/?q=${encodeURIComponent(searchTerm)}`);
const searchRes = await fetch(searchUrl).then((d) => d.json());
const results = [...searchRes.result.map((v) => ({ ...v, type: type}))];
// const searchUrl = getCorsUrl(`https://lookmovie.io/api/v1/${type}s/search/?q=${encodeURIComponent(searchTerm)}`);
const searchUrl = getCorsUrl(`https://lookmovie.io/${type}s/search/?q=${encodeURIComponent(searchTerm)}`);
const searchRes = await fetch(searchUrl).then((d) => d.text());
// Parse DOM to find search results on full search page
const parser = new DOMParser();
const doc = parser.parseFromString(searchRes, "text/html");
const nodes = Array.from(doc.querySelectorAll('.movie-item-style-1'));
const results = nodes.map(node => {
return {
type,
title: node.querySelector('h6 a').innerText,
year: node.querySelector('.year').innerText,
slug: node.querySelector('a').href.split('/').pop(),
}
});
const fuse = new Fuse(results, { threshold: 0.3, distance: 200, keys: ["title"] });
const matchedResults = fuse
@ -125,4 +154,4 @@ async function findContent(searchTerm, type) {
}
}
export { findContent, getStreamUrl };
export { findContent, getStreamUrl, getEpisodes };

View file

@ -1,72 +0,0 @@
/* eslint-disable no-restricted-globals */
// This service worker can be customized!
// See https://developers.google.com/web/tools/workbox/modules
// for the list of available Workbox modules, or add any other
// code you'd like.
// You can also remove this file if you'd prefer not to use a
// service worker, and the Workbox build step will be skipped.
import { clientsClaim } from 'workbox-core';
import { ExpirationPlugin } from 'workbox-expiration';
import { precacheAndRoute, createHandlerBoundToURL } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate } from 'workbox-strategies';
clientsClaim();
// Precache all of the assets generated by your build process.
// Their URLs are injected into the manifest variable below.
// This variable must be present somewhere in your service worker file,
// even if you decide not to use precaching. See https://cra.link/PWA
precacheAndRoute(self.__WB_MANIFEST);
// Set up App Shell-style routing, so that all navigation requests
// are fulfilled with your index.html shell. Learn more at
// https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$');
registerRoute(
// Return false to exempt requests from being fulfilled by index.html.
({ request, url }) => {
// If this isn't a navigation, skip.
if (request.mode !== 'navigate') {
return false;
} // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) {
return false;
} // If this looks like a URL for a resource, because it contains // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) {
return false;
} // Return true to signal that we want to use the handler.
return true;
},
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
);
// An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/
registerRoute(
// Add in any other file extensions or routing criteria as needed.
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
new StaleWhileRevalidate({
cacheName: 'images',
plugins: [
// Ensure that once this runtime cache reaches a maximum size the
// least-recently used images are removed.
new ExpirationPlugin({ maxEntries: 50 }),
],
})
);
// This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
});
// Any other custom service worker logic can go here.

View file

@ -0,0 +1,3 @@
.showType-show .title-size-big {
margin-bottom: 10px;
}

View file

@ -3,17 +3,80 @@ import { Title } from '../components/Title'
import { Card } from '../components/Card'
import { useMovie } from '../hooks/useMovie'
import { VideoElement } from '../components/VideoElement'
import { EpisodeSelector } from '../components/EpisodeSelector'
import './Movie.css'
import { getStreamUrl } from '../lib/lookMovie'
export function MovieView(props) {
const { streamUrl, streamData } = useMovie();
const { streamUrl, streamData, setStreamUrl } = useMovie();
const [season, setSeason] = React.useState("1");
const [seasonList, setSeasonList] = React.useState([]);
const [episodeLists, setEpisodeList] = React.useState([]);
const [episode, setEpisode] = React.useState({ episode: null, season: null });
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
setEpisodeList(streamData.episodes[season]);
}, [season, streamData.episodes])
React.useEffect(() => {
if (streamData.type === "show") {
setSeasonList(streamData.seasons);
setSeason(streamData.seasons[0])
// TODO load from localstorage last watched
setEpisode({ episode: streamData.episodes[streamData.seasons[0]][0], season: streamData.seasons[0] })
setEpisodeList(streamData.episodes[streamData.seasons[0]]);
}
}, [streamData])
React.useEffect(() => {
let cancel = false;
// ignore if not a show
if (streamData.type !== "show") return () => {
cancel = true;
};
if (!episode.episode) {
setLoading(false);
setStreamUrl('');
return;
}
setLoading(true);
getStreamUrl(streamData.slug, streamData.type, episode.season, episode.episode)
.then(({url}) => {
if (cancel) return;
setStreamUrl(url)
setLoading(false);
})
.catch(e => {
if (cancel) return;
console.error(e)
})
return () => {
cancel = true;
}
}, [episode, streamData, setStreamUrl])
return (
<div className="cardView">
<div className={`cardView showType-${streamData.type}`}>
<Card fullWidth>
<Title accent="Return to home" accentLink="search">
{streamData.title} {streamData.type === "show" ? `(${streamData.season}x${streamData.episode})` : '' }
{streamData.title}
</Title>
<VideoElement streamUrl={streamUrl}/>
{streamData.type === "show" ? <Title size="small">
Season {episode.season}: Episode {episode.episode}
</Title> : undefined}
<VideoElement streamUrl={streamUrl} loading={loading}/>
{streamData.type === "show" ?
<EpisodeSelector
setSeason={setSeason}
setEpisode={setEpisode}
seasons={seasonList}
episodes={episodeLists}
currentSeason={season}
currentEpisode={episode}
/>
: ''}
</Card>
</div>
)

View file

@ -24,7 +24,7 @@
}
.topRightCredits a, .topRightCredits a:visited {
color: #E880C5;
color: var(--theme-color);
text-decoration: none;
margin: 0;
}

View file

@ -7,18 +7,21 @@ import { Arrow } from '../components/Arrow'
import { Progress } from '../components/Progress'
import { findContent, getStreamUrl } from '../lib/lookMovie'
import { useMovie } from '../hooks/useMovie';
import { TypeSelector } from '../components/TypeSelector'
import { getEpisodes } from '../lib/lookMovie'
import './Search.css'
export function SearchView() {
const { navigate, setStreamUrl, setStreamData } = useMovie();
const maxSteps = 3;
const maxSteps = 4;
const [options, setOptions] = React.useState([]);
const [progress, setProgress] = React.useState(0);
const [text, setText] = React.useState("");
const [failed, setFailed] = React.useState(false);
const [showingOptions, setShowingOptions] = React.useState(false);
const [type, setType] = React.useState("movie");
const fail = (str) => {
setProgress(maxSteps);
@ -26,24 +29,46 @@ export function SearchView() {
setFailed(true)
}
async function getStream(title, slug, type, season, episode) {
async function getStream(title, slug, type) {
setStreamUrl("");
try {
setProgress(2);
setText(`Getting stream for "${title}"`)
const { url } = await getStreamUrl(slug, type, season, episode);
if (url === '') {
return fail(`Not found: ${title} (${season}x${episode})`)
let seasons = [];
let episodes = [];
if (type === "show") {
const episodeData = await getEpisodes(slug);
episodeData.forEach((e) => {
if (!seasons.includes(e.season))
seasons.push(e.season);
if (!episodes[e.season])
episodes[e.season] = []
episodes[e.season].push(e.episode)
})
}
let realUrl = '';
if (type === "movie") {
const { url } = await getStreamUrl(slug, type);
if (url === '') {
return fail(`Not found: ${title}`)
}
realUrl = url;
}
setProgress(maxSteps);
setStreamUrl(url);
setStreamUrl(realUrl);
setStreamData({
title,
type,
season,
episode
seasons,
episodes,
slug
})
setText(`Streaming...`)
navigate("movie")
@ -52,9 +77,9 @@ export function SearchView() {
}
}
async function searchMovie(query, contentType, season, episode) {
async function searchMovie(query, contentType) {
setFailed(false);
setText(`Searching for ${contentType} "${query}" ${contentType === 'show' ? ` (${season}x${episode})` : ''}`);
setText(`Searching for ${contentType} "${query}"`);
setProgress(1)
setShowingOptions(false)
@ -64,11 +89,6 @@ export function SearchView() {
if (options.length === 0) {
return fail(`Could not find that ${contentType}`)
} else if (options.length > 1) {
options.forEach((o) => {
o.season = season;
o.episode = episode;
});
setProgress(2);
setText(`Choose your ${contentType}`);
setOptions(options);
@ -77,7 +97,7 @@ export function SearchView() {
}
const { title, slug, type } = options[0];
getStream(title, slug, type, season, episode);
getStream(title, slug, type);
} catch (err) {
fail(`Failed to watch ${contentType}`)
}
@ -89,13 +109,21 @@ export function SearchView() {
<Title accent="Because watching content legally is boring">
What do you wanna watch?
</Title>
<InputBox placeholder="Hamilton" onSubmit={(str, type, season, episode) => searchMovie(str, type, season, episode)} />
<TypeSelector
setType={(type) => setType(type)}
choices={[
{ label: "Movie", value: "movie" },
{ label: "TV Show", value: "show" }
]}
selected={type}
/>
<InputBox placeholder="Hamilton" onSubmit={(str) => searchMovie(str, type)} />
<Progress show={progress > 0} failed={failed} progress={progress} steps={maxSteps} text={text} />
</Card>
<Card show={showingOptions} doTransition>
<Title size="medium">
Whoops, there are a few movies like that
Whoops, there are a few {type}s like that
</Title>
{options?.map((v, i) => (
<MovieRow key={i} title={v.title} type={v.type} year={v.year} season={v.season} episode={v.episode} onClick={() => {

109
yarn.lock
View file

@ -1437,6 +1437,59 @@
schema-utils "^2.6.5"
source-map "^0.7.3"
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78=
"@protobufjs/base64@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735"
integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==
"@protobufjs/codegen@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb"
integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==
"@protobufjs/eventemitter@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70"
integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A=
"@protobufjs/fetch@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45"
integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=
dependencies:
"@protobufjs/aspromise" "^1.1.1"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/float@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1"
integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=
"@protobufjs/inquire@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089"
integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=
"@protobufjs/path@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d"
integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=
"@protobufjs/pool@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54"
integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=
"@protobufjs/utf8@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=
"@rollup/plugin-node-resolve@^7.1.1":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-7.1.3.tgz#80de384edfbd7bfc9101164910f86078151a3eca"
@ -1752,6 +1805,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/long@^4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -1762,6 +1820,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.31.tgz#72286bd33d137aa0d152d47ec7c1762563d34055"
integrity sha512-vFHy/ezP5qI0rFgJ7aQnjDXwAMrG0KqqIH7tQG5PPv3BWBayOPIQNBjVc/P6hhdZfMx51REc6tfDNXHUio893g==
"@types/node@>=13.7.0":
version "16.3.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.3.2.tgz#655432817f83b51ac869c2d51dd8305fb8342e16"
integrity sha512-jJs9ErFLP403I+hMLGnqDRWT0RYKSvArxuBVh2veudHV7ifEC1WAmjJADacZ7mRbA2nWgHtn8xyECMAot0SkAw==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
@ -3093,6 +3156,22 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
castv2-client@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/castv2-client/-/castv2-client-1.2.0.tgz#a9193b1a5448b8cb9a0415bd021c8811ed7b0544"
integrity sha1-qRk7GlRIuMuaBBW9AhyIEe17BUQ=
dependencies:
castv2 "~0.1.4"
debug "^2.2.0"
castv2@~0.1.4:
version "0.1.10"
resolved "https://registry.yarnpkg.com/castv2/-/castv2-0.1.10.tgz#d3df00124f1ba8a97691c69dd44221d3b5f93c56"
integrity sha512-3QWevHrjT22KdF08Y2a217IYCDQDP7vEJaY4n0lPBeC5UBYbMFMadDfVTsaQwq7wqsEgYUHElPGm3EO1ey+TNw==
dependencies:
debug "^4.1.1"
protobufjs "^6.8.8"
chalk@2.4.2, chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -3757,7 +3836,7 @@ cssnano-preset-default@^4.0.7:
postcss-normalize-timing-functions "^4.0.2"
postcss-normalize-unicode "^4.0.1"
postcss-normalize-url "^4.0.1"
postcss-normalize-whitespace "^4.0.2"
postcss-normalize-var(--text)space "^4.0.2"
postcss-ordered-values "^4.1.2"
postcss-reduce-initial "^4.0.3"
postcss-reduce-transforms "^4.0.2"
@ -6930,6 +7009,11 @@ loglevel@^1.6.8:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197"
integrity sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==
long@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28"
integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==
loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -8433,9 +8517,9 @@ postcss-normalize-url@^4.0.1:
postcss "^7.0.0"
postcss-value-parser "^3.0.0"
postcss-normalize-whitespace@^4.0.2:
postcss-normalize-var(--text)space@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
resolved "https://registry.yarnpkg.com/postcss-normalize-var(--text)space/-/postcss-normalize-var(--text)space-4.0.2.tgz#bf1d4070fe4fcea87d1348e825d8cc0c5faa7d82"
integrity sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==
dependencies:
postcss "^7.0.0"
@ -8759,6 +8843,25 @@ prop-types@^15.7.2:
object-assign "^4.1.1"
react-is "^16.8.1"
protobufjs@^6.8.8:
version "6.11.2"
resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b"
integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==
dependencies:
"@protobufjs/aspromise" "^1.1.2"
"@protobufjs/base64" "^1.1.2"
"@protobufjs/codegen" "^2.0.4"
"@protobufjs/eventemitter" "^1.1.0"
"@protobufjs/fetch" "^1.1.0"
"@protobufjs/float" "^1.0.2"
"@protobufjs/inquire" "^1.1.0"
"@protobufjs/path" "^1.1.2"
"@protobufjs/pool" "^1.1.0"
"@protobufjs/utf8" "^1.1.0"
"@types/long" "^4.0.1"
"@types/node" ">=13.7.0"
long "^4.0.0"
proxy-addr@~2.0.5:
version "2.0.6"
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf"