Browse Source
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com> Co-authored-by: James Hawkins <jhawki2005@gmail.com>pull/138/head
24 changed files with 548 additions and 88 deletions
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
import { useVideoPlayerState } from "../VideoContext"; |
||||
import { PauseControl } from "./PauseControl"; |
||||
import { SkipTimeBackward, SkipTimeForward } from "./TimeControl"; |
||||
|
||||
export function MobileCenterControl() { |
||||
const { videoState } = useVideoPlayerState(); |
||||
|
||||
const isLoading = videoState.isFirstLoading || videoState.isLoading; |
||||
|
||||
return ( |
||||
<div className="flex items-center space-x-8"> |
||||
<SkipTimeBackward /> |
||||
<PauseControl |
||||
iconSize="text-5xl" |
||||
className={isLoading ? "pointer-events-none opacity-0" : ""} |
||||
/> |
||||
<SkipTimeForward /> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,14 @@
@@ -0,0 +1,14 @@
|
||||
import { useContext } from "react"; |
||||
import { VideoPlayerContext } from "../VideoContext"; |
||||
|
||||
export function QualityDisplayControl() { |
||||
const videoPlayerContext = useContext(VideoPlayerContext); |
||||
|
||||
return ( |
||||
<div className="rounded-md bg-denim-300 py-1 px-2 transition-colors"> |
||||
<p className="text-center text-xs font-bold text-slate-300 transition-colors"> |
||||
{videoPlayerContext.quality} |
||||
</p> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,185 @@
@@ -0,0 +1,185 @@
|
||||
import { useParams } from "react-router-dom"; |
||||
import { useCallback, useContext, useMemo, useState } from "react"; |
||||
import { Icon, Icons } from "@/components/Icon"; |
||||
import { getProviders } from "@/backend/helpers/register"; |
||||
import { useLoading } from "@/hooks/useLoading"; |
||||
import { DetailedMeta } from "@/backend/metadata/getmeta"; |
||||
import { MWMediaType } from "@/backend/metadata/types"; |
||||
import { MWProviderScrapeResult } from "@/backend/helpers/provider"; |
||||
import { runProvider } from "@/backend/helpers/run"; |
||||
import { IconPatch } from "@/components/buttons/IconPatch"; |
||||
import { Loading } from "@/components/layout/Loading"; |
||||
import { |
||||
useVideoPlayerState, |
||||
VideoPlayerDispatchContext, |
||||
} from "../VideoContext"; |
||||
import { VideoPlayerIconButton } from "../parts/VideoPlayerIconButton"; |
||||
import { VideoPopout } from "../parts/VideoPopout"; |
||||
|
||||
interface Props { |
||||
className?: string; |
||||
media?: DetailedMeta; |
||||
} |
||||
|
||||
function PopoutSourceSelect(props: { media: DetailedMeta }) { |
||||
const dispatch = useContext(VideoPlayerDispatchContext); |
||||
const providers = useMemo( |
||||
() => getProviders().filter((v) => v.type.includes(props.media.meta.type)), |
||||
[props] |
||||
); |
||||
const { episode, season } = useParams<{ episode: string; season: string }>(); |
||||
const [selected, setSelected] = useState<string | null>(null); |
||||
const selectedProvider = useMemo( |
||||
() => providers.find((v) => v.id === selected), |
||||
[selected, providers] |
||||
); |
||||
|
||||
const [scrapeData, setScrapeData] = useState<MWProviderScrapeResult | null>( |
||||
null |
||||
); |
||||
const [scrapeProvider, loadingProvider, errorProvider] = useLoading( |
||||
async (providerId: string) => { |
||||
const theProvider = providers.find((v) => v.id === providerId); |
||||
if (!theProvider) throw new Error("Invalid provider"); |
||||
return runProvider(theProvider, { |
||||
media: props.media, |
||||
progress: () => {}, |
||||
type: props.media.meta.type, |
||||
episode: (props.media.meta.type === MWMediaType.SERIES |
||||
? episode |
||||
: undefined) as any, |
||||
season: (props.media.meta.type === MWMediaType.SERIES |
||||
? season |
||||
: undefined) as any, |
||||
}); |
||||
} |
||||
); |
||||
|
||||
// TODO add embed support
|
||||
// TODO restore startAt when changing source
|
||||
// TODO auto choose when only one option
|
||||
// TODO close when selecting item
|
||||
// TODO show currently selected provider
|
||||
// TODO clear error state when switching
|
||||
// const [scrapeEmbed, embedLoading, embedError] = useLoading(
|
||||
// async (embed: MWEmbed) => {
|
||||
// if (!embed.type) throw new Error("Invalid embed type");
|
||||
// const theScraper = getEmbedScraperByType(embed.type);
|
||||
// if (!theScraper) throw new Error("Invalid scraper");
|
||||
// return runEmbedScraper(theScraper, {
|
||||
// progress: () => {},
|
||||
// url: embed.url,
|
||||
// });
|
||||
// }
|
||||
// );
|
||||
|
||||
const selectProvider = useCallback( |
||||
(id: string) => { |
||||
scrapeProvider(id).then((v) => { |
||||
if (!v) throw new Error("No scrape result"); |
||||
setScrapeData(v); |
||||
}); |
||||
setSelected(id); |
||||
}, |
||||
[setSelected, scrapeProvider] |
||||
); |
||||
|
||||
if (!selectedProvider) |
||||
return ( |
||||
<> |
||||
<div className="flex items-center space-x-3 border-b border-denim-500 p-4 font-bold text-white"> |
||||
<span>Select video source</span> |
||||
</div> |
||||
<div className="overflow-y-auto p-4"> |
||||
<div className="space-y-1"> |
||||
{providers.map((e) => ( |
||||
<div |
||||
className="text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600" |
||||
onClick={() => selectProvider(e.id)} |
||||
key={e.id} |
||||
> |
||||
{e.displayName} |
||||
</div> |
||||
))} |
||||
</div> |
||||
</div> |
||||
</> |
||||
); |
||||
|
||||
return ( |
||||
<> |
||||
<div className="flex items-center space-x-3 border-b border-denim-500 p-4 font-bold text-white"> |
||||
<button |
||||
className="-m-1.5 rounded p-1.5 hover:bg-denim-600" |
||||
onClick={() => setSelected(null)} |
||||
type="button" |
||||
> |
||||
<Icon icon={Icons.CHEVRON_LEFT} /> |
||||
</button> |
||||
<span>{selectedProvider.displayName}</span> |
||||
</div> |
||||
<div className="overflow-y-auto p-4 text-white"> |
||||
{loadingProvider ? ( |
||||
<div className="flex h-full w-full items-center justify-center"> |
||||
<Loading /> |
||||
</div> |
||||
) : errorProvider ? ( |
||||
<div className="flex h-full w-full items-center justify-center"> |
||||
<div className="flex flex-col flex-wrap items-center text-slate-400"> |
||||
<IconPatch |
||||
icon={Icons.EYE_SLASH} |
||||
className="text-xl text-bink-600" |
||||
/> |
||||
<p className="mt-6 w-full text-center"> |
||||
Something went wrong loading streams. |
||||
</p> |
||||
</div> |
||||
</div> |
||||
) : scrapeData ? ( |
||||
<div> |
||||
{scrapeData.stream ? ( |
||||
<div |
||||
className="text-denim-800 -mx-2 flex items-center space-x-1 rounded p-2 text-white hover:bg-denim-600" |
||||
onClick={() => |
||||
scrapeData.stream && |
||||
dispatch({ |
||||
url: scrapeData.stream.streamUrl, |
||||
quality: scrapeData.stream.quality, |
||||
sourceType: scrapeData.stream.type, |
||||
type: "SET_SOURCE", |
||||
}) |
||||
} |
||||
> |
||||
{selectedProvider.displayName} |
||||
</div> |
||||
) : null} |
||||
</div> |
||||
) : null} |
||||
</div> |
||||
</> |
||||
); |
||||
} |
||||
|
||||
export function SourceSelectionControl(props: Props) { |
||||
const { videoState } = useVideoPlayerState(); |
||||
|
||||
if (!props.media) return null; |
||||
|
||||
return ( |
||||
<div className={props.className}> |
||||
<div className="relative"> |
||||
<VideoPopout |
||||
id="source" |
||||
className="grid grid-rows-[auto,minmax(0,1fr)]" |
||||
> |
||||
<PopoutSourceSelect media={props.media} /> |
||||
</VideoPopout> |
||||
<VideoPlayerIconButton |
||||
icon={Icons.FILE} |
||||
text="Video source" |
||||
onClick={() => videoState.openPopout("source")} |
||||
/> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useRef, useState } from "react"; |
||||
|
||||
export function useIsMobile() { |
||||
const [isMobile, setIsMobile] = useState(false); |
||||
const isMobileCurrent = useRef<boolean | null>(false); |
||||
|
||||
useEffect(() => { |
||||
function onResize() { |
||||
const value = window.innerWidth < 1024; |
||||
const isChanged = isMobileCurrent.current !== value; |
||||
if (!isChanged) return; |
||||
|
||||
isMobileCurrent.current = value; |
||||
setIsMobile(value); |
||||
} |
||||
|
||||
onResize(); |
||||
window.addEventListener("resize", onResize); |
||||
|
||||
return () => { |
||||
window.removeEventListener("resize", onResize); |
||||
}; |
||||
}, []); |
||||
|
||||
return { |
||||
isMobile, |
||||
}; |
||||
} |
@ -0,0 +1,22 @@
@@ -0,0 +1,22 @@
|
||||
import { useVideoPlayerState } from "@/components/video/VideoContext"; |
||||
import { useState } from "react"; |
||||
|
||||
export function useVolumeControl() { |
||||
const [storedVolume, setStoredVolume] = useState(1); |
||||
const { videoState } = useVideoPlayerState(); |
||||
|
||||
const toggleVolume = () => { |
||||
if (videoState.volume > 0) { |
||||
setStoredVolume(videoState.volume); |
||||
videoState.setVolume(0); |
||||
} else { |
||||
videoState.setVolume(storedVolume > 0 ? storedVolume : 1); |
||||
} |
||||
}; |
||||
|
||||
return { |
||||
storedVolume, |
||||
setStoredVolume, |
||||
toggleVolume, |
||||
}; |
||||
} |
Loading…
Reference in new issue