mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 08:42:20 +00:00
add text styles to captions
This commit is contained in:
parent
53c6de1e6f
commit
3de83f28dd
5 changed files with 234 additions and 100 deletions
|
|
@ -770,7 +770,15 @@
|
||||||
"textBoldLabel": "Bold text",
|
"textBoldLabel": "Bold text",
|
||||||
"verticalPositionLabel": "Vertical position",
|
"verticalPositionLabel": "Vertical position",
|
||||||
"default": "Default",
|
"default": "Default",
|
||||||
"low": "Low"
|
"low": "Low",
|
||||||
|
"textStyle": {
|
||||||
|
"title": "Text style",
|
||||||
|
"default": "Default",
|
||||||
|
"raised": "Raised",
|
||||||
|
"depressed": "Depressed",
|
||||||
|
"uniform": "Uniform",
|
||||||
|
"dropShadow": "Drop Shadow"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"unsaved": "You have unsaved changes... ฅ^•ﻌ•^ฅ"
|
"unsaved": "You have unsaved changes... ฅ^•ﻌ•^ฅ"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import { Menu } from "@/components/player/internals/ContextMenu";
|
import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
|
|
@ -249,6 +250,7 @@ export function CaptionSettingsView({
|
||||||
size: 1,
|
size: 1,
|
||||||
backgroundBlur: 0.5,
|
backgroundBlur: 0.5,
|
||||||
bold: false,
|
bold: false,
|
||||||
|
fontStyle: "none",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -283,19 +285,6 @@ export function CaptionSettingsView({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Menu.FieldTitle>
|
|
||||||
{t("settings.subtitles.textBoldLabel")}
|
|
||||||
</Menu.FieldTitle>
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<Toggle
|
|
||||||
enabled={styling.bold}
|
|
||||||
onClick={() =>
|
|
||||||
handleStylingChange({ ...styling, bold: !styling.bold })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Menu.Divider />
|
<Menu.Divider />
|
||||||
<CaptionSetting
|
<CaptionSetting
|
||||||
label={t("settings.subtitles.backgroundLabel")}
|
label={t("settings.subtitles.backgroundLabel")}
|
||||||
|
|
@ -325,6 +314,91 @@ export function CaptionSettingsView({
|
||||||
onChange={(v) => handleStylingChange({ ...styling, size: v / 100 })}
|
onChange={(v) => handleStylingChange({ ...styling, size: v / 100 })}
|
||||||
value={styling.size * 100}
|
value={styling.size * 100}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.subtitles.textStyle.title") || "Font Style"}
|
||||||
|
</Menu.FieldTitle>
|
||||||
|
<div className="w-64">
|
||||||
|
<Dropdown
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: "default",
|
||||||
|
name: t("settings.subtitles.textStyle.default"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "raised",
|
||||||
|
name: t("settings.subtitles.textStyle.raised"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "depressed",
|
||||||
|
name: t("settings.subtitles.textStyle.depressed"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "uniform",
|
||||||
|
name: t("settings.subtitles.textStyle.uniform"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dropShadow",
|
||||||
|
name: t("settings.subtitles.textStyle.dropShadow"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
selectedItem={{
|
||||||
|
id: styling.fontStyle,
|
||||||
|
name:
|
||||||
|
t(`settings.subtitles.textStyle.${styling.fontStyle}`) ||
|
||||||
|
styling.fontStyle,
|
||||||
|
}}
|
||||||
|
setSelectedItem={(item) =>
|
||||||
|
handleStylingChange({
|
||||||
|
...styling,
|
||||||
|
fontStyle: item.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.subtitles.textBoldLabel")}
|
||||||
|
</Menu.FieldTitle>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Toggle
|
||||||
|
enabled={styling.bold}
|
||||||
|
onClick={() =>
|
||||||
|
handleStylingChange({ ...styling, bold: !styling.bold })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.subtitles.colorLabel")}
|
||||||
|
</Menu.FieldTitle>
|
||||||
|
<div className="flex justify-center items-center space-x-2">
|
||||||
|
{colors.map((color) => (
|
||||||
|
<ColorOption
|
||||||
|
key={color}
|
||||||
|
color={color}
|
||||||
|
active={styling.color === color}
|
||||||
|
onClick={() => handleStylingChange({ ...styling, color })}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="relative inline-block">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={styling.color}
|
||||||
|
onChange={(e) => {
|
||||||
|
const color = e.target.value;
|
||||||
|
handleStylingChange({ ...styling, color });
|
||||||
|
}}
|
||||||
|
className="absolute opacity-0 cursor-pointer w-10 h-10"
|
||||||
|
/>
|
||||||
|
<div style={{ color: styling.color }}>
|
||||||
|
<Icon icon={Icons.BRUSH} className="text-2xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Menu.FieldTitle>
|
<Menu.FieldTitle>
|
||||||
{t("settings.subtitles.verticalPositionLabel")}
|
{t("settings.subtitles.verticalPositionLabel")}
|
||||||
|
|
@ -366,36 +440,6 @@ export function CaptionSettingsView({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Menu.FieldTitle>
|
|
||||||
{t("settings.subtitles.colorLabel")}
|
|
||||||
</Menu.FieldTitle>
|
|
||||||
<div className="flex justify-center items-center space-x-2">
|
|
||||||
{colors.map((color) => (
|
|
||||||
<ColorOption
|
|
||||||
key={color}
|
|
||||||
color={color}
|
|
||||||
active={styling.color === color}
|
|
||||||
onClick={() => handleStylingChange({ ...styling, color })}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{/* Add Color Picker */}
|
|
||||||
<div className="relative inline-block">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={styling.color}
|
|
||||||
onChange={(e) => {
|
|
||||||
const color = e.target.value;
|
|
||||||
handleStylingChange({ ...styling, color });
|
|
||||||
}}
|
|
||||||
className="absolute opacity-0 cursor-pointer w-10 h-10"
|
|
||||||
/>
|
|
||||||
<div style={{ color: styling.color }}>
|
|
||||||
<Icon icon={Icons.BRUSH} className="text-2xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full md:w-auto"
|
className="w-full md:w-auto"
|
||||||
theme="secondary"
|
theme="secondary"
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,35 @@ export function CaptionCue({
|
||||||
return html;
|
return html;
|
||||||
}, [text, overrideCasing]);
|
}, [text, overrideCasing]);
|
||||||
|
|
||||||
|
const getTextEffectStyles = () => {
|
||||||
|
switch (styling.fontStyle) {
|
||||||
|
case "raised":
|
||||||
|
return {
|
||||||
|
textShadow: "0 2px 0 rgba(0,0,0,0.8), 0 1.5px 1.5px rgba(0,0,0,0.9)",
|
||||||
|
};
|
||||||
|
case "depressed":
|
||||||
|
return {
|
||||||
|
textShadow:
|
||||||
|
"0 -2px 0 rgba(0,0,0,0.8), 0 -1.5px 1.5px rgba(0,0,0,0.9)",
|
||||||
|
};
|
||||||
|
case "uniform":
|
||||||
|
return {
|
||||||
|
textShadow:
|
||||||
|
"1.5px 1.5px 1.5px rgba(0,0,0,0.8), -1.5px -1.5px 1.5px rgba(0,0,0,0.8), 1.5px -1.5px 1.5px rgba(0,0,0,0.8), -1.5px 1.5px 1.5px rgba(0,0,0,0.8)",
|
||||||
|
};
|
||||||
|
case "dropShadow":
|
||||||
|
return { textShadow: "2.5px 2.5px 4.5px rgba(0,0,0,0.9)" };
|
||||||
|
case "none":
|
||||||
|
default:
|
||||||
|
return { textShadow: "0 2px 4px rgba(0,0,0,0.5)" }; // Default is a light drop shadow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const textEffectStyles = getTextEffectStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p
|
<p
|
||||||
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center leading-normal [text-shadow:0_2px_4px_rgba(0,0,0,0.5)]"
|
className="pointer-events-none mb-1 select-none rounded px-4 py-1 text-center leading-normal"
|
||||||
style={{
|
style={{
|
||||||
color: styling.color,
|
color: styling.color,
|
||||||
fontSize: `${(1.5 * styling.size).toFixed(2)}em`,
|
fontSize: `${(1.5 * styling.size).toFixed(2)}em`,
|
||||||
|
|
@ -68,6 +94,7 @@ export function CaptionCue({
|
||||||
? `blur(${Math.floor(styling.backgroundBlur * 64)}px)`
|
? `blur(${Math.floor(styling.backgroundBlur * 64)}px)`
|
||||||
: "none",
|
: "none",
|
||||||
fontWeight: styling.bold ? "bold" : "normal",
|
fontWeight: styling.bold ? "bold" : "normal",
|
||||||
|
...textEffectStyles,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button } from "@/components/buttons/Button";
|
import { Button } from "@/components/buttons/Button";
|
||||||
import { Toggle } from "@/components/buttons/Toggle";
|
import { Toggle } from "@/components/buttons/Toggle";
|
||||||
|
import { Dropdown } from "@/components/form/Dropdown";
|
||||||
import { Icon, Icons } from "@/components/Icon";
|
import { Icon, Icons } from "@/components/Icon";
|
||||||
import {
|
import {
|
||||||
CaptionSetting,
|
CaptionSetting,
|
||||||
|
|
@ -104,6 +105,7 @@ export function CaptionsPart(props: {
|
||||||
backgroundBlur: 0.5,
|
backgroundBlur: 0.5,
|
||||||
bold: false,
|
bold: false,
|
||||||
verticalPosition: 3,
|
verticalPosition: 3,
|
||||||
|
fontStyle: "none",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -151,6 +153,104 @@ export function CaptionsPart(props: {
|
||||||
}
|
}
|
||||||
value={props.styling.size * 100}
|
value={props.styling.size * 100}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.subtitles.textStyle.title")}
|
||||||
|
</Menu.FieldTitle>
|
||||||
|
<div className="w-30">
|
||||||
|
<Dropdown
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
id: "default",
|
||||||
|
name: t("settings.subtitles.textStyle.default"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "raised",
|
||||||
|
name: t("settings.subtitles.textStyle.raised"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "depressed",
|
||||||
|
name: t("settings.subtitles.textStyle.depressed"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "uniform",
|
||||||
|
name: t("settings.subtitles.textStyle.uniform"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "dropShadow",
|
||||||
|
name: t("settings.subtitles.textStyle.dropShadow"),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
selectedItem={{
|
||||||
|
id: props.styling.fontStyle,
|
||||||
|
name:
|
||||||
|
t(
|
||||||
|
`settings.subtitles.textStyle.${props.styling.fontStyle}`,
|
||||||
|
) || props.styling.fontStyle,
|
||||||
|
}}
|
||||||
|
setSelectedItem={(item) =>
|
||||||
|
handleStylingChange({
|
||||||
|
...props.styling,
|
||||||
|
fontStyle: item.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.subtitles.textBoldLabel")}
|
||||||
|
</Menu.FieldTitle>
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
<Toggle
|
||||||
|
enabled={props.styling.bold}
|
||||||
|
onClick={() =>
|
||||||
|
handleStylingChange({
|
||||||
|
...props.styling,
|
||||||
|
bold: !props.styling.bold,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<Menu.FieldTitle>
|
||||||
|
{t("settings.subtitles.colorLabel")}
|
||||||
|
</Menu.FieldTitle>
|
||||||
|
<div className="flex justify-center items-center space-x-2">
|
||||||
|
{colors.map((v) => (
|
||||||
|
<ColorOption
|
||||||
|
onClick={() =>
|
||||||
|
handleStylingChange({
|
||||||
|
...props.styling,
|
||||||
|
color: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
color={v}
|
||||||
|
active={props.styling.color === v}
|
||||||
|
key={v}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
value={props.styling.color}
|
||||||
|
onChange={(e) => {
|
||||||
|
const color = e.target.value;
|
||||||
|
handleStylingChange({ ...props.styling, color });
|
||||||
|
subtitleStore.updateStyling({
|
||||||
|
...props.styling,
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="absolute opacity-0 cursor-pointer w-8 h-8"
|
||||||
|
/>
|
||||||
|
<div style={{ color: props.styling.color }}>
|
||||||
|
<Icon icon={Icons.BRUSH} className="text-2xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<Menu.FieldTitle>
|
<Menu.FieldTitle>
|
||||||
{t("settings.subtitles.verticalPositionLabel")}
|
{t("settings.subtitles.verticalPositionLabel")}
|
||||||
|
|
@ -192,61 +292,6 @@ export function CaptionsPart(props: {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Menu.FieldTitle>
|
|
||||||
{t("settings.subtitles.textBoldLabel")}
|
|
||||||
</Menu.FieldTitle>
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<Toggle
|
|
||||||
enabled={props.styling.bold}
|
|
||||||
onClick={() =>
|
|
||||||
handleStylingChange({
|
|
||||||
...props.styling,
|
|
||||||
bold: !props.styling.bold,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between items-center">
|
|
||||||
<Menu.FieldTitle>
|
|
||||||
{t("settings.subtitles.colorLabel")}
|
|
||||||
</Menu.FieldTitle>
|
|
||||||
<div className="flex justify-center items-center space-x-2">
|
|
||||||
{colors.map((v) => (
|
|
||||||
<ColorOption
|
|
||||||
onClick={() =>
|
|
||||||
handleStylingChange({
|
|
||||||
...props.styling,
|
|
||||||
color: v,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
color={v}
|
|
||||||
active={props.styling.color === v}
|
|
||||||
key={v}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{/* Add Color Picker */}
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={props.styling.color}
|
|
||||||
onChange={(e) => {
|
|
||||||
const color = e.target.value;
|
|
||||||
handleStylingChange({ ...props.styling, color });
|
|
||||||
subtitleStore.updateStyling({
|
|
||||||
...props.styling,
|
|
||||||
color,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="absolute opacity-0 cursor-pointer w-8 h-8"
|
|
||||||
/>
|
|
||||||
<div style={{ color: props.styling.color }}>
|
|
||||||
<Icon icon={Icons.BRUSH} className="text-2xl" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<CaptionPreview
|
<CaptionPreview
|
||||||
show
|
show
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,12 @@ export interface SubtitleStyling {
|
||||||
* vertical position percentage, ranges between 1 and 3 (rem)
|
* vertical position percentage, ranges between 1 and 3 (rem)
|
||||||
*/
|
*/
|
||||||
verticalPosition: number;
|
verticalPosition: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* font style for text rendering
|
||||||
|
* "default" | "raised" | "depressed" | "uniform" | "dropShadow"
|
||||||
|
*/
|
||||||
|
fontStyle: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubtitleStore {
|
export interface SubtitleStore {
|
||||||
|
|
@ -76,6 +82,7 @@ export const useSubtitleStore = create(
|
||||||
backgroundBlur: 0.5,
|
backgroundBlur: 0.5,
|
||||||
bold: false,
|
bold: false,
|
||||||
verticalPosition: 3,
|
verticalPosition: 3,
|
||||||
|
fontStyle: "none",
|
||||||
},
|
},
|
||||||
showDelayIndicator: false,
|
showDelayIndicator: false,
|
||||||
resetSubtitleSpecificSettings() {
|
resetSubtitleSpecificSettings() {
|
||||||
|
|
@ -106,6 +113,8 @@ export const useSubtitleStore = create(
|
||||||
100,
|
100,
|
||||||
Math.max(0, newStyling.verticalPosition),
|
Math.max(0, newStyling.verticalPosition),
|
||||||
);
|
);
|
||||||
|
if (newStyling.fontStyle !== undefined)
|
||||||
|
s.styling.fontStyle = newStyling.fontStyle;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetStyling() {
|
resetStyling() {
|
||||||
|
|
@ -117,6 +126,7 @@ export const useSubtitleStore = create(
|
||||||
backgroundBlur: 0.5,
|
backgroundBlur: 0.5,
|
||||||
bold: false,
|
bold: false,
|
||||||
verticalPosition: 3,
|
verticalPosition: 3,
|
||||||
|
fontStyle: "none",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue