add hover state to brand pill - use replace instead of push for search url - video loading and error state - extra elaboration of providers in readme

This commit is contained in:
mrjvs 2022-02-28 22:00:32 +01:00
parent b498735746
commit d72e98eb1e
6 changed files with 114 additions and 39 deletions

View File

@ -53,7 +53,7 @@ Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/iss
- [x] Global state for media objects - [x] Global state for media objects
- [x] Styling for pages - [x] Styling for pages
- [x] loading stream player view + error - [x] loading stream player view + error
- [ ] video load error, video loading (from actual video player) - [x] video load error, video loading (from actual video player)
- [ ] Series episodes+seasons - [ ] Series episodes+seasons
- [ ] implement source that are not mp4 - [ ] implement source that are not mp4
- [ ] Subtitles - [ ] Subtitles
@ -61,7 +61,7 @@ Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/iss
- [ ] Get rid of react warnings - [ ] Get rid of react warnings
- [ ] Implement more scrapers - [ ] Implement more scrapers
- [ ] Add 404 page for media (media not found, provider disabled, provider not found) & general (page not found) - [ ] Add 404 page for media (media not found, provider disabled, provider not found) & general (page not found)
- [ ] Brand tag hover state and cursor - [x] Brand tag hover state and cursor
- [ ] Handle disabled providers (continue watching, bookmarks & router) - [ ] Handle disabled providers (continue watching, bookmarks & router)
## After all rewrite code has been written ## After all rewrite code has been written

View File

@ -1,11 +1,16 @@
import { Icon, Icons } from 'components/Icon' import { Icon, Icons } from "components/Icon";
export function BrandPill() {
export function BrandPill(props: { clickable?: boolean }) {
return ( return (
<div className="bg-bink-100 bg-opacity-50 text-bink-600 rounded-full flex items-center space-x-2 px-4 py-2"> <div
className={`bg-bink-100 text-bink-600 flex items-center space-x-2 rounded-full bg-opacity-50 px-4 py-2 ${
props.clickable
? "hover:bg-bink-200 hover:text-bink-700 transition-[transform,background-color] hover:scale-105 active:scale-95"
: ""
}`}
>
<Icon className="text-xl" icon={Icons.MOVIE_WEB} /> <Icon className="text-xl" icon={Icons.MOVIE_WEB} />
<span className="font-semibold text-white">movie-web</span> <span className="font-semibold text-white">movie-web</span>
</div> </div>
) );
} }

View File

@ -2,7 +2,7 @@ import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon"; import { Icons } from "components/Icon";
import { DISCORD_LINK, GITHUB_LINK } from "mw_constants"; import { DISCORD_LINK, GITHUB_LINK } from "mw_constants";
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Link } from "react-router-dom" import { Link } from "react-router-dom";
import { BrandPill } from "./BrandPill"; import { BrandPill } from "./BrandPill";
export interface NavigationProps { export interface NavigationProps {
@ -11,19 +11,33 @@ export interface NavigationProps {
export function Navigation(props: NavigationProps) { export function Navigation(props: NavigationProps) {
return ( return (
<div className="flex justify-between items-center absolute left-0 right-0 top-0 py-5 px-7"> <div className="absolute left-0 right-0 top-0 flex items-center justify-between py-5 px-7">
<div className="flex justify-center items-center"> <div className="flex items-center justify-center">
<div className="mr-6"> <div className="mr-6">
<Link to="/"> <Link to="/">
<BrandPill/> <BrandPill clickable />
</Link> </Link>
</div> </div>
{props.children} {props.children}
</div> </div>
<div className="flex"> <div className="flex">
<a href={DISCORD_LINK} target="_blank" rel="noreferrer" className="text-2xl text-white"><IconPatch icon={Icons.DISCORD} clickable/></a> <a
<a href={GITHUB_LINK} target="_blank" rel="noreferrer" className="text-2xl text-white"><IconPatch icon={Icons.GITHUB} clickable/></a> href={DISCORD_LINK}
target="_blank"
rel="noreferrer"
className="text-2xl text-white"
>
<IconPatch icon={Icons.DISCORD} clickable />
</a>
<a
href={GITHUB_LINK}
target="_blank"
rel="noreferrer"
className="text-2xl text-white"
>
<IconPatch icon={Icons.GITHUB} clickable />
</a>
</div> </div>
</div> </div>
) );
} }

View File

@ -2,7 +2,7 @@ import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon"; import { Icons } from "components/Icon";
import { Loading } from "components/layout/Loading"; import { Loading } from "components/layout/Loading";
import { MWMediaStream } from "providers"; import { MWMediaStream } from "providers";
import { useRef } from "react"; import { useEffect, useRef, useState } from "react";
export interface VideoPlayerProps { export interface VideoPlayerProps {
source: MWMediaStream; source: MWMediaStream;
@ -30,23 +30,48 @@ export function SkeletonVideoPlayer(props: { error?: boolean }) {
export function VideoPlayer(props: VideoPlayerProps) { export function VideoPlayer(props: VideoPlayerProps) {
const videoRef = useRef<HTMLVideoElement | null>(null); const videoRef = useRef<HTMLVideoElement | null>(null);
const [hasErrored, setErrored] = useState(false);
const [isLoading, setLoading] = useState(true);
const showVideo = !isLoading && !hasErrored;
const mustUseHls = props.source.type === "m3u8"; const mustUseHls = props.source.type === "m3u8";
// reset if stream url changes
useEffect(() => {
setLoading(true);
setErrored(false);
}, [props.source.url]);
return ( return (
<video <>
className="bg-denim-500 w-full rounded-xl" {hasErrored ? (
ref={videoRef} <SkeletonVideoPlayer error />
onProgress={(e) => ) : isLoading ? (
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent) <SkeletonVideoPlayer />
} ) : null}
onLoadedData={(e) => { <video
if (props.startAt) className={`bg-denim-500 w-full rounded-xl ${
(e.target as HTMLVideoElement).currentTime = props.startAt; !showVideo ? "hidden" : ""
}} }`}
controls ref={videoRef}
autoPlay onProgress={(e) =>
> props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
{!mustUseHls ? <source src={props.source.url} type="video/mp4" /> : null} }
</video> onLoadedData={(e) => {
setLoading(false);
if (props.startAt)
(e.target as HTMLVideoElement).currentTime = props.startAt;
}}
onError={(e) => {
console.error("failed to playback stream", e);
setErrored(true);
}}
controls
autoPlay
>
{!mustUseHls ? (
<source src={props.source.url} type="video/mp4" />
) : null}
</video>
</>
); );
} }

View File

@ -3,24 +3,32 @@ import React, { useState } from "react";
import { generatePath, useHistory, useRouteMatch } from "react-router-dom"; import { generatePath, useHistory, useRouteMatch } from "react-router-dom";
export function useSearchQuery(): [MWQuery, (inp: Partial<MWQuery>) => void] { export function useSearchQuery(): [MWQuery, (inp: Partial<MWQuery>) => void] {
const history = useHistory() const history = useHistory();
const { path, params } = useRouteMatch<{ type: string, query: string}>() const { path, params } = useRouteMatch<{ type: string; query: string }>();
const [search, setSearch] = useState<MWQuery>({ const [search, setSearch] = useState<MWQuery>({
searchQuery: "", searchQuery: "",
type: MWMediaType.MOVIE, type: MWMediaType.MOVIE,
}); });
const updateParams = (inp: Partial<MWQuery>) => { const updateParams = (inp: Partial<MWQuery>) => {
const copySearch: MWQuery = {...search}; const copySearch: MWQuery = { ...search };
Object.assign(copySearch, inp); Object.assign(copySearch, inp);
history.push(generatePath(path, { query: copySearch.searchQuery.length === 0 ? undefined : inp.searchQuery, type: copySearch.type })) history.replace(
} generatePath(path, {
query:
copySearch.searchQuery.length === 0 ? undefined : inp.searchQuery,
type: copySearch.type,
})
);
};
React.useEffect(() => { React.useEffect(() => {
const type = Object.values(MWMediaType).find(v=>params.type === v) || MWMediaType.MOVIE; const type =
Object.values(MWMediaType).find((v) => params.type === v) ||
MWMediaType.MOVIE;
const searchQuery = params.query || ""; const searchQuery = params.query || "";
setSearch({ type, searchQuery }); setSearch({ type, searchQuery });
}, [params, setSearch]) }, [params, setSearch]);
return [search, updateParams] return [search, updateParams];
} }

23
src/providers/README.md Normal file
View File

@ -0,0 +1,23 @@
# the providers
to make this as clear as possible, here is some extra information on how the interal system works regarding providers.
| Term | explanation |
| ------------- | ------------------------------------------------------------------------------------- |
| Media | Object containing information about a piece of media. like title and its id's |
| PortableMedia | Object with just the identifiers of a piece of media. used for transport and saving |
| MediaStream | Object with a stream url in it. use it to view a piece of media. |
| Provider | group of methods to generate media and mediastreams from a source. aliased as scraper |
All types are prefixed with MW (MovieWeb) to prevent clashing names.
## Some rules
1. **Never** remove a provider completely if it's been in use before. just disable it.
2. **Never** change the ID of a provider if it's been in use before.
3. **Never** change system of the media ID of a provider without making it backwards compatible
All these rules are because `PortableMedia` objects need to stay functional. because:
- It's used for routing, links would stop working
- It's used for storage, continue watching and bookmarks would stop working