Browse Source
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com> Co-authored-by: James Hawkins <jhawki2005@gmail.com>pull/138/head
25 changed files with 284 additions and 125 deletions
@ -0,0 +1,34 @@ |
|||||||
|
import { mwFetch, proxiedFetch } from "@/backend/helpers/fetch"; |
||||||
|
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams"; |
||||||
|
import toWebVTT from "srt-webvtt"; |
||||||
|
|
||||||
|
export async function getCaptionUrl(caption: MWCaption): Promise<string> { |
||||||
|
if (caption.type === MWCaptionType.SRT) { |
||||||
|
let captionBlob: Blob; |
||||||
|
|
||||||
|
if (caption.needsProxy) { |
||||||
|
captionBlob = await proxiedFetch<Blob>(caption.url, { |
||||||
|
responseType: "blob" as any, |
||||||
|
}); |
||||||
|
} else { |
||||||
|
captionBlob = await mwFetch<Blob>(caption.url, { |
||||||
|
responseType: "blob" as any, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return toWebVTT(captionBlob); |
||||||
|
} |
||||||
|
|
||||||
|
if (caption.type === MWCaptionType.VTT) { |
||||||
|
if (caption.needsProxy) { |
||||||
|
const blob = await proxiedFetch<Blob>(caption.url, { |
||||||
|
responseType: "blob" as any, |
||||||
|
}); |
||||||
|
return URL.createObjectURL(blob); |
||||||
|
} |
||||||
|
|
||||||
|
return caption.url; |
||||||
|
} |
||||||
|
|
||||||
|
throw new Error("invalid type"); |
||||||
|
} |
@ -1,5 +1,9 @@ |
|||||||
import "./Spinner.css"; |
import "./Spinner.css"; |
||||||
|
|
||||||
export function Spinner() { |
interface SpinnerProps { |
||||||
return <div className="spinner" />; |
className: string; |
||||||
|
} |
||||||
|
|
||||||
|
export function Spinner(props: SpinnerProps) { |
||||||
|
return <div className={["spinner", props.className].join(" ")} />; |
||||||
} |
} |
||||||
|
@ -1,14 +1,70 @@ |
|||||||
import { PopoutSection } from "./PopoutUtils"; |
import { getCaptionUrl } from "@/backend/helpers/captions"; |
||||||
|
import { MWCaption } from "@/backend/helpers/streams"; |
||||||
|
import { Icon, Icons } from "@/components/Icon"; |
||||||
|
import { useLoading } from "@/hooks/useLoading"; |
||||||
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks"; |
||||||
|
import { useControls } from "@/video/state/logic/controls"; |
||||||
|
import { useMeta } from "@/video/state/logic/meta"; |
||||||
|
import { useSource } from "@/video/state/logic/source"; |
||||||
|
import { useMemo, useRef } from "react"; |
||||||
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; |
||||||
|
|
||||||
|
function makeCaptionId(caption: MWCaption, isLinked: boolean): string { |
||||||
|
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`; |
||||||
|
} |
||||||
|
|
||||||
|
// TODO add option to clear captions
|
||||||
export function CaptionSelectionPopout() { |
export function CaptionSelectionPopout() { |
||||||
|
const descriptor = useVideoPlayerDescriptor(); |
||||||
|
const meta = useMeta(descriptor); |
||||||
|
const source = useSource(descriptor); |
||||||
|
const controls = useControls(descriptor); |
||||||
|
const linkedCaptions = useMemo( |
||||||
|
() => |
||||||
|
meta?.captions.map((v) => ({ ...v, id: makeCaptionId(v, true) })) ?? [], |
||||||
|
[meta] |
||||||
|
); |
||||||
|
const loadingId = useRef<string>(""); |
||||||
|
const [setCaption, loading, error] = useLoading( |
||||||
|
async (caption: MWCaption, isLinked: boolean) => { |
||||||
|
const id = makeCaptionId(caption, isLinked); |
||||||
|
loadingId.current = id; |
||||||
|
controls.setCaption(id, await getCaptionUrl(caption)); |
||||||
|
controls.closePopout(); |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
const currentCaption = source.source?.caption?.id; |
||||||
|
|
||||||
return ( |
return ( |
||||||
<> |
<> |
||||||
<PopoutSection className="bg-ash-100 font-bold text-white"> |
<PopoutSection className="bg-ash-100 font-bold text-white"> |
||||||
<div>Captions</div> |
<div>Captions</div> |
||||||
</PopoutSection> |
</PopoutSection> |
||||||
<PopoutSection> |
<div className="relative overflow-y-auto"> |
||||||
<div>Hi Jeebies</div> |
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-200 px-5 py-3 text-sm font-bold uppercase"> |
||||||
</PopoutSection> |
<Icon className="text-base" icon={Icons.LINK} /> |
||||||
|
<span>Linked captions</span> |
||||||
|
</p> |
||||||
|
<PopoutSection className="pt-0"> |
||||||
|
<div> |
||||||
|
{linkedCaptions.map((link) => ( |
||||||
|
<PopoutListEntry |
||||||
|
key={link.langIso} |
||||||
|
active={link.id === currentCaption} |
||||||
|
loading={loading && link.id === loadingId.current} |
||||||
|
errored={error && link.id === loadingId.current} |
||||||
|
onClick={() => { |
||||||
|
loadingId.current = link.id; |
||||||
|
setCaption(link, true); |
||||||
|
}} |
||||||
|
> |
||||||
|
{link.langIso} |
||||||
|
</PopoutListEntry> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
</PopoutSection> |
||||||
|
</div> |
||||||
</> |
</> |
||||||
); |
); |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue