mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-21 14:47:41 +01:00
add caption settings popout
This commit is contained in:
parent
875be16c4c
commit
ef4cb064e7
3 changed files with 341 additions and 58 deletions
|
@ -4,14 +4,16 @@ import {
|
||||||
CUSTOM_CAPTION_ID,
|
CUSTOM_CAPTION_ID,
|
||||||
} from "@/backend/helpers/captions";
|
} from "@/backend/helpers/captions";
|
||||||
import { MWCaption } from "@/backend/helpers/streams";
|
import { MWCaption } from "@/backend/helpers/streams";
|
||||||
|
import { IconButton } from "@/components/buttons/IconButton";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { useLoading } from "@/hooks/useLoading";
|
import { useLoading } from "@/hooks/useLoading";
|
||||||
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
import { useVideoPlayerDescriptor } from "@/video/state/hooks";
|
||||||
import { useControls } from "@/video/state/logic/controls";
|
import { useControls } from "@/video/state/logic/controls";
|
||||||
import { useMeta } from "@/video/state/logic/meta";
|
import { useMeta } from "@/video/state/logic/meta";
|
||||||
import { useSource } from "@/video/state/logic/source";
|
import { useSource } from "@/video/state/logic/source";
|
||||||
import { ChangeEvent, useMemo, useRef } from "react";
|
import { ChangeEvent, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
|
||||||
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
|
||||||
|
@ -64,69 +66,80 @@ export function CaptionSelectionPopout() {
|
||||||
const captionFile = e.target.files[0];
|
const captionFile = e.target.files[0];
|
||||||
setCustomCaption(captionFile);
|
setCustomCaption(captionFile);
|
||||||
}
|
}
|
||||||
|
const [showCaptionSettings, setShowCaptionSettings] =
|
||||||
|
useState<boolean>(false);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PopoutSection className="bg-ash-100 font-bold text-white">
|
<PopoutSection className="flex flex-row justify-between bg-ash-100 font-bold text-white">
|
||||||
<div>{t("videoPlayer.popouts.captions")}</div>
|
<div>{t("videoPlayer.popouts.captions")}</div>
|
||||||
|
<IconButton
|
||||||
|
icon={Icons.SETTINGS}
|
||||||
|
onClick={() => {
|
||||||
|
setShowCaptionSettings((old) => !old);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
<div className="relative overflow-y-auto">
|
{showCaptionSettings ? (
|
||||||
<PopoutSection>
|
<CaptionSettingsPopout />
|
||||||
<PopoutListEntry
|
) : (
|
||||||
active={!currentCaption}
|
<div className="relative overflow-y-auto">
|
||||||
onClick={() => {
|
<PopoutSection>
|
||||||
controls.clearCaption();
|
<PopoutListEntry
|
||||||
controls.closePopout();
|
active={!currentCaption}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
controls.clearCaption();
|
||||||
{t("videoPlayer.popouts.noCaptions")}
|
controls.closePopout();
|
||||||
</PopoutListEntry>
|
}}
|
||||||
<PopoutListEntry
|
>
|
||||||
key={CUSTOM_CAPTION_ID}
|
{t("videoPlayer.popouts.noCaptions")}
|
||||||
active={currentCaption === CUSTOM_CAPTION_ID}
|
</PopoutListEntry>
|
||||||
loading={loadingCustomCaption}
|
<PopoutListEntry
|
||||||
errored={!!errorCustomCaption}
|
key={CUSTOM_CAPTION_ID}
|
||||||
onClick={() => {
|
active={currentCaption === CUSTOM_CAPTION_ID}
|
||||||
customCaptionUploadElement.current?.click();
|
loading={loadingCustomCaption}
|
||||||
}}
|
errored={!!errorCustomCaption}
|
||||||
>
|
onClick={() => {
|
||||||
{currentCaption === CUSTOM_CAPTION_ID
|
customCaptionUploadElement.current?.click();
|
||||||
? t("videoPlayer.popouts.customCaption")
|
}}
|
||||||
: t("videoPlayer.popouts.uploadCustomCaption")}
|
>
|
||||||
<input
|
{currentCaption === CUSTOM_CAPTION_ID
|
||||||
ref={customCaptionUploadElement}
|
? t("videoPlayer.popouts.customCaption")
|
||||||
type="file"
|
: t("videoPlayer.popouts.uploadCustomCaption")}
|
||||||
onChange={handleUploadCaption}
|
<input
|
||||||
className="hidden"
|
ref={customCaptionUploadElement}
|
||||||
accept=".vtt, .srt"
|
type="file"
|
||||||
/>
|
onChange={handleUploadCaption}
|
||||||
</PopoutListEntry>
|
className="hidden"
|
||||||
</PopoutSection>
|
accept=".vtt, .srt"
|
||||||
|
/>
|
||||||
|
</PopoutListEntry>
|
||||||
|
</PopoutSection>
|
||||||
|
|
||||||
<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">
|
<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">
|
||||||
<Icon className="text-base" icon={Icons.LINK} />
|
<Icon className="text-base" icon={Icons.LINK} />
|
||||||
<span>{t("videoPlayer.popouts.linkedCaptions")}</span>
|
<span>{t("videoPlayer.popouts.linkedCaptions")}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<PopoutSection className="pt-0">
|
<PopoutSection className="pt-0">
|
||||||
<div>
|
<div>
|
||||||
{linkedCaptions.map((link) => (
|
{linkedCaptions.map((link) => (
|
||||||
<PopoutListEntry
|
<PopoutListEntry
|
||||||
key={link.langIso}
|
key={link.langIso}
|
||||||
active={link.id === currentCaption}
|
active={link.id === currentCaption}
|
||||||
loading={loading && link.id === loadingId.current}
|
loading={loading && link.id === loadingId.current}
|
||||||
errored={error && link.id === loadingId.current}
|
errored={error && link.id === loadingId.current}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
loadingId.current = link.id;
|
loadingId.current = link.id;
|
||||||
setCaption(link, true);
|
setCaption(link, true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{link.langIso}
|
{link.langIso}
|
||||||
</PopoutListEntry>
|
</PopoutListEntry>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</PopoutSection>
|
</PopoutSection>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
189
src/video/components/popouts/CaptionSettingsPopout.tsx
Normal file
189
src/video/components/popouts/CaptionSettingsPopout.tsx
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
import { Dropdown, OptionItem } from "@/components/Dropdown";
|
||||||
|
import { useSettings } from "@/state/settings";
|
||||||
|
// import { useTranslation } from "react-i18next";
|
||||||
|
import { PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
|
export function CaptionSettingsPopout() {
|
||||||
|
// For now, won't add label texts to language files since options are prone to change
|
||||||
|
// const { t } = useTranslation();
|
||||||
|
const {
|
||||||
|
captionSettings,
|
||||||
|
setCaptionBackgroundColor,
|
||||||
|
setCaptionColor,
|
||||||
|
setCaptionDelay,
|
||||||
|
setCaptionFontSize,
|
||||||
|
setCaptionFontFamily,
|
||||||
|
setCaptionTextShadow,
|
||||||
|
} = useSettings();
|
||||||
|
// TODO: move it to context and specify which fonts to use
|
||||||
|
const fontFamilies: OptionItem[] = [
|
||||||
|
{ id: "Times New Roman", name: "Times New Roman" },
|
||||||
|
{ id: "monospace", name: "Monospace" },
|
||||||
|
{ id: "sans-serif", name: "Sans Serif" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const selectedFont = fontFamilies.find(
|
||||||
|
(f) => f.id === captionSettings.style.fontFamily
|
||||||
|
) ?? { id: "monospace", name: "Monospace" };
|
||||||
|
|
||||||
|
// TODO: Slider and color picker styling or complete re-write
|
||||||
|
return (
|
||||||
|
<PopoutSection className="overflow-auto">
|
||||||
|
<Dropdown
|
||||||
|
setSelectedItem={(e) => {
|
||||||
|
setCaptionFontFamily(e.id);
|
||||||
|
}}
|
||||||
|
selectedItem={selectedFont}
|
||||||
|
options={fontFamilies}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white" htmlFor="fontSize">
|
||||||
|
Font Size ({captionSettings.style.fontSize})
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||||
|
type="range"
|
||||||
|
name="fontSize"
|
||||||
|
id="fontSize"
|
||||||
|
max={30}
|
||||||
|
min={10}
|
||||||
|
step={1}
|
||||||
|
value={captionSettings.style.fontSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white">
|
||||||
|
Delay ({captionSettings.delay}s)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
||||||
|
type="range"
|
||||||
|
max={10 * 1000}
|
||||||
|
min={-10 * 1000}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white" htmlFor="captionColor">
|
||||||
|
Color
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => setCaptionColor(e.target.value)}
|
||||||
|
type="color"
|
||||||
|
name="captionColor"
|
||||||
|
id="captionColor"
|
||||||
|
value={captionSettings.style.color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white" htmlFor="backgroundColor">
|
||||||
|
Background Color
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => setCaptionBackgroundColor(`${e.target.value}cc`)}
|
||||||
|
type="color"
|
||||||
|
name="backgroundColor"
|
||||||
|
id="backgroundColor"
|
||||||
|
value={captionSettings.style.backgroundColor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label
|
||||||
|
className="font-bold text-white"
|
||||||
|
htmlFor="backgroundColorOpacity"
|
||||||
|
>
|
||||||
|
Background Color Opacity
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) =>
|
||||||
|
setCaptionBackgroundColor(
|
||||||
|
`${captionSettings.style.backgroundColor.substring(
|
||||||
|
0,
|
||||||
|
7
|
||||||
|
)}${e.target.valueAsNumber.toString(16)}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
type="range"
|
||||||
|
min={0}
|
||||||
|
max={255}
|
||||||
|
name="backgroundColorOpacity"
|
||||||
|
id="backgroundColorOpacity"
|
||||||
|
value={Number.parseInt(
|
||||||
|
captionSettings.style.backgroundColor.substring(7, 9),
|
||||||
|
16
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white" htmlFor="textShadowColor">
|
||||||
|
Text Shadow Color
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => {
|
||||||
|
const [offsetX, offsetY, blurRadius, color] =
|
||||||
|
captionSettings.style.textShadow.split(" ");
|
||||||
|
return setCaptionTextShadow(
|
||||||
|
`${offsetX} ${offsetY} ${blurRadius} ${e.target.value}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
type="color"
|
||||||
|
name="textShadowColor"
|
||||||
|
id="textShadowColor"
|
||||||
|
value={captionSettings.style.textShadow.split(" ")[3]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white">Text Shadow (Offset X)</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => {
|
||||||
|
const [offsetX, offsetY, blurRadius, color] =
|
||||||
|
captionSettings.style.textShadow.split(" ");
|
||||||
|
return setCaptionTextShadow(
|
||||||
|
`${e.target.valueAsNumber}px ${offsetY} ${blurRadius} ${color}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
type="range"
|
||||||
|
min={-10}
|
||||||
|
max={10}
|
||||||
|
value={parseFloat(captionSettings.style.textShadow.split("px")[0])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white">Text Shadow (Offset Y)</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => {
|
||||||
|
const [offsetX, offsetY, blurRadius, color] =
|
||||||
|
captionSettings.style.textShadow.split(" ");
|
||||||
|
return setCaptionTextShadow(
|
||||||
|
`${offsetX} ${e.target.value}px ${blurRadius} ${color}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
type="range"
|
||||||
|
min={-10}
|
||||||
|
max={10}
|
||||||
|
value={parseFloat(captionSettings.style.textShadow.split("px")[1])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between py-2">
|
||||||
|
<label className="font-bold text-white">Text Shadow Blur</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => {
|
||||||
|
const [offsetX, offsetY, blurRadius, color] =
|
||||||
|
captionSettings.style.textShadow.split(" ");
|
||||||
|
|
||||||
|
return setCaptionTextShadow(
|
||||||
|
`${offsetX} ${offsetY} ${e.target.valueAsNumber}px ${color}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
type="range"
|
||||||
|
value={parseFloat(captionSettings.style.textShadow.split("px")[2])}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</PopoutSection>
|
||||||
|
);
|
||||||
|
}
|
81
src/views/settings/SettingsView.tsx
Normal file
81
src/views/settings/SettingsView.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
import { Dropdown, OptionItem } from "@/components/Dropdown";
|
||||||
|
import { useSettings } from "@/state/settings";
|
||||||
|
|
||||||
|
export function SettingsView() {
|
||||||
|
const languages: OptionItem[] = [
|
||||||
|
{ id: "en", name: "English" },
|
||||||
|
{ id: "tr", name: "Turkish" },
|
||||||
|
];
|
||||||
|
const {
|
||||||
|
language,
|
||||||
|
captionSettings,
|
||||||
|
setLanguage,
|
||||||
|
setCaptionBackgroundColor,
|
||||||
|
setCaptionColor,
|
||||||
|
setCaptionFontSize,
|
||||||
|
} = useSettings();
|
||||||
|
const selectedLanguage = languages.find((lang) => lang.id === language) || {
|
||||||
|
id: "en",
|
||||||
|
name: "English",
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex aspect-square flex-row pl-28">
|
||||||
|
<div className="flex flex-col p-10">
|
||||||
|
<label className="font-bold text-white">Language</label>
|
||||||
|
<Dropdown
|
||||||
|
setSelectedItem={(item) => setLanguage(item.id)}
|
||||||
|
selectedItem={selectedLanguage}
|
||||||
|
options={languages}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col p-10">
|
||||||
|
<div className="font-bold text-white">Caption Settings</div>
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<label className="font-bold text-white" htmlFor="fontSize">
|
||||||
|
Font Size
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||||
|
type="range"
|
||||||
|
name="fontSize"
|
||||||
|
id="fontSize"
|
||||||
|
max={40}
|
||||||
|
min={10}
|
||||||
|
value={captionSettings.style.fontSize}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<label className="font-bold text-white" htmlFor="color">
|
||||||
|
Color
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="ml-10"
|
||||||
|
onChange={(e) => setCaptionColor(e.target.value)}
|
||||||
|
type="color"
|
||||||
|
name="color"
|
||||||
|
id="color"
|
||||||
|
value={captionSettings.style.color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-between">
|
||||||
|
<label className="font-bold text-white" htmlFor="bgColor">
|
||||||
|
Background Color
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className="ml-10"
|
||||||
|
onChange={(e) => setCaptionBackgroundColor(e.target.value)}
|
||||||
|
type="color"
|
||||||
|
name="bgColor"
|
||||||
|
id="bgColor"
|
||||||
|
value={captionSettings.style.backgroundColor}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-white">
|
||||||
|
{JSON.stringify(captionSettings, null, "\t\t")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue