Port to react

This commit is contained in:
James Hawkins 2021-07-13 23:31:37 +01:00
parent 964b412053
commit 0b99f1c35e
35 changed files with 12255 additions and 143 deletions

54
.github/workflows/build-deploy.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Build & deploy
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v1
with:
node-version: 13.x
- name: Install NPM packages
run: npm ci
- name: Build project
run: npm run build
- name: Upload production-ready build files
uses: actions/upload-artifact@v2
with:
name: production-files
path: ./build
deploy:
name: Deploy
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master'
steps:
- name: Download artifact
uses: actions/download-artifact@v2
with:
name: production-files
path: ./build
- name: Deploy to gh-pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./build

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View file

@ -1,5 +1,5 @@
# movie-web
Available at: [movie.squeezebox.dev](https://movie.squeezebox.dev)
Credits to [@JipFr](https://github.com/JipFr) for initial work on [movie-cli](https://github.com/JipFr/movie-cli)
Small web app for watching movies easily. Check it out at **[movie.squeezebox.dev](https://movie.squeezebox.dev)**.
## Credits
- Thanks to [@JipFr](https://github.com/JipFr) for initial work on [movie-cli](https://github.com/JipFr/movie-cli)
- Thanks to [@mrjvs](https://github.com/mrjvs) for help porting to React

View file

@ -1,59 +0,0 @@
@font-face {
font-family: 'JetBrainsMono';
src: url(../fonts/JetBrainsMono-Regular.woff2);
font-weight: 400;
font-style: normal;
}
html, body {
height: 1vh;
}
body {
margin: 0;
color: #95979F;
background-color: #0c0e14;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40' viewBox='0 0 40 40'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23586ca8' fill-opacity='0.12'%3E%3Cpath d='M0 38.59l2.83-2.83 1.41 1.41L1.41 40H0v-1.41zM0 1.4l2.83 2.83 1.41-1.41L1.41 0H0v1.41zM38.59 40l-2.83-2.83 1.41-1.41L40 38.59V40h-1.41zM40 1.41l-2.83 2.83-1.41-1.41L38.59 0H40v1.41zM20 18.6l2.83-2.83 1.41 1.41L21.41 20l2.83 2.83-1.41 1.41L20 21.41l-2.83 2.83-1.41-1.41L18.59 20l-2.83-2.83 1.41-1.41L20 18.59z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
font-family: 'JetBrainsMono';
}
.messages {
background-color: #2D313D;
border-radius: 10px;
width: 80%;
padding-left: 10px;
}
.error {
color: #f3565d;
}
.info {
color: #2e5bbd;
}
.content {
padding: 1rem;
border-radius: 10px;
background-color: #2D313D;
width: 80%;
}
.video {
width: 100%;
}
form {
background-color: #2D313D;
padding: 5px;
width: 300px;
text-align: center;
}
input[type="submit"] {
width: 20%;
}
input[type="text"] {
width: 70%;
}

View file

@ -1,34 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>movie-web</title>
<link rel="stylesheet" href="./assets/css/style.css" type="text/css">
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.4.6"></script>
<script src="https://unpkg.com/json5@^2.0.0/dist/index.min.js"></script>
<script src="assets/js/index.js"></script>
</head>
<body>
<form action='#' onsubmit='findMovie();return false;'>
<input type='text' id='search' placeholder='Find movie...'><!--
--><input type='submit'>
</form>
<div class='content'>
<video id="video" class="video" controls autoplay></video>
</div>
<div class='messages'>
<strong>
<p id='error' class='error'></p>
<p id='info' class='info'></p>
</strong>
</div>
</body>
</html>

42
package.json Normal file
View file

@ -0,0 +1,42 @@
{
"name": "movie-web",
"version": "0.1.0",
"private": true,
"homepage": "https://movie.squeezebox.dev",
"dependencies": {
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"fuse.js": "^6.4.6",
"hls.js": "^1.0.7",
"json5": "^2.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

20
public/index.html Normal file
View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="because watching movies legally is boring"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>movie-web</title>
</head>
<body>
<noscript style="color: white">You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

25
public/manifest.json Normal file
View file

@ -0,0 +1,25 @@
{
"short_name": "movie-web",
"name": "movie-web",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#191c24",
"background_color": "#0c0e14"
}

3
public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

31
src/App.js Normal file
View file

@ -0,0 +1,31 @@
import './index.css';
import { SearchView } from './views/Search';
import { NotFound } from './views/NotFound';
import { MovieView } from './views/Movie';
import { useMovie, MovieProvider} from './hooks/useMovie';
function Router() {
const { page } = useMovie();
if (page === "search") {
return <SearchView/>
}
if (page === "movie") {
return <MovieView/>
}
return (
<NotFound/>
)
}
function App() {
return (
<MovieProvider>
<Router/>
</MovieProvider>
);
}
export default App;

7
src/components/Arrow.css Normal file
View file

@ -0,0 +1,7 @@
.feather.left {
transform: rotate(180deg);
}
.arrow {
display: inline-block;
}

14
src/components/Arrow.js Normal file
View file

@ -0,0 +1,14 @@
import React from 'react'
import './Arrow.css'
// left?: boolean
export function Arrow(props) {
return (
<div className="arrow">
<svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={`feather ${props.left?'left':''}`}>
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</div>
)
}

28
src/components/Card.css Normal file
View file

@ -0,0 +1,28 @@
.card {
background-color: #22232A;
padding: 3rem 4rem;
width: 39rem;
max-width: 100%;
margin: 0 3rem;
border-radius: 10px;
box-sizing: border-box;
transition: height 500ms ease-in-out, transform 800ms ease-in-out, opacity 800ms ease-in-out;
}
.card.full {
width: 75rem;
}
.card-wrapper {
transition: height 500ms ease-in-out;
overflow: hidden;
}
.card.doTransition {
opacity: 0;
transform: translateY(-.7rem);
}
.card.doTransition.show {
opacity: 1;
transform: translateY(0rem);
}

28
src/components/Card.js Normal file
View file

@ -0,0 +1,28 @@
import React from 'react'
import './Card.css'
// fullWidth: boolean
// show: boolean
// doTransition: boolean
export function Card(props) {
const [showing, setShowing] = React.useState(false);
const measureRef = React.useRef(null)
const [height, setHeight] = React.useState(0);
React.useEffect(() => {
if (!measureRef?.current) return;
setShowing(props.show);
setHeight(measureRef.current.clientHeight)
}, [props.show, measureRef])
return (
<div className="card-wrapper" style={{
height: props.doTransition ? (showing ? height : 0) : "initial",
}}>
<div className={`card ${ props.fullWidth ? 'full' : '' } ${ showing ? 'show' : '' } ${ props.doTransition ? 'doTransition' : '' }`} ref={measureRef}>
{props.children}
</div>
</div>
)
}

View file

@ -0,0 +1,72 @@
.inputBar {
width: 100%;
display: flex;
height: 3rem;
}
.inputBar > *:first-child{
border-radius: 0 !important;
border-top-left-radius: 10px !important;
border-bottom-left-radius: 10px !important;
}
.inputBar > *:last-child {
border-radius: 0 !important;
border-top-right-radius: 10px !important;
border-bottom-right-radius: 10px !important;
}
.inputTextBox {
border-width: 0;
outline: none;
background-color: #36363e;
color: white;
padding: .7rem 1.5rem;
height: auto;
flex: 1;
}
.inputSearchButton {
background-color: #A73B83;
border-width: 0;
color: white;
padding: .5rem 2.1rem;
font-weight: bold;
cursor: pointer;
}
.inputSearchButton:hover {
background-color: #9C3179;
}
.inputTextBox:hover {
background-color: #3C3D44;
}
.inputSearchButton .text > .arrow {
opacity: 0;
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
position: absolute;
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;
}
.inputSearchButton:hover .text {
transform: translateX(-10px);
}
.inputSearchButton:active {
background-color: #8b286a;
}

View file

@ -0,0 +1,26 @@
import React from 'react';
import { Arrow } from './Arrow';
import './InputBox.css'
// props = { onSubmit: (str) => {}, placeholder: string}
export function InputBox({ onSubmit, placeholder }) {
const [value, setValue] = React.useState("");
return (
<form className="inputBar" onSubmit={(e) => {
e.preventDefault();
onSubmit(value)
return false;
}}>
<input
type='text'
className="inputTextBox"
id="inputTextBox"
placeholder={placeholder}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
<button className="inputSearchButton"><span className="text">Search<span className="arrow"><Arrow/></span></span></button>
</form>
)
}

View file

@ -0,0 +1,45 @@
.movieRow {
display: flex;
border-radius: 5px;
background-color: #35363D;
color: white;
padding: .8rem 1.5rem;
margin-top: .5rem;
cursor: pointer;
transition: transform 50ms ease-in-out;
user-select: none;
}
.movieRow p {
margin: 0;
}
.movieRow .left {
flex: 1;
display: flex;
align-items: flex-start;
}
.movieRow .watch {
color: #D678B7;
display: flex;
align-items: center;
}
.movieRow .watch .arrow {
margin-left: .5rem;
transition: transform 50ms ease-in-out;
transform: translateY(.1rem);
}
.movieRow:active {
transform: scale(1.02);
}
.movieRow:hover {
background-color: #3A3B40;
}
.movieRow:hover .watch .arrow {
transform: translateX(.3rem) translateY(.1rem);
}

View file

@ -0,0 +1,19 @@
import React from 'react'
import { Arrow } from './Arrow'
import './MovieRow.css'
// title: string
// onClick: () => void
export function MovieRow(props) {
return (
<div className="movieRow" onClick={() => props.onClick && props.onClick()}>
<div className="left">
{props.title}
</div>
<div className="watch">
<p>Watch movie</p>
<Arrow/>
</div>
</div>
)
}

View file

@ -0,0 +1,43 @@
.progress {
text-align: center;
color: #BCBECB;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
height: 5rem;
margin-top: 1rem;
transition: height 800ms ease-in-out, opacity 800ms ease-in-out;
opacity: 1;
}
.progress.hide {
opacity: 0;
height: 0rem;
}
.progress p {
margin: 0;
margin-bottom: 1rem;
}
.progress .bar {
width: 13rem;
max-width: 100%;
background-color: #35363D;
border-radius: 10px;
height: 7px;
display: inline-block;
}
.progress .bar .bar-inner {
transition: width 400ms ease-in-out, background-color 100ms ease-in-out;
background-color: #D463AE;
border-radius: 10px;
height: 100%;
width: 0%;
}
.progress.failed .bar .bar-inner {
background-color: #d85b66;
}

View file

@ -0,0 +1,21 @@
import React from 'react'
import './Progress.css'
// show: boolean
// progress: number
// steps: number
// text: string
// failed: boolean
export function Progress(props) {
return (
<div className={`progress ${props.show?'':'hide'} ${props.failed?'failed':''}`}>
{ props.text && props.text.length > 0 ? (
<p>{props.text}</p>) : null}
<div className="bar">
<div className="bar-inner" style={{
width: (props.progress / props.steps * 100).toFixed(0) + "%"
}}/>
</div>
</div>
)
}

36
src/components/Title.css Normal file
View file

@ -0,0 +1,36 @@
.title {
font-size: 2rem;
color: white;
max-width: 20rem;
margin: 0;
padding: 0;
margin-bottom: 3.5rem;
}
.title-size-medium {
font-size: 1.5rem;
}
.title-accent {
color: #E880C5;
font-weight: 600;
margin: 0;
padding: 0;
margin-bottom: 0.5rem;
margin-top: 1rem;
display: inline-block;
}
.title-accent.title-accent-link {
cursor: pointer;
}
.title-accent.title-accent-link .arrow {
transition: transform 100ms ease-in-out;
transform: translateY(.1rem);
margin-right: .2rem;
}
.title-accent.title-accent-link:hover .arrow {
transform: translateY(.1rem) translateX(-.5rem);
}

25
src/components/Title.js Normal file
View file

@ -0,0 +1,25 @@
import React from 'react';
import { useMovie } from '../hooks/useMovie'
import { Arrow } from '../components/Arrow'
import './Title.css'
// size: "big" | "medium" | "small" | null
// accent: string | null
// accentLink: string | null
export function Title(props) {
const { navigate } = useMovie();
const size = props.size || "big";
const accentLink = props.accentLink || "";
const accent = props.accent || "";
return (
<div>
{accent.length > 0 ? (
<p onClick={ () => accentLink.length > 0 && navigate(accentLink)} className={`title-accent ${accentLink.length > 0 ? 'title-accent-link' : ''}`}>
{accentLink.length > 0 ? (<Arrow left/>) : null}{accent}
</p>
) : null}
<h1 className={"title " + ( size ? 'title-size-' + size : '' )}>{props.children}</h1>
</div>
)
}

View file

@ -0,0 +1,3 @@
.videoElement {
width: 100%;
}

View file

@ -0,0 +1,28 @@
import React from 'react'
import Hls from 'hls.js'
import './VideoElement.css'
// streamUrl: string
export function VideoElement({ streamUrl }) {
const videoRef = React.useRef(null);
React.useEffect(() => {
if (!videoRef || !videoRef.current) return;
const hls = new Hls();
if (!Hls.isSupported() && videoRef.current.canPlayType('application/vnd.apple.mpegurl')) {
videoRef.current.src = streamUrl;
return;
} else if (!Hls.isSupported()) {
return; // TODO show error
}
hls.attachMedia(videoRef.current);
hls.loadSource(streamUrl);
}, [videoRef, streamUrl])
return (
<video className="videoElement" ref={videoRef} controls autoPlay />
)
}

29
src/hooks/useMovie.js Normal file
View file

@ -0,0 +1,29 @@
import React from 'react'
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: "" })
return (
<MovieContext.Provider value={{
navigate(str) {
setPage(str)
},
page,
setStreamUrl: setStream,
streamUrl: stream,
streamData,
setStreamData(d) {
setStreamData(p => ({...p,...d}))
},
}}>
{props.children}
</MovieContext.Provider>
)
}
export function useMovie(props) {
return React.useContext(MovieContext);
}

14
src/index.css Normal file
View file

@ -0,0 +1,14 @@
body, html {
margin: 0;
background-color: #16171D;
min-height: 100vh;
}
body, html, input, button {
font-family: 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 1rem;
}

11
src/index.js Normal file
View file

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

View file

@ -1,3 +1,6 @@
import Fuse from 'fuse.js'
import JSON5 from 'json5'
function getCorsUrl(url) {
return `https://hidden-inlet-27205.herokuapp.com/${url}`;
}
@ -38,41 +41,13 @@ async function getAccessToken(config) {
return "Invalid type provided in config";
}
async function findMovie() {
const searchTerm = document.getElementById('search').value;
sendMessage('info', `Searching for "${searchTerm}"`)
const searchUrl = getCorsUrl(`https://lookmovie.io/api/v1/movies/search/?q=${encodeURIComponent(searchTerm)}`);
const searchRes = await fetch(searchUrl).then((d) => d.json());
let results = [ ...searchRes.result.map((v) => ({ ...v, type: "movie" })) ];
const fuse = new Fuse(results, { threshold: 0.3, distance: 200, keys: ["title"] });
const matchedResults = fuse
.search(searchTerm.toString())
.map((result) => result.item);
let toShow;
if (matchedResults.length > 1) {
const response = window.prompt(`Pick a movie from:\n${matchedResults.map((i, v) => `${v}) ${i.title}`).join('\n')}`, 'Enter number');
toShow = matchedResults[response];
} else {
toShow = matchedResults[0];
}
if (!toShow) {
sendMessage('error', 'Unable to find that, sorry!')
return;
}
sendMessage('info', `Scraping the ${toShow.type} "${toShow.title}"`)
const url = getCorsUrl(`https://lookmovie.io/${toShow.type}s/view/${toShow.slug}`);
async function getStreamUrl(slug, type) {
const url = getCorsUrl(`https://lookmovie.io/${type}s/view/${slug}`);
const pageReq = await fetch(url).then((d) => d.text());
const data = JSON5.parse("{" +
pageReq
.slice(pageReq.indexOf(`${toShow.type}_storage`))
.slice(pageReq.indexOf(`${type}_storage`))
.split("};")[0]
.split("= {")[1]
.trim() +
@ -80,27 +55,45 @@ async function findMovie() {
);
const videoUrl = await getVideoUrl({
slug: toShow.slug,
slug: slug,
movieId: data.id_movie,
type: "movie",
});
sendMessage('info', `Streaming "${toShow.title}"`)
streamVideo(videoUrl)
return { url: videoUrl }
}
function sendMessage(type, message) {
if (!['info', 'error'].includes(type)) return;
document.getElementById(type).innerHTML += `${message}<br>`;
}
async function findMovie(searchTerm) {
const searchUrl = getCorsUrl(`https://lookmovie.io/api/v1/movies/search/?q=${encodeURIComponent(searchTerm)}`);
const searchRes = await fetch(searchUrl).then((d) => d.json());
let results = [...searchRes.result.map((v) => ({ ...v, type: "movie" }))];
function streamVideo(url) {
var video = document.getElementById('video');
const fuse = new Fuse(results, { threshold: 0.3, distance: 200, keys: ["title"] });
const matchedResults = fuse
.search(searchTerm.toString())
.map((result) => result.item);
if (Hls.isSupported()) {
var video = document.getElementById('video');
var hls = new Hls();
hls.attachMedia(video);
hls.loadSource(url);
if (matchedResults.length === 0) {
return { options: [] }
}
if (matchedResults.length > 1) {
const res = { options: [] };
matchedResults.forEach((r) => res.options.push({
title: r.title,
slug: r.slug,
type: r.type
}));
return res;
} else {
const { title, slug, type } = matchedResults[0];
return {
options: [{ title, slug, type }]
}
}
}
export { findMovie, getStreamUrl };

0
src/views/Movie.css Normal file
View file

20
src/views/Movie.js Normal file
View file

@ -0,0 +1,20 @@
import React from 'react'
import { Title } from '../components/Title'
import { Card } from '../components/Card'
import { useMovie } from '../hooks/useMovie'
import { VideoElement } from '../components/VideoElement'
export function MovieView(props) {
const { streamUrl, streamData } = useMovie();
return (
<div className="cardView">
<Card fullWidth>
<Title accent="Return to home" accentLink="search">
{ streamData.title }
</Title>
<VideoElement streamUrl={streamUrl}/>
</Card>
</div>
)
}

15
src/views/NotFound.js Normal file
View file

@ -0,0 +1,15 @@
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>
)
}

17
src/views/Search.css Normal file
View file

@ -0,0 +1,17 @@
.cardView {
display: flex;
min-height: 100vh;
justify-content: center;
align-items: center;
flex-direction: column;
padding: 1rem;
box-sizing: border-box;
}
.cardView > div {
margin-top: 2rem;
}
.cardView > div:first-child {
margin-top: 0;
}

96
src/views/Search.js Normal file
View file

@ -0,0 +1,96 @@
import React from 'react';
import { InputBox } from '../components/InputBox'
import { Title } from '../components/Title'
import { Card } from '../components/Card'
import { MovieRow } from '../components/MovieRow'
import { Progress } from '../components/Progress'
import { findMovie, getStreamUrl } from '../lib/lookMovie'
import { useMovie } from '../hooks/useMovie';
import './Search.css'
export function SearchView() {
const { navigate, setStreamUrl, setStreamData } = useMovie();
const maxSteps = 3;
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 fail = (str) => {
setProgress(maxSteps);
setText(str)
setFailed(true)
}
async function getStream(title, slug, type) {
setStreamUrl("");
try {
setProgress(2);
setText(`Getting stream for "${title}"`)
const { url } = await getStreamUrl(slug, type);
setProgress(maxSteps);
setStreamUrl(url);
setStreamData({
title,
type,
})
setText(`Streaming...`)
navigate("movie")
} catch (err) {
fail("Failed to get stream")
}
}
async function searchMovie(query) {
setFailed(false);
setText(`Searching for "${query}"`);
setProgress(1)
setShowingOptions(false)
try {
const { options } = await findMovie(query)
if (options.length === 0) {
return fail("Could not find that movie")
} else if (options.length > 1) {
setProgress(2);
setText("Choose your movie")
setOptions(options.map(v=>({ title: v.title, slug: v.slug, type: v.type })));
setShowingOptions(true)
return;
}
const { title, slug, type } = options[0];
getStream(title, slug, type);
} catch (err) {
fail("Failed to watch movie")
}
}
return (
<div className="cardView">
<Card>
<Title accent="Because watching movies legally is boring">
What movie do you wanna watch?
</Title>
<InputBox placeholder="Hamilton" onSubmit={(str) => searchMovie(str)} />
<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
</Title>
{options?.map((v, i) => (
<MovieRow key={i} title={v.title} onClick={() => {
setShowingOptions(false)
getStream(v.title, v.slug, v.type)
}}/>
))}
</Card>
</div>
)
}

11417
yarn.lock Normal file

File diff suppressed because it is too large Load diff