A small web app for watching movies and shows easily
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

147 lines
5.3 KiB

import {
getCaptionUrl,
parseSubtitles,
subtitleTypeList,
} from "@/backend/helpers/captions";
import { MWCaption, MWCaptionType } from "@/backend/helpers/streams";
import { Icon, Icons } from "@/components/Icon";
import { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingView } from "@/components/popout/FloatingView";
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
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 { useTranslation } from "react-i18next";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
const customCaption = "external-custom";
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
return isLinked ? `linked-${caption.langIso}` : `external-${caption.langIso}`;
}
export function CaptionSelectionPopout(props: {
router: ReturnType<typeof useFloatingRouter>;
prefix: string;
}) {
const { t } = useTranslation();
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;
const blobUrl = await getCaptionUrl(caption);
const result = await fetch(blobUrl);
const text = await result.text();
parseSubtitles(text); // This will throw if the file is invalid
controls.setCaption(id, blobUrl);
// sometimes this doesn't work, so we add a small delay
setTimeout(() => {
controls.closePopout();
}, 100);
}
);
const currentCaption = source.source?.caption?.id;
const customCaptionUploadElement = useRef<HTMLInputElement>(null);
return (
<FloatingView
{...props.router.pageProps(props.prefix)}
width={320}
height={500}
>
<FloatingCardView.Header
title={t("videoPlayer.popouts.captions")}
description={t("videoPlayer.popouts.descriptions.captions")}
goBack={() => props.router.navigate("/")}
action={
<button
type="button"
onClick={() =>
props.router.navigate(`${props.prefix}/caption-settings`)
}
className="flex cursor-pointer items-center space-x-2 transition-colors duration-200 hover:text-white"
>
<span>{t("videoPlayer.popouts.captionPreferences.title")}</span>
<Icon icon={Icons.GEAR} />
</button>
}
/>
<FloatingCardView.Content noSection>
<PopoutSection>
<PopoutListEntry
active={!currentCaption}
onClick={() => {
controls.clearCaption();
controls.closePopout();
}}
>
{t("videoPlayer.popouts.noCaptions")}
</PopoutListEntry>
<PopoutListEntry
key={customCaption}
active={currentCaption === customCaption}
loading={loading && loadingId.current === customCaption}
errored={error && loadingId.current === customCaption}
onClick={() => customCaptionUploadElement.current?.click()}
>
{currentCaption === customCaption
? t("videoPlayer.popouts.customCaption")
: t("videoPlayer.popouts.uploadCustomCaption")}
<input
className="hidden"
ref={customCaptionUploadElement}
accept={subtitleTypeList.join(",")}
type="file"
onChange={(e) => {
if (!e.target.files) return;
const customSubtitle = {
langIso: "custom",
url: URL.createObjectURL(e.target.files[0]),
type: MWCaptionType.UNKNOWN,
};
setCaption(customSubtitle, false);
}}
/>
</PopoutListEntry>
</PopoutSection>
<p className="sticky top-0 z-10 flex items-center space-x-1 bg-ash-300 px-5 py-3 text-xs font-bold uppercase">
<Icon className="text-base" icon={Icons.LINK} />
<span>{t("videoPlayer.popouts.linkedCaptions")}</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>
</FloatingCardView.Content>
</FloatingView>
);
}