diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx index 2b4bf0aa..c66fe12a 100644 --- a/src/video/components/popouts/CaptionSelectionPopout.tsx +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -4,14 +4,16 @@ import { CUSTOM_CAPTION_ID, } from "@/backend/helpers/captions"; import { MWCaption } from "@/backend/helpers/streams"; +import { IconButton } from "@/components/buttons/IconButton"; 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 { ChangeEvent, useMemo, useRef } from "react"; +import { ChangeEvent, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { CaptionSettingsPopout } from "./CaptionSettingsPopout"; import { PopoutListEntry, PopoutSection } from "./PopoutUtils"; function makeCaptionId(caption: MWCaption, isLinked: boolean): string { @@ -64,69 +66,80 @@ export function CaptionSelectionPopout() { const captionFile = e.target.files[0]; setCustomCaption(captionFile); } - + const [showCaptionSettings, setShowCaptionSettings] = + useState(false); return ( <> - +
{t("videoPlayer.popouts.captions")}
+ { + setShowCaptionSettings((old) => !old); + }} + />
-
- - { - controls.clearCaption(); - controls.closePopout(); - }} - > - {t("videoPlayer.popouts.noCaptions")} - - { - customCaptionUploadElement.current?.click(); - }} - > - {currentCaption === CUSTOM_CAPTION_ID - ? t("videoPlayer.popouts.customCaption") - : t("videoPlayer.popouts.uploadCustomCaption")} - - - + {showCaptionSettings ? ( + + ) : ( +
+ + { + controls.clearCaption(); + controls.closePopout(); + }} + > + {t("videoPlayer.popouts.noCaptions")} + + { + customCaptionUploadElement.current?.click(); + }} + > + {currentCaption === CUSTOM_CAPTION_ID + ? t("videoPlayer.popouts.customCaption") + : t("videoPlayer.popouts.uploadCustomCaption")} + + + -

- - {t("videoPlayer.popouts.linkedCaptions")} -

+

+ + {t("videoPlayer.popouts.linkedCaptions")} +

- -
- {linkedCaptions.map((link) => ( - { - loadingId.current = link.id; - setCaption(link, true); - }} - > - {link.langIso} - - ))} -
-
-
+ +
+ {linkedCaptions.map((link) => ( + { + loadingId.current = link.id; + setCaption(link, true); + }} + > + {link.langIso} + + ))} +
+
+
+ )} ); } diff --git a/src/video/components/popouts/CaptionSettingsPopout.tsx b/src/video/components/popouts/CaptionSettingsPopout.tsx new file mode 100644 index 00000000..237a54c5 --- /dev/null +++ b/src/video/components/popouts/CaptionSettingsPopout.tsx @@ -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 ( + + { + setCaptionFontFamily(e.id); + }} + selectedItem={selectedFont} + options={fontFamilies} + /> +
+ + setCaptionFontSize(e.target.valueAsNumber)} + type="range" + name="fontSize" + id="fontSize" + max={30} + min={10} + step={1} + value={captionSettings.style.fontSize} + /> +
+ +
+ + setCaptionDelay(e.target.valueAsNumber)} + type="range" + max={10 * 1000} + min={-10 * 1000} + step={1} + /> +
+ +
+ + setCaptionColor(e.target.value)} + type="color" + name="captionColor" + id="captionColor" + value={captionSettings.style.color} + /> +
+ +
+ + setCaptionBackgroundColor(`${e.target.value}cc`)} + type="color" + name="backgroundColor" + id="backgroundColor" + value={captionSettings.style.backgroundColor} + /> +
+
+ + + 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 + )} + /> +
+
+ + { + 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]} + /> +
+
+ + { + 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])} + /> +
+ +
+ + { + 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])} + /> +
+ +
+ + { + 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])} + /> +
+
+ ); +} diff --git a/src/views/settings/SettingsView.tsx b/src/views/settings/SettingsView.tsx new file mode 100644 index 00000000..601e037a --- /dev/null +++ b/src/views/settings/SettingsView.tsx @@ -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 ( +
+
+ + setLanguage(item.id)} + selectedItem={selectedLanguage} + options={languages} + /> +
+
+
Caption Settings
+
+
+ + setCaptionFontSize(e.target.valueAsNumber)} + type="range" + name="fontSize" + id="fontSize" + max={40} + min={10} + value={captionSettings.style.fontSize} + /> +
+ + setCaptionColor(e.target.value)} + type="color" + name="color" + id="color" + value={captionSettings.style.color} + /> +
+
+ + setCaptionBackgroundColor(e.target.value)} + type="color" + name="bgColor" + id="bgColor" + value={captionSettings.style.backgroundColor} + /> +
+
+
+
+ {JSON.stringify(captionSettings, null, "\t\t")} +
+
+
+ ); +}