12 changed files with 304 additions and 9 deletions
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
import { Icons } from "@/components/Icon"; |
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||
import { VideoPlayerIconButton } from "@/video/components/parts/VideoPlayerIconButton"; |
||||
import { useControls } from "@/video/state/logic/controls"; |
||||
import { PopoutAnchor } from "@/video/components/popouts/PopoutAnchor"; |
||||
import { useInterface } from "@/video/state/logic/interface"; |
||||
|
||||
interface Props { |
||||
className?: string; |
||||
} |
||||
|
||||
export function SourceSelectionAction(props: Props) { |
||||
const descriptor = useVideoPlayerDescriptor(); |
||||
const videoInterface = useInterface(descriptor); |
||||
const controls = useControls(descriptor); |
||||
|
||||
return ( |
||||
<div className={props.className}> |
||||
<div className="relative"> |
||||
<PopoutAnchor for="source"> |
||||
<VideoPlayerIconButton |
||||
active={videoInterface.popout === "source"} |
||||
icon={Icons.FILE} |
||||
text="Source" |
||||
wide |
||||
onClick={() => controls.openPopout("source")} |
||||
/> |
||||
</PopoutAnchor> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,205 @@
@@ -0,0 +1,205 @@
|
||||
import { useMemo, useRef, useState } from "react"; |
||||
import { Icon, Icons } from "@/components/Icon"; |
||||
import { useLoading } from "@/hooks/useLoading"; |
||||
import { Loading } from "@/components/layout/Loading"; |
||||
import { IconPatch } from "@/components/buttons/IconPatch"; |
||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||
import { useMeta } from "@/video/state/logic/meta"; |
||||
import { useControls } from "@/video/state/logic/controls"; |
||||
import { MWStream } from "@/backend/helpers/streams"; |
||||
import { getProviders } from "@/backend/helpers/register"; |
||||
import { runProvider } from "@/backend/helpers/run"; |
||||
import { MWProviderScrapeResult } from "@/backend/helpers/provider"; |
||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; |
||||
|
||||
// TODO HLS does not work
|
||||
export function SourceSelectionPopout() { |
||||
const descriptor = useVideoPlayerDescriptor(); |
||||
const controls = useControls(descriptor); |
||||
const meta = useMeta(descriptor); |
||||
const providers = useMemo( |
||||
() => |
||||
meta ? getProviders().filter((v) => v.type.includes(meta.meta.type)) : [], |
||||
[meta] |
||||
); |
||||
|
||||
const [selectedProvider, setSelectedProvider] = useState<string | null>(null); |
||||
const [scrapeResult, setScrapeResult] = |
||||
useState<MWProviderScrapeResult | null>(null); |
||||
const showingProvider = !!selectedProvider; |
||||
const selectedProviderPopulated = useMemo( |
||||
() => providers.find((v) => v.id === selectedProvider) ?? null, |
||||
[providers, selectedProvider] |
||||
); |
||||
const [runScraper, loading, error] = useLoading( |
||||
async (providerId: string) => { |
||||
const theProvider = providers.find((v) => v.id === providerId); |
||||
if (!theProvider) throw new Error("Invalid provider"); |
||||
if (!meta) throw new Error("need meta"); |
||||
return runProvider(theProvider, { |
||||
media: { |
||||
imdbId: "", // TODO get actual ids
|
||||
tmdbId: "", |
||||
meta: meta.meta, |
||||
}, |
||||
progress: () => {}, |
||||
type: meta.meta.type, |
||||
episode: meta.episode?.episodeId as any, |
||||
season: meta.episode?.seasonId as any, |
||||
}); |
||||
} |
||||
); |
||||
|
||||
function selectSource(stream: MWStream) { |
||||
controls.setSource({ |
||||
quality: stream.quality, |
||||
source: stream.streamUrl, |
||||
type: stream.type, |
||||
}); |
||||
if (meta) { |
||||
controls.setMeta({ |
||||
...meta, |
||||
captions: stream.captions, |
||||
}); |
||||
} |
||||
controls.closePopout(); |
||||
} |
||||
|
||||
const providerRef = useRef<string | null>(null); |
||||
const selectProvider = (providerId?: string) => { |
||||
if (!providerId) { |
||||
providerRef.current = null; |
||||
setSelectedProvider(null); |
||||
return; |
||||
} |
||||
|
||||
runScraper(providerId).then((v) => { |
||||
if (!providerRef.current) return; |
||||
if (v) { |
||||
const len = v.embeds.length + (v.stream ? 1 : 0); |
||||
if (len === 1) { |
||||
const realStream = v.stream; |
||||
if (!realStream) { |
||||
// TODO scrape embed
|
||||
throw new Error("no embed scraper configured"); |
||||
} |
||||
selectSource(realStream); |
||||
return; |
||||
} |
||||
} |
||||
setScrapeResult(v ?? null); |
||||
}); |
||||
providerRef.current = providerId; |
||||
setSelectedProvider(providerId); |
||||
}; |
||||
|
||||
const titlePositionClass = useMemo(() => { |
||||
const offset = !showingProvider ? "left-0" : "left-10"; |
||||
return [ |
||||
"absolute w-full transition-[left,opacity] duration-200", |
||||
offset, |
||||
].join(" "); |
||||
}, [showingProvider]); |
||||
|
||||
return ( |
||||
<> |
||||
<PopoutSection className="bg-ash-100 font-bold text-white"> |
||||
<div className="relative flex items-center"> |
||||
<button |
||||
className={[ |
||||
"-m-1.5 rounded-lg p-1.5 transition-opacity duration-100 hover:bg-ash-200", |
||||
!showingProvider ? "pointer-events-none opacity-0" : "opacity-1", |
||||
].join(" ")} |
||||
onClick={() => selectProvider()} |
||||
type="button" |
||||
> |
||||
<Icon icon={Icons.CHEVRON_LEFT} /> |
||||
</button> |
||||
<span |
||||
className={[ |
||||
titlePositionClass, |
||||
showingProvider ? "opacity-1" : "opacity-0", |
||||
].join(" ")} |
||||
> |
||||
{selectedProviderPopulated?.displayName ?? ""} |
||||
</span> |
||||
<span |
||||
className={[ |
||||
titlePositionClass, |
||||
!showingProvider ? "opacity-1" : "opacity-0", |
||||
].join(" ")} |
||||
> |
||||
Sources |
||||
</span> |
||||
</div> |
||||
</PopoutSection> |
||||
<div className="relative grid h-full grid-rows-[minmax(1px,1fr)]"> |
||||
<PopoutSection |
||||
className={[ |
||||
"absolute inset-0 z-30 overflow-y-auto border-ash-400 bg-ash-100 transition-[max-height,padding] duration-200", |
||||
showingProvider |
||||
? "max-h-full border-t" |
||||
: "max-h-0 overflow-hidden py-0", |
||||
].join(" ")} |
||||
> |
||||
{loading ? ( |
||||
<div className="flex h-full w-full items-center justify-center"> |
||||
<Loading /> |
||||
</div> |
||||
) : error ? ( |
||||
<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 the embeds for this thing that |
||||
you like |
||||
</p> |
||||
</div> |
||||
</div> |
||||
) : ( |
||||
<> |
||||
{scrapeResult?.stream ? ( |
||||
<PopoutListEntry |
||||
isOnDarkBackground |
||||
onClick={() => { |
||||
if (scrapeResult.stream) selectSource(scrapeResult.stream); |
||||
}} |
||||
> |
||||
Native source |
||||
</PopoutListEntry> |
||||
) : null} |
||||
{scrapeResult?.embeds.map((v) => ( |
||||
<PopoutListEntry |
||||
isOnDarkBackground |
||||
key={v.url} |
||||
onClick={() => { |
||||
console.log("EMBED CHOSEN"); |
||||
}} |
||||
> |
||||
{v.type} |
||||
</PopoutListEntry> |
||||
))} |
||||
</> |
||||
)} |
||||
</PopoutSection> |
||||
<PopoutSection className="relative h-full overflow-y-auto"> |
||||
<div> |
||||
{providers.map((v) => ( |
||||
<PopoutListEntry |
||||
key={v.id} |
||||
onClick={() => { |
||||
selectProvider(v.id); |
||||
}} |
||||
> |
||||
{v.displayName} |
||||
</PopoutListEntry> |
||||
))} |
||||
</div> |
||||
</PopoutSection> |
||||
</div> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
|
||||
import { resetForSource } from "@/video/state/init"; |
||||
import { updateMediaPlaying } from "@/video/state/logic/mediaplaying"; |
||||
import { updateMisc } from "@/video/state/logic/misc"; |
||||
import { updateProgress } from "@/video/state/logic/progress"; |
||||
import { VideoPlayerState } from "@/video/state/types"; |
||||
|
||||
export function resetStateForSource(descriptor: string, s: VideoPlayerState) { |
||||
const state = s; |
||||
if (state.hlsInstance) { |
||||
state.hlsInstance.destroy(); |
||||
state.hlsInstance = null; |
||||
} |
||||
resetForSource(state); |
||||
updateMediaPlaying(descriptor, state); |
||||
updateProgress(descriptor, state); |
||||
updateMisc(descriptor, state); |
||||
} |
Loading…
Reference in new issue