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] Styling for pages
- [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
- [ ] implement source that are not mp4
- [ ] Subtitles
@ -61,7 +61,7 @@ Check out [this project's issues](https://github.com/JamesHawkinss/movie-web/iss
- [ ] Get rid of react warnings
- [ ] Implement more scrapers
- [ ] 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)
## After all rewrite code has been written

View file

@ -1,11 +1,16 @@
import { Icon, Icons } from 'components/Icon'
export function BrandPill() {
import { Icon, Icons } from "components/Icon";
export function BrandPill(props: { clickable?: boolean }) {
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} />
<span className="font-semibold text-white">movie-web</span>
</div>
)
);
}

View file

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

View file

@ -2,7 +2,7 @@ import { IconPatch } from "components/buttons/IconPatch";
import { Icons } from "components/Icon";
import { Loading } from "components/layout/Loading";
import { MWMediaStream } from "providers";
import { useRef } from "react";
import { useEffect, useRef, useState } from "react";
export interface VideoPlayerProps {
source: MWMediaStream;
@ -30,23 +30,48 @@ export function SkeletonVideoPlayer(props: { error?: boolean }) {
export function VideoPlayer(props: VideoPlayerProps) {
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";
// reset if stream url changes
useEffect(() => {
setLoading(true);
setErrored(false);
}, [props.source.url]);
return (
<video
className="bg-denim-500 w-full rounded-xl"
ref={videoRef}
onProgress={(e) =>
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
}
onLoadedData={(e) => {
if (props.startAt)
(e.target as HTMLVideoElement).currentTime = props.startAt;
}}
controls
autoPlay
>
{!mustUseHls ? <source src={props.source.url} type="video/mp4" /> : null}
</video>
<>
{hasErrored ? (
<SkeletonVideoPlayer error />
) : isLoading ? (
<SkeletonVideoPlayer />
) : null}
<video
className={`bg-denim-500 w-full rounded-xl ${
!showVideo ? "hidden" : ""
}`}
ref={videoRef}
onProgress={(e) =>
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
}
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";
export function useSearchQuery(): [MWQuery, (inp: Partial<MWQuery>) => void] {
const history = useHistory()
const { path, params } = useRouteMatch<{ type: string, query: string}>()
const history = useHistory();
const { path, params } = useRouteMatch<{ type: string; query: string }>();
const [search, setSearch] = useState<MWQuery>({
searchQuery: "",
type: MWMediaType.MOVIE,
});
const updateParams = (inp: Partial<MWQuery>) => {
const copySearch: MWQuery = {...search};
const copySearch: MWQuery = { ...search };
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(() => {
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 || "";
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