mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 00:35:07 +01:00
fine-tune caption rendering
This commit is contained in:
parent
5664540acc
commit
01f46ce23c
@ -70,7 +70,7 @@
|
||||
"seasons": "Seasons",
|
||||
"captions": "Captions",
|
||||
"captionPreferences": {
|
||||
"title": "Caption Preferences",
|
||||
"title": "Customize",
|
||||
"delay": "Delay",
|
||||
"fontSize": "Size",
|
||||
"opacity": "Opacity",
|
||||
|
@ -8,8 +8,6 @@ interface MWSettingsDataSetters {
|
||||
setCaptionDelay(delay: number): void;
|
||||
setCaptionColor(color: string): void;
|
||||
setCaptionFontSize(size: number): void;
|
||||
setCaptionFontFamily(fontFamily: string): void;
|
||||
setCaptionTextShadow(textShadow: string): void;
|
||||
setCaptionBackgroundColor(backgroundColor: string): void;
|
||||
}
|
||||
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
|
||||
@ -50,23 +48,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
|
||||
setCaptionFontSize(size) {
|
||||
setSettings((oldSettings) => {
|
||||
const style = oldSettings.captionSettings.style;
|
||||
style.fontSize = enforceRange(10, size, 30);
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setCaptionFontFamily(fontFamily) {
|
||||
setSettings((oldSettings) => {
|
||||
const captionStyle = oldSettings.captionSettings.style;
|
||||
captionStyle.fontFamily = fontFamily;
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
},
|
||||
setCaptionTextShadow(textShadow) {
|
||||
setSettings((oldSettings) => {
|
||||
const captionStyle = oldSettings.captionSettings.style;
|
||||
captionStyle.textShadow = textShadow;
|
||||
style.fontSize = enforceRange(10, size, 60);
|
||||
const newSettings = oldSettings;
|
||||
return newSettings;
|
||||
});
|
||||
|
@ -5,20 +5,18 @@ export const SettingsStore = createVersionedStore<MWSettingsData>()
|
||||
.setKey("mw-settings")
|
||||
.addVersion({
|
||||
version: 0,
|
||||
create() {
|
||||
create(): MWSettingsData {
|
||||
return {
|
||||
language: "en",
|
||||
captionSettings: {
|
||||
delay: 0,
|
||||
style: {
|
||||
color: "#ffffff",
|
||||
fontSize: 20,
|
||||
fontFamily: "inherit",
|
||||
textShadow: "2px 2px 2px black",
|
||||
backgroundColor: "#000000ff",
|
||||
fontSize: 25,
|
||||
backgroundColor: "#00000096",
|
||||
},
|
||||
},
|
||||
} as MWSettingsData;
|
||||
};
|
||||
},
|
||||
})
|
||||
.build();
|
||||
|
@ -4,8 +4,6 @@ export interface CaptionStyleSettings {
|
||||
* Range is [10, 30]
|
||||
*/
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
textShadow: string;
|
||||
backgroundColor: string;
|
||||
}
|
||||
|
||||
|
@ -1,25 +0,0 @@
|
||||
import { sanitize } from "@/backend/helpers/captions";
|
||||
import { useSettings } from "@/state/settings";
|
||||
|
||||
export function Caption({ text }: { text?: string }) {
|
||||
const { captionSettings } = useSettings();
|
||||
return (
|
||||
<span
|
||||
className="pointer-events-none mb-1 select-none px-1 text-center"
|
||||
dir="auto"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(text || "", {
|
||||
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
|
||||
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt"],
|
||||
ADD_TAGS: ["v", "lang"],
|
||||
ALLOWED_ATTR: ["title", "lang"],
|
||||
}),
|
||||
}}
|
||||
style={{
|
||||
whiteSpace: "pre-line",
|
||||
...captionSettings.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
@ -28,7 +28,7 @@ import { PopoutProviderAction } from "@/video/components/popouts/PopoutProviderA
|
||||
import { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
||||
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
||||
import { CaptionRenderer } from "./CaptionRenderer";
|
||||
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
|
||||
import { SettingsAction } from "./actions/SettingsAction";
|
||||
import { DividerAction } from "./actions/DividerAction";
|
||||
|
||||
@ -166,7 +166,7 @@ export function VideoPlayer(props: Props) {
|
||||
</Transition>
|
||||
{show ? <PopoutProviderAction /> : null}
|
||||
</BackdropAction>
|
||||
<CaptionRenderer isControlsShown={show} />
|
||||
<CaptionRendererAction isControlsShown={show} />
|
||||
{props.children}
|
||||
</VideoPlayerError>
|
||||
</>
|
||||
|
@ -1,14 +1,36 @@
|
||||
import { Transition } from "@/components/Transition";
|
||||
import { useSettings } from "@/state/settings";
|
||||
import { sanitize } from "@/backend/helpers/captions";
|
||||
import { parse, Cue } from "node-webvtt";
|
||||
import { useRef } from "react";
|
||||
import { useAsync } from "react-use";
|
||||
import { useVideoPlayerDescriptor } from "../state/hooks";
|
||||
import { useProgress } from "../state/logic/progress";
|
||||
import { useSource } from "../state/logic/source";
|
||||
import { Caption } from "./Caption";
|
||||
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||
import { useProgress } from "../../state/logic/progress";
|
||||
import { useSource } from "../../state/logic/source";
|
||||
|
||||
export function CaptionRenderer({
|
||||
function CaptionCue({ text }: { text?: string }) {
|
||||
const { captionSettings } = useSettings();
|
||||
return (
|
||||
<span
|
||||
className="pointer-events-none mb-1 select-none whitespace-pre-line rounded px-4 py-1 text-center [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
||||
dir="auto"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: sanitize(text || "", {
|
||||
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
|
||||
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt"],
|
||||
ADD_TAGS: ["v", "lang"],
|
||||
ALLOWED_ATTR: ["title", "lang"],
|
||||
}),
|
||||
}}
|
||||
style={{
|
||||
...captionSettings.style,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CaptionRendererAction({
|
||||
isControlsShown,
|
||||
}: {
|
||||
isControlsShown: boolean;
|
||||
@ -44,7 +66,7 @@ export function CaptionRenderer({
|
||||
return (
|
||||
<Transition
|
||||
className={[
|
||||
"absolute flex w-full flex-col items-center transition-[bottom]",
|
||||
"pointer-events-none absolute flex w-full flex-col items-center transition-[bottom]",
|
||||
isControlsShown ? "bottom-24" : "bottom-12",
|
||||
].join(" ")}
|
||||
animation="slide-up"
|
||||
@ -53,7 +75,7 @@ export function CaptionRenderer({
|
||||
{captions.current.map(
|
||||
({ identifier, end, start, text }) =>
|
||||
isVisible(start, end) && (
|
||||
<Caption key={identifier || `${start}-${end}`} text={text} />
|
||||
<CaptionCue key={identifier || `${start}-${end}`} text={text} />
|
||||
)
|
||||
)}
|
||||
</Transition>
|
@ -44,11 +44,7 @@ function VideoElement(props: Props) {
|
||||
muted={mediaPlaying.volume === 0}
|
||||
playsInline
|
||||
className="h-full w-full"
|
||||
>
|
||||
{/* {source.source?.caption ? (
|
||||
<track default kind="captions" src={source.source.caption.url} />
|
||||
) : null} */}
|
||||
</video>
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -14,12 +14,10 @@ export type SliderProps = {
|
||||
value: number;
|
||||
valueDisplay?: string;
|
||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||
stops?: number[];
|
||||
};
|
||||
|
||||
export function Slider(props: SliderProps) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
const stops = props.stops ?? [Math.floor((props.max + props.min) / 2)];
|
||||
useEffect(() => {
|
||||
const e = ref.current as HTMLInputElement;
|
||||
e.style.setProperty("--value", e.value);
|
||||
@ -41,13 +39,7 @@ export function Slider(props: SliderProps) {
|
||||
max={props.max}
|
||||
min={props.min}
|
||||
step={props.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">
|
||||
@ -88,13 +80,12 @@ export function CaptionSettingsPopout(props: {
|
||||
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
|
||||
value={captionSettings.delay}
|
||||
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
||||
stops={[-5, 0, 5]}
|
||||
/>
|
||||
<Slider
|
||||
label="Size"
|
||||
min={10}
|
||||
min={14}
|
||||
step={1}
|
||||
max={30}
|
||||
max={60}
|
||||
value={captionSettings.style.fontSize}
|
||||
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||
/>
|
||||
@ -131,20 +122,16 @@ export function CaptionSettingsPopout(props: {
|
||||
<div className="flex flex-row gap-2">
|
||||
{colors.map((color) => (
|
||||
<div
|
||||
className={`flex h-8 w-8 items-center justify-center rounded ${
|
||||
className={`flex h-8 w-8 items-center justify-center rounded transition-[background-color,transform] duration-100 hover:bg-[#1c161b79] active:scale-110 ${
|
||||
color === captionSettings.style.color ? "bg-[#1C161B]" : ""
|
||||
}`}
|
||||
onClick={() => setCaptionColor(color)}
|
||||
>
|
||||
<input
|
||||
<div
|
||||
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)}
|
||||
/>
|
||||
<Icon
|
||||
className={[
|
||||
|
Loading…
Reference in New Issue
Block a user