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:
parent
2ce42fdb85
commit
340673237b
3 changed files with 95 additions and 51 deletions
|
@ -11,6 +11,10 @@ import { Transition } from "@/components/Transition";
|
|||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
|
||||
|
||||
const wordOverrides: Record<string, string> = {
|
||||
i: "I",
|
||||
};
|
||||
|
||||
export function CaptionCue({
|
||||
text,
|
||||
styling,
|
||||
|
@ -20,29 +24,29 @@ export function CaptionCue({
|
|||
styling: SubtitleStyling;
|
||||
overrideCasing: boolean;
|
||||
}) {
|
||||
const wordOverrides: Record<string, string> = {
|
||||
i: "I",
|
||||
};
|
||||
const parsedHtml = useMemo(() => {
|
||||
let textToUse = text;
|
||||
if (overrideCasing && text) {
|
||||
textToUse = text.slice(0, 1) + text.slice(1).toLowerCase();
|
||||
}
|
||||
|
||||
let textToUse = text;
|
||||
if (overrideCasing && text) {
|
||||
textToUse = text.slice(0, 1) + text.slice(1).toLowerCase();
|
||||
}
|
||||
const textWithNewlines = (textToUse || "")
|
||||
.split(" ")
|
||||
.map((word) => wordOverrides[word] ?? word)
|
||||
.join(" ")
|
||||
.replaceAll(/ i'/g, " I'")
|
||||
.replaceAll(/\r?\n/g, "<br />");
|
||||
|
||||
const textWithNewlines = (textToUse || "")
|
||||
.split(" ")
|
||||
.map((word) => wordOverrides[word] ?? word)
|
||||
.join(" ")
|
||||
.replaceAll(/ i'/g, " I'")
|
||||
.replaceAll(/\r?\n/g, "<br />");
|
||||
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
|
||||
// added a <br /> for newlines
|
||||
const html = sanitize(textWithNewlines, {
|
||||
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"],
|
||||
ADD_TAGS: ["v", "lang"],
|
||||
ALLOWED_ATTR: ["title", "lang"],
|
||||
});
|
||||
|
||||
// https://www.w3.org/TR/webvtt1/#dom-construction-rules
|
||||
// added a <br /> for newlines
|
||||
const html = sanitize(textWithNewlines, {
|
||||
ALLOWED_TAGS: ["c", "b", "i", "u", "span", "ruby", "rt", "br"],
|
||||
ADD_TAGS: ["v", "lang"],
|
||||
ALLOWED_ATTR: ["title", "lang"],
|
||||
});
|
||||
return html;
|
||||
}, [text, overrideCasing]);
|
||||
|
||||
return (
|
||||
<p
|
||||
|
@ -57,7 +61,7 @@ export function CaptionCue({
|
|||
// its sanitised a few lines up
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: html,
|
||||
__html: parsedHtml,
|
||||
}}
|
||||
dir="auto"
|
||||
/>
|
||||
|
|
|
@ -32,7 +32,7 @@ function SettingsLayout(props: { children: React.ReactNode }) {
|
|||
)}
|
||||
>
|
||||
<SidebarPart />
|
||||
<div className="space-y-16">{props.children}</div>
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
</WideContainer>
|
||||
);
|
||||
|
@ -80,13 +80,13 @@ export function SettingsPage() {
|
|||
<RegisterCalloutPart />
|
||||
)}
|
||||
</div>
|
||||
<div id="settings-locale">
|
||||
<div id="settings-locale" className="mt-48">
|
||||
<LocalePart />
|
||||
</div>
|
||||
<div id="settings-appearance">
|
||||
<div id="settings-appearance" className="mt-48">
|
||||
<ThemePart active={activeTheme} setTheme={setTheme} />
|
||||
</div>
|
||||
<div id="settings-captions">
|
||||
<div id="settings-captions" className="mt-48">
|
||||
<CaptionsPart />
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import {
|
||||
CaptionSetting,
|
||||
ColorOption,
|
||||
|
@ -5,12 +9,61 @@ import {
|
|||
} from "@/components/player/atoms/settings/CaptionSettingsView";
|
||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||
import { CaptionCue } from "@/components/player/Player";
|
||||
import { Transition } from "@/components/Transition";
|
||||
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() {
|
||||
const styling = useSubtitleStore((s) => s.styling);
|
||||
const isFullscreenPreview = false;
|
||||
const [fullscreenPreview, setFullscreenPreview] = useState(false);
|
||||
const updateStyling = useSubtitleStore((s) => s.updateStyling);
|
||||
|
||||
return (
|
||||
|
@ -48,30 +101,17 @@ export function CaptionsPart() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="w-full aspect-video rounded relative overflow-hidden"
|
||||
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="text-white pointer-events-none absolute flex w-full flex-col items-center transition-[bottom] bottom-0 p-4">
|
||||
<div
|
||||
className={
|
||||
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>
|
||||
<CaptionPreview
|
||||
show
|
||||
styling={styling}
|
||||
onToggle={() => setFullscreenPreview((s) => !s)}
|
||||
/>
|
||||
<CaptionPreview
|
||||
show={fullscreenPreview}
|
||||
fullscreen
|
||||
styling={styling}
|
||||
onToggle={() => setFullscreenPreview((s) => !s)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue