diff --git a/.github/workflows/build-deploy.yml b/.github/workflows/build-deploy.yml new file mode 100644 index 00000000..5ce797d5 --- /dev/null +++ b/.github/workflows/build-deploy.yml @@ -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 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..04c5395e --- /dev/null +++ b/.gitignore @@ -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* diff --git a/README.md b/README.md index 8d4c77cf..679d0dca 100644 --- a/README.md +++ b/README.md @@ -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) \ No newline at end of file +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 diff --git a/assets/css/style.css b/assets/css/style.css deleted file mode 100644 index 7801a75c..00000000 --- a/assets/css/style.css +++ /dev/null @@ -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%; -} \ No newline at end of file diff --git a/assets/fonts/JetBrainsMono-Regular.woff2 b/assets/fonts/JetBrainsMono-Regular.woff2 deleted file mode 100644 index 80c62dd1..00000000 Binary files a/assets/fonts/JetBrainsMono-Regular.woff2 and /dev/null differ diff --git a/index.html b/index.html deleted file mode 100644 index 9ec86391..00000000 --- a/index.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - movie-web - - - - - - - - - - -
- -
- -
- -
- -
- -

-

-
-
- - \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..29118a46 --- /dev/null +++ b/package.json @@ -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" + ] + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 00000000..b01aea56 --- /dev/null +++ b/public/index.html @@ -0,0 +1,20 @@ + + + + + + + + + + + movie-web + + + +
+ + diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..9080f78d --- /dev/null +++ b/public/manifest.json @@ -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" +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 00000000..e9e57dc4 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/src/App.js b/src/App.js new file mode 100644 index 00000000..7461c0bd --- /dev/null +++ b/src/App.js @@ -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 + } + + if (page === "movie") { + return + } + + return ( + + ) +} + +function App() { + return ( + + + + ); +} + +export default App; diff --git a/src/components/Arrow.css b/src/components/Arrow.css new file mode 100644 index 00000000..0578ea6d --- /dev/null +++ b/src/components/Arrow.css @@ -0,0 +1,7 @@ +.feather.left { + transform: rotate(180deg); +} + +.arrow { + display: inline-block; +} diff --git a/src/components/Arrow.js b/src/components/Arrow.js new file mode 100644 index 00000000..373f810a --- /dev/null +++ b/src/components/Arrow.js @@ -0,0 +1,14 @@ +import React from 'react' +import './Arrow.css' + +// left?: boolean +export function Arrow(props) { + return ( +
+ + + + +
+ ) +} diff --git a/src/components/Card.css b/src/components/Card.css new file mode 100644 index 00000000..c3f24f63 --- /dev/null +++ b/src/components/Card.css @@ -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); +} \ No newline at end of file diff --git a/src/components/Card.js b/src/components/Card.js new file mode 100644 index 00000000..c7b73e9b --- /dev/null +++ b/src/components/Card.js @@ -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 ( +
+
+ {props.children} +
+
+ ) +} diff --git a/src/components/InputBox.css b/src/components/InputBox.css new file mode 100644 index 00000000..ef774ce0 --- /dev/null +++ b/src/components/InputBox.css @@ -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; +} diff --git a/src/components/InputBox.js b/src/components/InputBox.js new file mode 100644 index 00000000..35e6c1a2 --- /dev/null +++ b/src/components/InputBox.js @@ -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 ( +
{ + e.preventDefault(); + onSubmit(value) + return false; + }}> + setValue(e.target.value)} + /> + +
+ ) +} diff --git a/src/components/MovieRow.css b/src/components/MovieRow.css new file mode 100644 index 00000000..824aca4c --- /dev/null +++ b/src/components/MovieRow.css @@ -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); +} \ No newline at end of file diff --git a/src/components/MovieRow.js b/src/components/MovieRow.js new file mode 100644 index 00000000..47955dec --- /dev/null +++ b/src/components/MovieRow.js @@ -0,0 +1,19 @@ +import React from 'react' +import { Arrow } from './Arrow' +import './MovieRow.css' + +// title: string +// onClick: () => void +export function MovieRow(props) { + return ( +
props.onClick && props.onClick()}> +
+ {props.title} +
+
+

Watch movie

+ +
+
+ ) +} diff --git a/src/components/Progress.css b/src/components/Progress.css new file mode 100644 index 00000000..9c0b84f4 --- /dev/null +++ b/src/components/Progress.css @@ -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; +} diff --git a/src/components/Progress.js b/src/components/Progress.js new file mode 100644 index 00000000..05af5c4c --- /dev/null +++ b/src/components/Progress.js @@ -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 ( +
+ { props.text && props.text.length > 0 ? ( +

{props.text}

) : null} +
+
+
+
+ ) +} \ No newline at end of file diff --git a/src/components/Title.css b/src/components/Title.css new file mode 100644 index 00000000..bd1790ed --- /dev/null +++ b/src/components/Title.css @@ -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); +} diff --git a/src/components/Title.js b/src/components/Title.js new file mode 100644 index 00000000..2740f84c --- /dev/null +++ b/src/components/Title.js @@ -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 ( +
+ {accent.length > 0 ? ( +

accentLink.length > 0 && navigate(accentLink)} className={`title-accent ${accentLink.length > 0 ? 'title-accent-link' : ''}`}> + {accentLink.length > 0 ? () : null}{accent} +

+ ) : null} +

{props.children}

+
+ ) +} diff --git a/src/components/VideoElement.css b/src/components/VideoElement.css new file mode 100644 index 00000000..9a1f47f7 --- /dev/null +++ b/src/components/VideoElement.css @@ -0,0 +1,3 @@ +.videoElement { + width: 100%; +} diff --git a/src/components/VideoElement.js b/src/components/VideoElement.js new file mode 100644 index 00000000..4aa2cd7d --- /dev/null +++ b/src/components/VideoElement.js @@ -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 ( +