mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-10 21:45:06 +01:00
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:
parent
b498735746
commit
d72e98eb1e
@ -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
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
23
src/providers/README.md
Normal 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
|
Loading…
Reference in New Issue
Block a user