35 changed files with 12255 additions and 143 deletions
@ -0,0 +1,54 @@
@@ -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 |
@ -0,0 +1,23 @@
@@ -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* |
@ -1,5 +1,5 @@
@@ -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 |
||||
|
@ -1,59 +0,0 @@
@@ -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%; |
||||
} |
Binary file not shown.
@ -1,34 +0,0 @@
@@ -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> |
@ -0,0 +1,42 @@
@@ -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" |
||||
] |
||||
} |
||||
} |
@ -0,0 +1,20 @@
@@ -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> |
@ -0,0 +1,25 @@
@@ -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" |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html |
||||
User-agent: * |
||||
Disallow: |
@ -0,0 +1,31 @@
@@ -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; |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
.feather.left { |
||||
transform: rotate(180deg); |
||||
} |
||||
|
||||
.arrow { |
||||
display: inline-block; |
||||
} |
@ -0,0 +1,14 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,28 @@
@@ -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); |
||||
} |
@ -0,0 +1,28 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,72 @@
@@ -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; |
||||
} |
@ -0,0 +1,26 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,45 @@
@@ -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); |
||||
} |
@ -0,0 +1,19 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,43 @@
@@ -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; |
||||
} |
@ -0,0 +1,21 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,36 @@
@@ -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); |
||||
} |
@ -0,0 +1,25 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
.videoElement { |
||||
width: 100%; |
||||
} |
@ -0,0 +1,28 @@
@@ -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 /> |
||||
) |
||||
} |
@ -0,0 +1,29 @@
@@ -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); |
||||
} |
@ -0,0 +1,14 @@
@@ -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; |
||||
} |
@ -0,0 +1,11 @@
@@ -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') |
||||
); |
@ -0,0 +1,20 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,15 @@
@@ -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> |
||||
) |
||||
} |
@ -0,0 +1,17 @@
@@ -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; |
||||
} |
@ -0,0 +1,96 @@
@@ -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> |
||||
) |
||||
} |
Loading…
Reference in new issue