add caption settings popout

This commit is contained in:
frost768 2023-03-15 17:48:50 +03:00
parent 06d043d482
commit 464b78d914
8 changed files with 191 additions and 182 deletions

View File

@ -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,22 +65,28 @@ 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
); );
} }
const serie = v as FLIXTVSerie; if (media.meta.type === MWMediaType.SERIES) {
if (serie.seasons && media.meta.seasons) { if (v.type !== "TV Series") return false;
return ( const serie = v as FLIXTVSerie;
compareTitle(serie.title, media.meta.title) && if (serie.seasons && media.meta.seasons) {
serie.seasons === media.meta.seasons.length return (
); compareTitle(serie.title, media.meta.title) &&
serie.seasons === media.meta.seasons.length
);
}
return false;
} }
return compareTitle(serie.title, media.meta.title); 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: {

View File

@ -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;
}

View File

@ -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": {

View File

@ -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;
}); });

View File

@ -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;

View File

@ -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>

View File

@ -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">
<input {colors.map((color) => (
onChange={(e) => { <div
const [offsetX, offsetY, blurRadius, color] = className={`flex h-8 w-8 items-center justify-center rounded ${
captionSettings.style.textShadow.split(" "); color === captionSettings.style.color ? "bg-[#1C161B]" : ""
return setCaptionTextShadow( }`}
`${offsetX} ${offsetY} ${blurRadius} ${e.target.value}` >
); <input
}} className="h-4 w-4 cursor-pointer appearance-none rounded-full"
type="color" type="radio"
name="textShadowColor" name="color"
id="textShadowColor" key={color}
value={captionSettings.style.textShadow.split(" ")[3]} value={color}
/> style={{
</div> backgroundColor: color,
<div className="flex flex-row justify-between py-2"> }}
<label className="font-bold text-white">Text Shadow (Offset X)</label> onChange={(e) => setCaptionColor(e.target.value)}
<input />
onChange={(e) => { </div>
const [offsetX, offsetY, blurRadius, color] = ))}
captionSettings.style.textShadow.split(" "); </div>
return setCaptionTextShadow( </div>
`${e.target.valueAsNumber}px ${offsetY} ${blurRadius} ${color}` </FloatingCardView.Content>
); </FloatingView>
}}
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>
); );
} }

View File

@ -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"
/>
</> </>
); );
} }