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 { 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"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue