1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

Add fullscreen preview for caption settings + optimize subtitle rendering

This commit is contained in:
mrjvs 2023-11-20 19:36:35 +01:00
parent 2ce42fdb85
commit 340673237b
3 changed files with 95 additions and 51 deletions

View file

@ -11,6 +11,10 @@ import { Transition } from "@/components/Transition";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles"; import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
const wordOverrides: Record<string, string> = {
i: "I",
};
export function CaptionCue({ export function CaptionCue({
text, text,
styling, styling,
@ -20,29 +24,29 @@ export function CaptionCue({
styling: SubtitleStyling; styling: SubtitleStyling;
overrideCasing: boolean; overrideCasing: boolean;
}) { }) {
const wordOverrides: Record<string, string> = { const parsedHtml = useMemo(() => {
i: "I", let textToUse = text;
}; if (overrideCasing && text) {
textToUse = text.slice(0, 1) + text.slice(1).toLowerCase();
}
let textToUse = text; const textWithNewlines = (textToUse || "")
if (overrideCasing && text) { .split(" ")
textToUse = text.slice(0, 1) + text.slice(1).toLowerCase(); .map((word) => wordOverrides[word] ?? word)
} .join(" ")
.replaceAll(/ i'/g, " I'")
.replaceAll(/\r?\n/g, "<br />");
const textWithNewlines = (textToUse || "") // https://www.w3.org/TR/webvtt1/#dom-construction-rules
.split(" ") // added a <br /> for newlines
.map((word) => wordOverrides[word] ?? word) const html = sanitize(textWithNewlines, {
.join(" ") ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"],
.replaceAll(/ i'/g, " I'") ADD_TAGS: ["v", "lang"],
.replaceAll(/\r?\n/g, "<br />"); ALLOWED_ATTR: ["title", "lang"],
});
// https://www.w3.org/TR/webvtt1/#dom-construction-rules return html;
// added a <br /> for newlines }, [text, overrideCasing]);
const html = sanitize(textWithNewlines, {
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"],
ADD_TAGS: ["v", "lang"],
ALLOWED_ATTR: ["title", "lang"],
});
return ( return (
<p <p
@ -57,7 +61,7 @@ export function CaptionCue({
// its sanitised a few lines up // its sanitised a few lines up
// eslint-disable-next-line react/no-danger // eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: html, __html: parsedHtml,
}} }}
dir="auto" dir="auto"
/> />

View file

@ -32,7 +32,7 @@ function SettingsLayout(props: { children: React.ReactNode }) {
)} )}
> >
<SidebarPart /> <SidebarPart />
<div className="space-y-16">{props.children}</div> <div>{props.children}</div>
</div> </div>
</WideContainer> </WideContainer>
); );
@ -80,13 +80,13 @@ export function SettingsPage() {
<RegisterCalloutPart /> <RegisterCalloutPart />
)} )}
</div> </div>
<div id="settings-locale"> <div id="settings-locale" className="mt-48">
<LocalePart /> <LocalePart />
</div> </div>
<div id="settings-appearance"> <div id="settings-appearance" className="mt-48">
<ThemePart active={activeTheme} setTheme={setTheme} /> <ThemePart active={activeTheme} setTheme={setTheme} />
</div> </div>
<div id="settings-captions"> <div id="settings-captions" className="mt-48">
<CaptionsPart /> <CaptionsPart />
</div> </div>
</SettingsLayout> </SettingsLayout>

View file

@ -1,3 +1,7 @@
import classNames from "classnames";
import { useState } from "react";
import { Icon, Icons } from "@/components/Icon";
import { import {
CaptionSetting, CaptionSetting,
ColorOption, ColorOption,
@ -5,12 +9,61 @@ import {
} from "@/components/player/atoms/settings/CaptionSettingsView"; } from "@/components/player/atoms/settings/CaptionSettingsView";
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
import { CaptionCue } from "@/components/player/Player"; import { CaptionCue } from "@/components/player/Player";
import { Transition } from "@/components/Transition";
import { Heading1 } from "@/components/utils/Text"; import { Heading1 } from "@/components/utils/Text";
import { useSubtitleStore } from "@/stores/subtitles"; import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
export function CaptionPreview(props: {
fullscreen?: boolean;
show?: boolean;
styling: SubtitleStyling;
onToggle: () => void;
}) {
return (
<div
className={classNames({
"pointer-events-none overflow-hidden w-full rounded": true,
"aspect-video relative": !props.fullscreen,
"fixed inset-0 z-50": props.fullscreen,
})}
>
<Transition animation="fade" show={props.show}>
<div
className="absolute inset-0 pointer-events-auto"
style={{
backgroundImage:
"radial-gradient(102.95% 87.07% at 100% 100%, #EEAA45 0%, rgba(165, 186, 151, 0.56) 54.69%, rgba(74, 207, 254, 0.00) 100%), linear-gradient(180deg, #48D3FF 0%, #3B27B2 100%)",
}}
>
<div
className="bg-black absolute right-3 top-3 text-white bg-opacity-25 duration-100 transition-[background-color,transform] active:scale-110 hover:bg-opacity-50 p-2 rounded-md cursor-pointer"
onClick={props.onToggle}
>
<Icon icon={props.fullscreen ? Icons.X : Icons.EXPAND} />
</div>
<div className="text-white pointer-events-none absolute flex w-full flex-col items-center transition-[bottom] bottom-0 p-4">
<div
className={
props.fullscreen ? "" : "transform origin-bottom text-[0.5rem]"
}
>
<CaptionCue
text="I must not fear. Fear is the mind-killer."
styling={props.styling}
overrideCasing={false}
/>
</div>
</div>
</div>
</Transition>
</div>
);
}
export function CaptionsPart() { export function CaptionsPart() {
const styling = useSubtitleStore((s) => s.styling); const styling = useSubtitleStore((s) => s.styling);
const isFullscreenPreview = false; const [fullscreenPreview, setFullscreenPreview] = useState(false);
const updateStyling = useSubtitleStore((s) => s.updateStyling); const updateStyling = useSubtitleStore((s) => s.updateStyling);
return ( return (
@ -48,30 +101,17 @@ export function CaptionsPart() {
</div> </div>
</div> </div>
</div> </div>
<div <CaptionPreview
className="w-full aspect-video rounded relative overflow-hidden" show
style={{ styling={styling}
backgroundImage: onToggle={() => setFullscreenPreview((s) => !s)}
"radial-gradient(102.95% 87.07% at 100% 100%, #EEAA45 0%, rgba(165, 186, 151, 0.56) 54.69%, rgba(74, 207, 254, 0.00) 100%), linear-gradient(180deg, #48D3FF 0%, #3B27B2 100%)", />
}} <CaptionPreview
> show={fullscreenPreview}
<div className="text-white pointer-events-none absolute flex w-full flex-col items-center transition-[bottom] bottom-0 p-4"> fullscreen
<div styling={styling}
className={ onToggle={() => setFullscreenPreview((s) => !s)}
isFullscreenPreview />
? ""
: "transform origin-bottom text-[0.5rem]"
}
>
<CaptionCue
// Can we keep this Dune quote 🥺
text="I must not fear. Fear is the mind-killer."
styling={styling}
overrideCasing={false}
/>
</div>
</div>
</div>
</div> </div>
</div> </div>
); );