mirror of
https://github.com/movie-web/movie-web.git
synced 2024-12-28 19:01:53 +01:00
add caption settings popout
This commit is contained in:
parent
06d043d482
commit
464b78d914
@ -18,15 +18,14 @@ interface FLIXMediaBase {
|
|||||||
title: string;
|
title: string;
|
||||||
url: string;
|
url: string;
|
||||||
image: string;
|
image: string;
|
||||||
|
type: "Movie" | "TV Series";
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FLIXTVSerie extends FLIXMediaBase {
|
interface FLIXTVSerie extends FLIXMediaBase {
|
||||||
type: "TV Series";
|
|
||||||
seasons: number | null;
|
seasons: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FLIXMovie extends FLIXMediaBase {
|
interface FLIXMovie extends FLIXMediaBase {
|
||||||
type: "Movie";
|
|
||||||
releaseDate: string;
|
releaseDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,14 +65,18 @@ registerProvider({
|
|||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
|
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
|
||||||
if (media.meta.type === MWMediaType.MOVIE) {
|
if (media.meta.type === MWMediaType.MOVIE) {
|
||||||
|
if (v.type !== "Movie") return false;
|
||||||
const movie = v as FLIXMovie;
|
const movie = v as FLIXMovie;
|
||||||
return (
|
return (
|
||||||
compareTitle(movie.title, media.meta.title) &&
|
compareTitle(movie.title, media.meta.title) &&
|
||||||
movie.releaseDate === media.meta.year
|
movie.releaseDate === media.meta.year
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (media.meta.type === MWMediaType.SERIES) {
|
||||||
|
if (v.type !== "TV Series") return false;
|
||||||
const serie = v as FLIXTVSerie;
|
const serie = v as FLIXTVSerie;
|
||||||
if (serie.seasons && media.meta.seasons) {
|
if (serie.seasons && media.meta.seasons) {
|
||||||
return (
|
return (
|
||||||
@ -81,7 +84,9 @@ registerProvider({
|
|||||||
serie.seasons === media.meta.seasons.length
|
serie.seasons === media.meta.seasons.length
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return compareTitle(serie.title, media.meta.title);
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
if (!foundItem) throw new Error("No watchable item found");
|
if (!foundItem) throw new Error("No watchable item found");
|
||||||
const flixId = foundItem.id;
|
const flixId = foundItem.id;
|
||||||
@ -110,6 +115,7 @@ registerProvider({
|
|||||||
)?.id;
|
)?.id;
|
||||||
}
|
}
|
||||||
if (!episodeId) throw new Error("No watchable item found");
|
if (!episodeId) throw new Error("No watchable item found");
|
||||||
|
|
||||||
const watchInfo = await proxiedFetch<any>("/watch", {
|
const watchInfo = await proxiedFetch<any>("/watch", {
|
||||||
baseURL: flixHqBase,
|
baseURL: flixHqBase,
|
||||||
params: {
|
params: {
|
||||||
|
@ -54,3 +54,35 @@ body[data-no-select] {
|
|||||||
.google-cast-button:not(.casting) google-cast-launcher {
|
.google-cast-button:not(.casting) google-cast-launcher {
|
||||||
@apply brightness-[500];
|
@apply brightness-[500];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type=range] {
|
||||||
|
@apply bg-[#1C161B];
|
||||||
|
height: 0.25rem;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
outline: none;
|
||||||
|
line-height: normal;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 1rem;
|
||||||
|
background: white;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
height: 1rem;
|
||||||
|
background: white;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
@ -69,6 +69,13 @@
|
|||||||
"sources": "Sources",
|
"sources": "Sources",
|
||||||
"seasons": "Seasons",
|
"seasons": "Seasons",
|
||||||
"captions": "Captions",
|
"captions": "Captions",
|
||||||
|
"captionPreferences": {
|
||||||
|
"title": "Caption Preferences",
|
||||||
|
"delay": "Delay",
|
||||||
|
"fontSize": "Size",
|
||||||
|
"opacity": "Opacity",
|
||||||
|
"color": "Color"
|
||||||
|
},
|
||||||
"episode": "E{{index}} - {{title}}",
|
"episode": "E{{index}} - {{title}}",
|
||||||
"noCaptions": "No captions",
|
"noCaptions": "No captions",
|
||||||
"linkedCaptions": "Linked captions",
|
"linkedCaptions": "Linked captions",
|
||||||
@ -84,7 +91,8 @@
|
|||||||
"embeds": "Choose which video to view",
|
"embeds": "Choose which video to view",
|
||||||
"seasons": "Choose which season you want to watch",
|
"seasons": "Choose which season you want to watch",
|
||||||
"episode": "Pick an episode",
|
"episode": "Pick an episode",
|
||||||
"captions": "Choose a subtitle language"
|
"captions": "Choose a subtitle language",
|
||||||
|
"captionPreferences": "Make subtitles look how you want it"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"errors": {
|
"errors": {
|
||||||
|
@ -34,11 +34,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
|
|||||||
setCaptionDelay(delay: number) {
|
setCaptionDelay(delay: number) {
|
||||||
setSettings((oldSettings) => {
|
setSettings((oldSettings) => {
|
||||||
const captionSettings = oldSettings.captionSettings;
|
const captionSettings = oldSettings.captionSettings;
|
||||||
captionSettings.delay = enforceRange(
|
captionSettings.delay = enforceRange(-10, delay, 10);
|
||||||
-10 * 1000,
|
|
||||||
delay / 1000,
|
|
||||||
10 * 1000
|
|
||||||
);
|
|
||||||
const newSettings = oldSettings;
|
const newSettings = oldSettings;
|
||||||
return newSettings;
|
return newSettings;
|
||||||
});
|
});
|
||||||
|
@ -11,11 +11,11 @@ export const SettingsStore = createVersionedStore<MWSettingsData>()
|
|||||||
captionSettings: {
|
captionSettings: {
|
||||||
delay: 0,
|
delay: 0,
|
||||||
style: {
|
style: {
|
||||||
color: "white",
|
color: "#ffffff",
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontFamily: "inherit",
|
fontFamily: "inherit",
|
||||||
textShadow: "2px 2px 2px black",
|
textShadow: "2px 2px 2px black",
|
||||||
backgroundColor: "black",
|
backgroundColor: "#000000ff",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as MWSettingsData;
|
} as MWSettingsData;
|
||||||
|
@ -4,7 +4,6 @@ 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 { FloatingCardView } from "@/components/popout/FloatingCard";
|
import { FloatingCardView } from "@/components/popout/FloatingCard";
|
||||||
import { FloatingView } from "@/components/popout/FloatingView";
|
import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
@ -14,9 +13,8 @@ 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, useState } from "react";
|
import { ChangeEvent, useMemo, useRef } 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 {
|
||||||
@ -72,8 +70,6 @@ export function CaptionSelectionPopout(props: {
|
|||||||
const captionFile = e.target.files[0];
|
const captionFile = e.target.files[0];
|
||||||
setCustomCaption(captionFile);
|
setCustomCaption(captionFile);
|
||||||
}
|
}
|
||||||
const [showCaptionSettings, setShowCaptionSettings] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
return (
|
return (
|
||||||
<FloatingView
|
<FloatingView
|
||||||
{...props.router.pageProps(props.prefix)}
|
{...props.router.pageProps(props.prefix)}
|
||||||
@ -84,6 +80,16 @@ export function CaptionSelectionPopout(props: {
|
|||||||
title={t("videoPlayer.popouts.captions")}
|
title={t("videoPlayer.popouts.captions")}
|
||||||
description={t("videoPlayer.popouts.descriptions.captions")}
|
description={t("videoPlayer.popouts.descriptions.captions")}
|
||||||
goBack={() => props.router.navigate("/")}
|
goBack={() => props.router.navigate("/")}
|
||||||
|
action={
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => props.router.navigate("/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>
|
<FloatingCardView.Content noSection>
|
||||||
<PopoutSection>
|
<PopoutSection>
|
||||||
|
@ -1,103 +1,109 @@
|
|||||||
import { Dropdown, OptionItem } from "@/components/Dropdown";
|
import { FloatingCardView } from "@/components/popout/FloatingCard";
|
||||||
|
import { FloatingView } from "@/components/popout/FloatingView";
|
||||||
|
import { useFloatingRouter } from "@/hooks/useFloatingRouter";
|
||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
// import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { ChangeEventHandler } from "react";
|
||||||
import { PopoutSection } from "./PopoutUtils";
|
import { PopoutSection } from "./PopoutUtils";
|
||||||
|
|
||||||
export function CaptionSettingsPopout() {
|
type SliderProps = {
|
||||||
|
label: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step: number;
|
||||||
|
value: number;
|
||||||
|
valueDisplay?: string;
|
||||||
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
|
stops?: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function Slider(params: SliderProps) {
|
||||||
|
const stops = params.stops ?? [Math.floor((params.max + params.min) / 2)];
|
||||||
|
return (
|
||||||
|
<div className="mb-6 flex flex-row gap-4">
|
||||||
|
<div className="flex w-full flex-col gap-2">
|
||||||
|
<label className="font-bold">{params.label}</label>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
onChange={params.onChange}
|
||||||
|
value={params.value}
|
||||||
|
max={params.max}
|
||||||
|
min={params.min}
|
||||||
|
step={params.step}
|
||||||
|
list="stops"
|
||||||
|
/>
|
||||||
|
<datalist id="stops">
|
||||||
|
{stops.map((s) => (
|
||||||
|
<option value={s} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
|
</div>
|
||||||
|
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
|
||||||
|
<div className="text-center font-bold text-white">
|
||||||
|
{params.valueDisplay ?? params.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CaptionSettingsPopout(props: {
|
||||||
|
router: ReturnType<typeof useFloatingRouter>;
|
||||||
|
prefix: string;
|
||||||
|
}) {
|
||||||
// For now, won't add label texts to language files since options are prone to change
|
// For now, won't add label texts to language files since options are prone to change
|
||||||
// const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const {
|
const {
|
||||||
captionSettings,
|
captionSettings,
|
||||||
setCaptionBackgroundColor,
|
setCaptionBackgroundColor,
|
||||||
setCaptionColor,
|
setCaptionColor,
|
||||||
setCaptionDelay,
|
setCaptionDelay,
|
||||||
setCaptionFontSize,
|
setCaptionFontSize,
|
||||||
setCaptionFontFamily,
|
|
||||||
setCaptionTextShadow,
|
|
||||||
} = useSettings();
|
} = useSettings();
|
||||||
// TODO: move it to context and specify which fonts to use
|
const colors = ["#ffffff", "#00ffff", "#ffff00"];
|
||||||
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 (
|
return (
|
||||||
<PopoutSection className="overflow-auto">
|
<FloatingView {...props.router.pageProps(props.prefix)} width={375}>
|
||||||
<Dropdown
|
<FloatingCardView.Header
|
||||||
setSelectedItem={(e) => {
|
title={t("videoPlayer.popouts.captionPreferences.title")}
|
||||||
setCaptionFontFamily(e.id);
|
description={t("videoPlayer.popouts.descriptions.captionPreferences")}
|
||||||
}}
|
goBack={() => props.router.navigate("/captions")}
|
||||||
selectedItem={selectedFont}
|
|
||||||
options={fontFamilies}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-row justify-between py-2">
|
<FloatingCardView.Content>
|
||||||
<label className="font-bold text-white" htmlFor="fontSize">
|
<Slider
|
||||||
Font Size ({captionSettings.style.fontSize})
|
label={t("videoPlayer.popouts.captionPreferences.delay")}
|
||||||
</label>
|
max={10}
|
||||||
<input
|
min={-10}
|
||||||
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
step={0.1}
|
||||||
type="range"
|
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
|
||||||
name="fontSize"
|
value={captionSettings.delay}
|
||||||
id="fontSize"
|
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
||||||
max={30}
|
stops={[-5, 0, 5]}
|
||||||
|
/>
|
||||||
|
<Slider
|
||||||
|
label="Size"
|
||||||
min={10}
|
min={10}
|
||||||
step={1}
|
step={1}
|
||||||
|
max={30}
|
||||||
value={captionSettings.style.fontSize}
|
value={captionSettings.style.fontSize}
|
||||||
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||||
/>
|
/>
|
||||||
</div>
|
<Slider
|
||||||
|
label={t("videoPlayer.popouts.captionPreferences.opacity")}
|
||||||
<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}
|
step={1}
|
||||||
/>
|
min={0}
|
||||||
</div>
|
max={255}
|
||||||
|
valueDisplay={`${(
|
||||||
<div className="flex flex-row justify-between py-2">
|
(parseInt(
|
||||||
<label className="font-bold text-white" htmlFor="captionColor">
|
captionSettings.style.backgroundColor.substring(7, 9),
|
||||||
Color
|
16
|
||||||
</label>
|
) /
|
||||||
<input
|
255) *
|
||||||
onChange={(e) => setCaptionColor(e.target.value)}
|
100
|
||||||
type="color"
|
).toFixed(0)}%`}
|
||||||
name="captionColor"
|
value={parseInt(
|
||||||
id="captionColor"
|
captionSettings.style.backgroundColor.substring(7, 9),
|
||||||
value={captionSettings.style.color}
|
16
|
||||||
/>
|
)}
|
||||||
</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}`)}
|
|
||||||
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) =>
|
onChange={(e) =>
|
||||||
setCaptionBackgroundColor(
|
setCaptionBackgroundColor(
|
||||||
`${captionSettings.style.backgroundColor.substring(
|
`${captionSettings.style.backgroundColor.substring(
|
||||||
@ -106,84 +112,34 @@ export function CaptionSettingsPopout() {
|
|||||||
)}${e.target.valueAsNumber.toString(16)}`
|
)}${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">
|
||||||
<div className="flex flex-row justify-between py-2">
|
<label className="font-bold" htmlFor="color">
|
||||||
<label className="font-bold text-white" htmlFor="textShadowColor">
|
{t("videoPlayer.popouts.captionPreferences.color")}
|
||||||
Text Shadow Color
|
|
||||||
</label>
|
</label>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
{colors.map((color) => (
|
||||||
|
<div
|
||||||
|
className={`flex h-8 w-8 items-center justify-center rounded ${
|
||||||
|
color === captionSettings.style.color ? "bg-[#1C161B]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
onChange={(e) => {
|
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
|
||||||
const [offsetX, offsetY, blurRadius, color] =
|
type="radio"
|
||||||
captionSettings.style.textShadow.split(" ");
|
name="color"
|
||||||
return setCaptionTextShadow(
|
key={color}
|
||||||
`${offsetX} ${offsetY} ${blurRadius} ${e.target.value}`
|
value={color}
|
||||||
);
|
style={{
|
||||||
|
backgroundColor: color,
|
||||||
}}
|
}}
|
||||||
type="color"
|
onChange={(e) => setCaptionColor(e.target.value)}
|
||||||
name="textShadowColor"
|
|
||||||
id="textShadowColor"
|
|
||||||
value={captionSettings.style.textShadow.split(" ")[3]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<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>
|
||||||
|
</FloatingCardView.Content>
|
||||||
<div className="flex flex-row justify-between py-2">
|
</FloatingView>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import { CaptionsSelectionAction } from "@/video/components/actions/list-entries
|
|||||||
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
|
||||||
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
|
||||||
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
import { SourceSelectionPopout } from "./SourceSelectionPopout";
|
||||||
|
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
|
||||||
|
|
||||||
export function SettingsPopout() {
|
export function SettingsPopout() {
|
||||||
const floatingRouter = useFloatingRouter();
|
const floatingRouter = useFloatingRouter();
|
||||||
@ -24,6 +25,10 @@ export function SettingsPopout() {
|
|||||||
</FloatingView>
|
</FloatingView>
|
||||||
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
<SourceSelectionPopout router={floatingRouter} prefix="source" />
|
||||||
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
|
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
|
||||||
|
<CaptionSettingsPopout
|
||||||
|
router={floatingRouter}
|
||||||
|
prefix="caption-settings"
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user