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;
url: string;
image: string;
type: "Movie" | "TV Series";
}
interface FLIXTVSerie extends FLIXMediaBase {
type: "TV Series";
seasons: number | null;
}
interface FLIXMovie extends FLIXMediaBase {
type: "Movie";
releaseDate: string;
}
@ -66,22 +65,28 @@ registerProvider({
baseURL: flixHqBase,
}
);
const foundItem = searchResults.results.find((v: FLIXMediaBase) => {
if (media.meta.type === MWMediaType.MOVIE) {
if (v.type !== "Movie") return false;
const movie = v as FLIXMovie;
return (
compareTitle(movie.title, media.meta.title) &&
movie.releaseDate === media.meta.year
);
}
const serie = v as FLIXTVSerie;
if (serie.seasons && media.meta.seasons) {
return (
compareTitle(serie.title, media.meta.title) &&
serie.seasons === media.meta.seasons.length
);
if (media.meta.type === MWMediaType.SERIES) {
if (v.type !== "TV Series") return false;
const serie = v as FLIXTVSerie;
if (serie.seasons && media.meta.seasons) {
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");
const flixId = foundItem.id;
@ -110,6 +115,7 @@ registerProvider({
)?.id;
}
if (!episodeId) throw new Error("No watchable item found");
const watchInfo = await proxiedFetch<any>("/watch", {
baseURL: flixHqBase,
params: {

View File

@ -54,3 +54,35 @@ body[data-no-select] {
.google-cast-button:not(.casting) google-cast-launcher {
@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",
"seasons": "Seasons",
"captions": "Captions",
"captionPreferences": {
"title": "Caption Preferences",
"delay": "Delay",
"fontSize": "Size",
"opacity": "Opacity",
"color": "Color"
},
"episode": "E{{index}} - {{title}}",
"noCaptions": "No captions",
"linkedCaptions": "Linked captions",
@ -84,7 +91,8 @@
"embeds": "Choose which video to view",
"seasons": "Choose which season you want to watch",
"episode": "Pick an episode",
"captions": "Choose a subtitle language"
"captions": "Choose a subtitle language",
"captionPreferences": "Make subtitles look how you want it"
}
},
"errors": {

View File

@ -34,11 +34,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
setCaptionDelay(delay: number) {
setSettings((oldSettings) => {
const captionSettings = oldSettings.captionSettings;
captionSettings.delay = enforceRange(
-10 * 1000,
delay / 1000,
10 * 1000
);
captionSettings.delay = enforceRange(-10, delay, 10);
const newSettings = oldSettings;
return newSettings;
});

View File

@ -11,11 +11,11 @@ export const SettingsStore = createVersionedStore<MWSettingsData>()
captionSettings: {
delay: 0,
style: {
color: "white",
color: "#ffffff",
fontSize: 20,
fontFamily: "inherit",
textShadow: "2px 2px 2px black",
backgroundColor: "black",
backgroundColor: "#000000ff",
},
},
} as MWSettingsData;

View File

@ -4,7 +4,6 @@ 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 { FloatingCardView } from "@/components/popout/FloatingCard";
import { FloatingView } from "@/components/popout/FloatingView";
@ -14,9 +13,8 @@ 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, useState } from "react";
import { ChangeEvent, useMemo, useRef } from "react";
import { useTranslation } from "react-i18next";
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
import { PopoutListEntry, PopoutSection } from "./PopoutUtils";
function makeCaptionId(caption: MWCaption, isLinked: boolean): string {
@ -72,8 +70,6 @@ export function CaptionSelectionPopout(props: {
const captionFile = e.target.files[0];
setCustomCaption(captionFile);
}
const [showCaptionSettings, setShowCaptionSettings] =
useState<boolean>(false);
return (
<FloatingView
{...props.router.pageProps(props.prefix)}
@ -84,6 +80,16 @@ export function CaptionSelectionPopout(props: {
title={t("videoPlayer.popouts.captions")}
description={t("videoPlayer.popouts.descriptions.captions")}
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>
<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 { useTranslation } from "react-i18next";
import { useTranslation } from "react-i18next";
import { ChangeEventHandler } from "react";
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
// const { t } = useTranslation();
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
const colors = ["#ffffff", "#00ffff", "#ffff00"];
return (
<PopoutSection className="overflow-auto">
<Dropdown
setSelectedItem={(e) => {
setCaptionFontFamily(e.id);
}}
selectedItem={selectedFont}
options={fontFamilies}
<FloatingView {...props.router.pageProps(props.prefix)} width={375}>
<FloatingCardView.Header
title={t("videoPlayer.popouts.captionPreferences.title")}
description={t("videoPlayer.popouts.descriptions.captionPreferences")}
goBack={() => props.router.navigate("/captions")}
/>
<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}
<FloatingCardView.Content>
<Slider
label={t("videoPlayer.popouts.captionPreferences.delay")}
max={10}
min={-10}
step={0.1}
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
value={captionSettings.delay}
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
stops={[-5, 0, 5]}
/>
<Slider
label="Size"
min={10}
step={1}
max={30}
value={captionSettings.style.fontSize}
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
/>
</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}
<Slider
label={t("videoPlayer.popouts.captionPreferences.opacity")}
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}`)}
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
min={0}
max={255}
valueDisplay={`${(
(parseInt(
captionSettings.style.backgroundColor.substring(7, 9),
16
) /
255) *
100
).toFixed(0)}%`}
value={parseInt(
captionSettings.style.backgroundColor.substring(7, 9),
16
)}
onChange={(e) =>
setCaptionBackgroundColor(
`${captionSettings.style.backgroundColor.substring(
@ -106,84 +112,34 @@ export function CaptionSettingsPopout() {
)}${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>
<div className="flex flex-row justify-between">
<label className="font-bold" htmlFor="color">
{t("videoPlayer.popouts.captionPreferences.color")}
</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
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
type="radio"
name="color"
key={color}
value={color}
style={{
backgroundColor: color,
}}
onChange={(e) => setCaptionColor(e.target.value)}
/>
</div>
))}
</div>
</div>
</FloatingCardView.Content>
</FloatingView>
);
}

View File

@ -7,6 +7,7 @@ import { CaptionsSelectionAction } from "@/video/components/actions/list-entries
import { SourceSelectionAction } from "@/video/components/actions/list-entries/SourceSelectionAction";
import { CaptionSelectionPopout } from "./CaptionSelectionPopout";
import { SourceSelectionPopout } from "./SourceSelectionPopout";
import { CaptionSettingsPopout } from "./CaptionSettingsPopout";
export function SettingsPopout() {
const floatingRouter = useFloatingRouter();
@ -24,6 +25,10 @@ export function SettingsPopout() {
</FloatingView>
<SourceSelectionPopout router={floatingRouter} prefix="source" />
<CaptionSelectionPopout router={floatingRouter} prefix="captions" />
<CaptionSettingsPopout
router={floatingRouter}
prefix="caption-settings"
/>
</>
);
}