mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-10 23:15: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] 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
|
||||||
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 (
|
||||||
|
<>
|
||||||
|
{hasErrored ? (
|
||||||
|
<SkeletonVideoPlayer error />
|
||||||
|
) : isLoading ? (
|
||||||
|
<SkeletonVideoPlayer />
|
||||||
|
) : null}
|
||||||
<video
|
<video
|
||||||
className="bg-denim-500 w-full rounded-xl"
|
className={`bg-denim-500 w-full rounded-xl ${
|
||||||
|
!showVideo ? "hidden" : ""
|
||||||
|
}`}
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
onProgress={(e) =>
|
onProgress={(e) =>
|
||||||
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
|
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)
|
||||||
}
|
}
|
||||||
onLoadedData={(e) => {
|
onLoadedData={(e) => {
|
||||||
|
setLoading(false);
|
||||||
if (props.startAt)
|
if (props.startAt)
|
||||||
(e.target as HTMLVideoElement).currentTime = props.startAt;
|
(e.target as HTMLVideoElement).currentTime = props.startAt;
|
||||||
}}
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
console.error("failed to playback stream", e);
|
||||||
|
setErrored(true);
|
||||||
|
}}
|
||||||
controls
|
controls
|
||||||
autoPlay
|
autoPlay
|
||||||
>
|
>
|
||||||
{!mustUseHls ? <source src={props.source.url} type="video/mp4" /> : null}
|
{!mustUseHls ? (
|
||||||
|
<source src={props.source.url} type="video/mp4" />
|
||||||
|
) : null}
|
||||||
</video>
|
</video>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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
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