mirror of
https://github.com/movie-web/movie-web.git
synced 2024-11-11 01:55:05 +01:00
fine-tune caption rendering
This commit is contained in:
parent
5664540acc
commit
01f46ce23c
@ -70,7 +70,7 @@
|
|||||||
"seasons": "Seasons",
|
"seasons": "Seasons",
|
||||||
"captions": "Captions",
|
"captions": "Captions",
|
||||||
"captionPreferences": {
|
"captionPreferences": {
|
||||||
"title": "Caption Preferences",
|
"title": "Customize",
|
||||||
"delay": "Delay",
|
"delay": "Delay",
|
||||||
"fontSize": "Size",
|
"fontSize": "Size",
|
||||||
"opacity": "Opacity",
|
"opacity": "Opacity",
|
||||||
|
@ -8,8 +8,6 @@ interface MWSettingsDataSetters {
|
|||||||
setCaptionDelay(delay: number): void;
|
setCaptionDelay(delay: number): void;
|
||||||
setCaptionColor(color: string): void;
|
setCaptionColor(color: string): void;
|
||||||
setCaptionFontSize(size: number): void;
|
setCaptionFontSize(size: number): void;
|
||||||
setCaptionFontFamily(fontFamily: string): void;
|
|
||||||
setCaptionTextShadow(textShadow: string): void;
|
|
||||||
setCaptionBackgroundColor(backgroundColor: string): void;
|
setCaptionBackgroundColor(backgroundColor: string): void;
|
||||||
}
|
}
|
||||||
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
|
type MWSettingsDataWrapper = MWSettingsData & MWSettingsDataSetters;
|
||||||
@ -50,23 +48,7 @@ export function SettingsProvider(props: { children: ReactNode }) {
|
|||||||
setCaptionFontSize(size) {
|
setCaptionFontSize(size) {
|
||||||
setSettings((oldSettings) => {
|
setSettings((oldSettings) => {
|
||||||
const style = oldSettings.captionSettings.style;
|
const style = oldSettings.captionSettings.style;
|
||||||
style.fontSize = enforceRange(10, size, 30);
|
style.fontSize = enforceRange(10, size, 60);
|
||||||
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;
|
|
||||||
const newSettings = oldSettings;
|
const newSettings = oldSettings;
|
||||||
return newSettings;
|
return newSettings;
|
||||||
});
|
});
|
||||||
|
@ -5,20 +5,18 @@ export const SettingsStore = createVersionedStore<MWSettingsData>()
|
|||||||
.setKey("mw-settings")
|
.setKey("mw-settings")
|
||||||
.addVersion({
|
.addVersion({
|
||||||
version: 0,
|
version: 0,
|
||||||
create() {
|
create(): MWSettingsData {
|
||||||
return {
|
return {
|
||||||
language: "en",
|
language: "en",
|
||||||
captionSettings: {
|
captionSettings: {
|
||||||
delay: 0,
|
delay: 0,
|
||||||
style: {
|
style: {
|
||||||
color: "#ffffff",
|
color: "#ffffff",
|
||||||
fontSize: 20,
|
fontSize: 25,
|
||||||
fontFamily: "inherit",
|
backgroundColor: "#00000096",
|
||||||
textShadow: "2px 2px 2px black",
|
|
||||||
backgroundColor: "#000000ff",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} as MWSettingsData;
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.build();
|
.build();
|
||||||
|
@ -4,8 +4,6 @@ export interface CaptionStyleSettings {
|
|||||||
* Range is [10, 30]
|
* Range is [10, 30]
|
||||||
*/
|
*/
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
fontFamily: string;
|
|
||||||
textShadow: string;
|
|
||||||
backgroundColor: 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 { ChromecastAction } from "@/video/components/actions/ChromecastAction";
|
||||||
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
import { CastingTextAction } from "@/video/components/actions/CastingTextAction";
|
||||||
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
import { PictureInPictureAction } from "@/video/components/actions/PictureInPictureAction";
|
||||||
import { CaptionRenderer } from "./CaptionRenderer";
|
import { CaptionRendererAction } from "./actions/CaptionRendererAction";
|
||||||
import { SettingsAction } from "./actions/SettingsAction";
|
import { SettingsAction } from "./actions/SettingsAction";
|
||||||
import { DividerAction } from "./actions/DividerAction";
|
import { DividerAction } from "./actions/DividerAction";
|
||||||
|
|
||||||
@ -166,7 +166,7 @@ export function VideoPlayer(props: Props) {
|
|||||||
</Transition>
|
</Transition>
|
||||||
{show ? <PopoutProviderAction /> : null}
|
{show ? <PopoutProviderAction /> : null}
|
||||||
</BackdropAction>
|
</BackdropAction>
|
||||||
<CaptionRenderer isControlsShown={show} />
|
<CaptionRendererAction isControlsShown={show} />
|
||||||
{props.children}
|
{props.children}
|
||||||
</VideoPlayerError>
|
</VideoPlayerError>
|
||||||
</>
|
</>
|
||||||
|
@ -1,14 +1,36 @@
|
|||||||
import { Transition } from "@/components/Transition";
|
import { Transition } from "@/components/Transition";
|
||||||
import { useSettings } from "@/state/settings";
|
import { useSettings } from "@/state/settings";
|
||||||
|
import { sanitize } from "@/backend/helpers/captions";
|
||||||
import { parse, Cue } from "node-webvtt";
|
import { parse, Cue } from "node-webvtt";
|
||||||
import { useRef } from "react";
|
import { useRef } from "react";
|
||||||
import { useAsync } from "react-use";
|
import { useAsync } from "react-use";
|
||||||
import { useVideoPlayerDescriptor } from "../state/hooks";
|
import { useVideoPlayerDescriptor } from "../../state/hooks";
|
||||||
import { useProgress } from "../state/logic/progress";
|
import { useProgress } from "../../state/logic/progress";
|
||||||
import { useSource } from "../state/logic/source";
|
import { useSource } from "../../state/logic/source";
|
||||||
import { Caption } from "./Caption";
|
|
||||||
|
|
||||||
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,
|
||||||
}: {
|
}: {
|
||||||
isControlsShown: boolean;
|
isControlsShown: boolean;
|
||||||
@ -44,7 +66,7 @@ export function CaptionRenderer({
|
|||||||
return (
|
return (
|
||||||
<Transition
|
<Transition
|
||||||
className={[
|
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",
|
isControlsShown ? "bottom-24" : "bottom-12",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
animation="slide-up"
|
animation="slide-up"
|
||||||
@ -53,7 +75,7 @@ export function CaptionRenderer({
|
|||||||
{captions.current.map(
|
{captions.current.map(
|
||||||
({ identifier, end, start, text }) =>
|
({ identifier, end, start, text }) =>
|
||||||
isVisible(start, end) && (
|
isVisible(start, end) && (
|
||||||
<Caption key={identifier || `${start}-${end}`} text={text} />
|
<CaptionCue key={identifier || `${start}-${end}`} text={text} />
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Transition>
|
</Transition>
|
@ -44,11 +44,7 @@ function VideoElement(props: Props) {
|
|||||||
muted={mediaPlaying.volume === 0}
|
muted={mediaPlaying.volume === 0}
|
||||||
playsInline
|
playsInline
|
||||||
className="h-full w-full"
|
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;
|
value: number;
|
||||||
valueDisplay?: string;
|
valueDisplay?: string;
|
||||||
onChange: ChangeEventHandler<HTMLInputElement>;
|
onChange: ChangeEventHandler<HTMLInputElement>;
|
||||||
stops?: number[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Slider(props: SliderProps) {
|
export function Slider(props: SliderProps) {
|
||||||
const ref = useRef<HTMLInputElement>(null);
|
const ref = useRef<HTMLInputElement>(null);
|
||||||
const stops = props.stops ?? [Math.floor((props.max + props.min) / 2)];
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const e = ref.current as HTMLInputElement;
|
const e = ref.current as HTMLInputElement;
|
||||||
e.style.setProperty("--value", e.value);
|
e.style.setProperty("--value", e.value);
|
||||||
@ -41,13 +39,7 @@ export function Slider(props: SliderProps) {
|
|||||||
max={props.max}
|
max={props.max}
|
||||||
min={props.min}
|
min={props.min}
|
||||||
step={props.step}
|
step={props.step}
|
||||||
list="stops"
|
|
||||||
/>
|
/>
|
||||||
<datalist id="stops">
|
|
||||||
{stops.map((s) => (
|
|
||||||
<option value={s} />
|
|
||||||
))}
|
|
||||||
</datalist>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
|
<div className="mt-1 aspect-[2/1] h-8 rounded-sm bg-[#1C161B] pt-1">
|
||||||
<div className="text-center font-bold text-white">
|
<div className="text-center font-bold text-white">
|
||||||
@ -88,13 +80,12 @@ export function CaptionSettingsPopout(props: {
|
|||||||
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
|
valueDisplay={`${captionSettings.delay.toFixed(1)}s`}
|
||||||
value={captionSettings.delay}
|
value={captionSettings.delay}
|
||||||
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
onChange={(e) => setCaptionDelay(e.target.valueAsNumber)}
|
||||||
stops={[-5, 0, 5]}
|
|
||||||
/>
|
/>
|
||||||
<Slider
|
<Slider
|
||||||
label="Size"
|
label="Size"
|
||||||
min={10}
|
min={14}
|
||||||
step={1}
|
step={1}
|
||||||
max={30}
|
max={60}
|
||||||
value={captionSettings.style.fontSize}
|
value={captionSettings.style.fontSize}
|
||||||
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
onChange={(e) => setCaptionFontSize(e.target.valueAsNumber)}
|
||||||
/>
|
/>
|
||||||
@ -131,20 +122,16 @@ export function CaptionSettingsPopout(props: {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{colors.map((color) => (
|
{colors.map((color) => (
|
||||||
<div
|
<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]" : ""
|
color === captionSettings.style.color ? "bg-[#1C161B]" : ""
|
||||||
}`}
|
}`}
|
||||||
|
onClick={() => setCaptionColor(color)}
|
||||||
>
|
>
|
||||||
<input
|
<div
|
||||||
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
|
className="h-4 w-4 cursor-pointer appearance-none rounded-full"
|
||||||
type="radio"
|
|
||||||
name="color"
|
|
||||||
key={color}
|
|
||||||
value={color}
|
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
}}
|
}}
|
||||||
onChange={(e) => setCaptionColor(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
className={[
|
className={[
|
||||||
|
Loading…
Reference in New Issue
Block a user