From 464b78d91429cbf04d8019d87eae998ea706d562 Mon Sep 17 00:00:00 2001 From: frost768 Date: Wed, 15 Mar 2023 17:48:50 +0300 Subject: [PATCH] add caption settings popout --- src/backend/providers/flixhq.ts | 24 +- src/setup/index.css | 32 ++ src/setup/locales/en/translation.json | 10 +- src/state/settings/context.tsx | 6 +- src/state/settings/store.ts | 4 +- .../popouts/CaptionSelectionPopout.tsx | 16 +- .../popouts/CaptionSettingsPopout.tsx | 276 ++++++++---------- .../components/popouts/SettingsPopout.tsx | 5 + 8 files changed, 191 insertions(+), 182 deletions(-) diff --git a/src/backend/providers/flixhq.ts b/src/backend/providers/flixhq.ts index 0de96b21..da2910c0 100644 --- a/src/backend/providers/flixhq.ts +++ b/src/backend/providers/flixhq.ts @@ -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("/watch", { baseURL: flixHqBase, params: { diff --git a/src/setup/index.css b/src/setup/index.css index ebb56bec..e70f4363 100644 --- a/src/setup/index.css +++ b/src/setup/index.css @@ -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; +} \ No newline at end of file diff --git a/src/setup/locales/en/translation.json b/src/setup/locales/en/translation.json index b4dd8ce5..c7af52ec 100644 --- a/src/setup/locales/en/translation.json +++ b/src/setup/locales/en/translation.json @@ -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": { diff --git a/src/state/settings/context.tsx b/src/state/settings/context.tsx index f0f19057..e7603a18 100644 --- a/src/state/settings/context.tsx +++ b/src/state/settings/context.tsx @@ -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; }); diff --git a/src/state/settings/store.ts b/src/state/settings/store.ts index e71647d5..6eabb777 100644 --- a/src/state/settings/store.ts +++ b/src/state/settings/store.ts @@ -11,11 +11,11 @@ export const SettingsStore = createVersionedStore() captionSettings: { delay: 0, style: { - color: "white", + color: "#ffffff", fontSize: 20, fontFamily: "inherit", textShadow: "2px 2px 2px black", - backgroundColor: "black", + backgroundColor: "#000000ff", }, }, } as MWSettingsData; diff --git a/src/video/components/popouts/CaptionSelectionPopout.tsx b/src/video/components/popouts/CaptionSelectionPopout.tsx index c8f14f14..7682d968 100644 --- a/src/video/components/popouts/CaptionSelectionPopout.tsx +++ b/src/video/components/popouts/CaptionSelectionPopout.tsx @@ -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(false); return ( props.router.navigate("/")} + action={ + + } /> diff --git a/src/video/components/popouts/CaptionSettingsPopout.tsx b/src/video/components/popouts/CaptionSettingsPopout.tsx index ff4cffc6..03f16c63 100644 --- a/src/video/components/popouts/CaptionSettingsPopout.tsx +++ b/src/video/components/popouts/CaptionSettingsPopout.tsx @@ -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; + stops?: number[]; +}; + +function Slider(params: SliderProps) { + const stops = params.stops ?? [Math.floor((params.max + params.min) / 2)]; + return ( +
+
+ + + + {stops.map((s) => ( + +
+
+
+ {params.valueDisplay ?? params.value} +
+
+
+ ); +} + +export function CaptionSettingsPopout(props: { + router: ReturnType; + 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 ( - - { - setCaptionFontFamily(e.id); - }} - selectedItem={selectedFont} - options={fontFamilies} + + props.router.navigate("/captions")} /> -
- - setCaptionFontSize(e.target.valueAsNumber)} - type="range" - name="fontSize" - id="fontSize" - max={30} + + setCaptionDelay(e.target.valueAsNumber)} + stops={[-5, 0, 5]} + /> + setCaptionFontSize(e.target.valueAsNumber)} /> -
- -
- - setCaptionDelay(e.target.valueAsNumber)} - type="range" - max={10 * 1000} - min={-10 * 1000} + -
- -
- - setCaptionColor(e.target.value)} - type="color" - name="captionColor" - id="captionColor" - value={captionSettings.style.color} - /> -
- -
- - setCaptionBackgroundColor(`${e.target.value}`)} - type="color" - name="backgroundColor" - id="backgroundColor" - value={captionSettings.style.backgroundColor} - /> -
-
- - 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 - )} /> -
-
- - { - 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])} - /> -
-
+
+ +
+ {colors.map((color) => ( +
+ setCaptionColor(e.target.value)} + /> +
+ ))} +
+
+
+
); } diff --git a/src/video/components/popouts/SettingsPopout.tsx b/src/video/components/popouts/SettingsPopout.tsx index 9640397d..20e6736f 100644 --- a/src/video/components/popouts/SettingsPopout.tsx +++ b/src/video/components/popouts/SettingsPopout.tsx @@ -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() { + ); }