mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-29 16:07:40 +01:00
Better show support
This commit is contained in:
parent
c10807808a
commit
57e9bc2dc1
25 changed files with 495 additions and 205 deletions
|
@ -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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.card {
|
||||
background-color: #22232A;
|
||||
background-color: var(--card);
|
||||
padding: 3rem 4rem;
|
||||
margin: 0 3rem;
|
||||
border-radius: 10px;
|
||||
|
|
0
src/components/EpisodeSelector.css
Normal file
0
src/components/EpisodeSelector.css
Normal file
13
src/components/EpisodeSelector.js
Normal file
13
src/components/EpisodeSelector.js
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
48
src/components/NumberSelector.css
Normal file
48
src/components/NumberSelector.css
Normal 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);
|
||||
}
|
20
src/components/NumberSelector.js
Normal file
20
src/components/NumberSelector.js
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
59
src/components/TypeSelector.css
Normal file
59
src/components/TypeSelector.css
Normal 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;
|
||||
}
|
||||
}
|
24
src/components/TypeSelector.js
Normal file
24
src/components/TypeSelector.js
Normal 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>
|
||||
)
|
||||
}
|
|
@ -5,6 +5,6 @@
|
|||
}
|
||||
|
||||
.videoElementText {
|
||||
color: white;
|
||||
color: var(--text);
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
)
|
||||
|
|
|
@ -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={{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
@ -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 };
|
|
@ -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.
|
|
@ -0,0 +1,3 @@
|
|||
.showType-show .title-size-big {
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
}
|
||||
|
||||
.topRightCredits a, .topRightCredits a:visited {
|
||||
color: #E880C5;
|
||||
color: var(--theme-color);
|
||||
text-decoration: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
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} (${season}x${episode})`)
|
||||
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
109
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue