make native subtitles a proper setting

This commit is contained in:
Pas 2025-07-07 22:44:18 -06:00
parent 30f0d4ac52
commit 0edb4ece08
6 changed files with 260 additions and 189 deletions

View file

@ -23,6 +23,7 @@ export interface SettingsInput {
enableSourceOrder?: boolean;
proxyTmdb?: boolean;
enableLowPerformanceMode?: boolean;
enableNativeSubtitles?: boolean;
}
export interface SettingsResponse {
@ -44,6 +45,7 @@ export interface SettingsResponse {
enableSourceOrder?: boolean;
proxyTmdb?: boolean;
enableLowPerformanceMode?: boolean;
enableNativeSubtitles?: boolean;
}
export function updateSettings(

View file

@ -10,6 +10,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { useProgressBar } from "@/hooks/useProgressBar";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
export function ColorOption(props: {
@ -229,6 +230,7 @@ export function CaptionSettingsView({
const { t } = useTranslation();
const router = useOverlayRouter(id);
const subtitleStore = useSubtitleStore();
const preferencesStore = usePreferencesStore();
const styling = subtitleStore.styling;
const overrideCasing = subtitleStore.overrideCasing;
const delay = subtitleStore.delay;
@ -236,12 +238,17 @@ export function CaptionSettingsView({
const setDelay = subtitleStore.setDelay;
const updateStyling = subtitleStore.updateStyling;
const setCaptionAsTrack = usePlayerStore((s) => s.setCaptionAsTrack);
const captionAsTrack = usePlayerStore((s) => s.caption.asTrack);
const enableNativeSubtitles = preferencesStore.enableNativeSubtitles;
useEffect(() => {
subtitleStore.updateStyling(styling);
}, [styling, subtitleStore]);
// Sync preferences with player store
useEffect(() => {
setCaptionAsTrack(enableNativeSubtitles);
}, [enableNativeSubtitles, setCaptionAsTrack]);
const handleStylingChange = (newStyling: SubtitleStyling) => {
updateStyling(newStyling);
};
@ -267,7 +274,7 @@ export function CaptionSettingsView({
{t("player.menus.subtitles.settings.backlink")}
</Menu.BackLink>
<Menu.Section className="space-y-6 pb-5">
{!captionAsTrack ? (
{!enableNativeSubtitles ? (
<>
<div className="flex justify-between items-center">
<Menu.FieldTitle>
@ -275,8 +282,12 @@ export function CaptionSettingsView({
</Menu.FieldTitle>
<div className="flex justify-center items-center">
<Toggle
enabled={captionAsTrack}
onClick={() => setCaptionAsTrack(!captionAsTrack)}
enabled={enableNativeSubtitles}
onClick={() =>
preferencesStore.setEnableNativeSubtitles(
!enableNativeSubtitles,
)
}
/>
</div>
</div>
@ -478,8 +489,12 @@ export function CaptionSettingsView({
</Menu.FieldTitle>
<div className="flex justify-center items-center">
<Toggle
enabled={captionAsTrack}
onClick={() => setCaptionAsTrack(!captionAsTrack)}
enabled={enableNativeSubtitles}
onClick={() =>
preferencesStore.setEnableNativeSubtitles(
!enableNativeSubtitles,
)
}
/>
</div>
</div>

View file

@ -57,6 +57,9 @@ export function useAuthData() {
const setEnableLowPerformanceMode = usePreferencesStore(
(s) => s.setEnableLowPerformanceMode,
);
const setEnableNativeSubtitles = usePreferencesStore(
(s) => s.setEnableNativeSubtitles,
);
const login = useCallback(
async (
@ -164,6 +167,10 @@ export function useAuthData() {
if (settings.enableLowPerformanceMode !== undefined) {
setEnableLowPerformanceMode(settings.enableLowPerformanceMode);
}
if (settings.enableNativeSubtitles !== undefined) {
setEnableNativeSubtitles(settings.enableNativeSubtitles);
}
},
[
replaceBookmarks,
@ -185,6 +192,7 @@ export function useAuthData() {
setProxyTmdb,
setFebboxKey,
setEnableLowPerformanceMode,
setEnableNativeSubtitles,
],
);

View file

@ -54,6 +54,8 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
enableSourceOrder: store.enableSourceOrder,
proxyTmdb: store.proxyTmdb,
febboxKey: store.febboxKey,
enableLowPerformanceMode: store.enableLowPerformanceMode,
enableNativeSubtitles: store.enableNativeSubtitles,
}));
const backendUrl = useBackendUrl();

View file

@ -16,6 +16,8 @@ import { Menu } from "@/components/player/internals/ContextMenu";
import { CaptionCue } from "@/components/player/Player";
import { Heading1 } from "@/components/utils/Text";
import { Transition } from "@/components/utils/Transition";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
import { SubtitleStyling, useSubtitleStore } from "@/stores/subtitles";
export function CaptionPreview(props: {
@ -86,11 +88,19 @@ export function CaptionsPart(props: {
const [fullscreenPreview, setFullscreenPreview] = useState(false);
const subtitleStore = useSubtitleStore();
const preferencesStore = usePreferencesStore();
const setCaptionAsTrack = usePlayerStore((s) => s.setCaptionAsTrack);
const enableNativeSubtitles = preferencesStore.enableNativeSubtitles;
useEffect(() => {
subtitleStore.updateStyling(props.styling);
}, [props.styling, subtitleStore, subtitleStore.updateStyling]);
// Sync preferences with player store
useEffect(() => {
setCaptionAsTrack(enableNativeSubtitles);
}, [enableNativeSubtitles, setCaptionAsTrack]);
const handleStylingChange = (newStyling: SubtitleStyling) => {
props.setStyling(newStyling);
subtitleStore.updateStyling(newStyling);
@ -114,203 +124,229 @@ export function CaptionsPart(props: {
<Heading1 border>{t("settings.subtitles.title")}</Heading1>
<div className="grid md:grid-cols-[1fr,356px] gap-8">
<div className="space-y-6">
<CaptionSetting
label={t("settings.subtitles.backgroundLabel")}
max={100}
min={0}
onChange={(v) =>
handleStylingChange({
...props.styling,
backgroundOpacity: v / 100,
})
}
value={props.styling.backgroundOpacity * 100}
textTransformer={(s) => `${s}%`}
/>
<CaptionSetting
label={t("settings.subtitles.backgroundBlurLabel")}
max={100}
min={0}
onChange={(v) =>
handleStylingChange({
...props.styling,
backgroundBlur: v / 100,
})
}
value={props.styling.backgroundBlur * 100}
textTransformer={(s) => `${s}%`}
/>
<CaptionSetting
label={t("settings.subtitles.textSizeLabel")}
max={200}
min={1}
textTransformer={(s) => `${s}%`}
onChange={(v) =>
handleStylingChange({
...props.styling,
size: v / 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")}
{t("player.menus.subtitles.useNativeSubtitles")}
</Menu.FieldTitle>
<div className="flex justify-center items-center">
<Toggle
enabled={props.styling.bold}
enabled={enableNativeSubtitles}
onClick={() =>
handleStylingChange({
...props.styling,
bold: !props.styling.bold,
})
preferencesStore.setEnableNativeSubtitles(
!enableNativeSubtitles,
)
}
/>
</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" />
<span className="text-xs text-type-secondary">
{t("player.menus.subtitles.useNativeSubtitlesDescription")}
</span>
{!enableNativeSubtitles && (
<>
<CaptionSetting
label={t("settings.subtitles.backgroundLabel")}
max={100}
min={0}
onChange={(v) =>
handleStylingChange({
...props.styling,
backgroundOpacity: v / 100,
})
}
value={props.styling.backgroundOpacity * 100}
textTransformer={(s) => `${s}%`}
/>
<CaptionSetting
label={t("settings.subtitles.backgroundBlurLabel")}
max={100}
min={0}
onChange={(v) =>
handleStylingChange({
...props.styling,
backgroundBlur: v / 100,
})
}
value={props.styling.backgroundBlur * 100}
textTransformer={(s) => `${s}%`}
/>
<CaptionSetting
label={t("settings.subtitles.textSizeLabel")}
max={200}
min={1}
textTransformer={(s) => `${s}%`}
onChange={(v) =>
handleStylingChange({
...props.styling,
size: v / 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>
</div>
<div className="flex justify-between items-center">
<Menu.FieldTitle>
{t("settings.subtitles.verticalPositionLabel")}
</Menu.FieldTitle>
<div className="flex justify-center items-center space-x-2">
<button
type="button"
className={classNames(
"px-3 py-1 rounded transition-colors duration-100",
props.styling.verticalPosition === 3
? "bg-video-context-buttonFocus"
: "bg-video-context-buttonFocus bg-opacity-0 hover:bg-opacity-50",
)}
onClick={() =>
handleStylingChange({
...props.styling,
verticalPosition: 3,
})
}
<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">
<Menu.FieldTitle>
{t("settings.subtitles.verticalPositionLabel")}
</Menu.FieldTitle>
<div className="flex justify-center items-center space-x-2">
<button
type="button"
className={classNames(
"px-3 py-1 rounded transition-colors duration-100",
props.styling.verticalPosition === 3
? "bg-video-context-buttonFocus"
: "bg-video-context-buttonFocus bg-opacity-0 hover:bg-opacity-50",
)}
onClick={() =>
handleStylingChange({
...props.styling,
verticalPosition: 3,
})
}
>
{t("settings.subtitles.default")}
</button>
<button
type="button"
className={classNames(
"px-3 py-1 rounded transition-colors duration-100",
props.styling.verticalPosition === 1
? "bg-video-context-buttonFocus"
: "bg-video-context-buttonFocus bg-opacity-0 hover:bg-opacity-50",
)}
onClick={() =>
handleStylingChange({
...props.styling,
verticalPosition: 1,
})
}
>
{t("settings.subtitles.low")}
</button>
</div>
</div>
<Button
className="w-full md:w-auto"
theme="secondary"
onClick={resetSubStyling}
>
{t("settings.subtitles.default")}
</button>
<button
type="button"
className={classNames(
"px-3 py-1 rounded transition-colors duration-100",
props.styling.verticalPosition === 1
? "bg-video-context-buttonFocus"
: "bg-video-context-buttonFocus bg-opacity-0 hover:bg-opacity-50",
)}
onClick={() =>
handleStylingChange({
...props.styling,
verticalPosition: 1,
})
}
>
{t("settings.subtitles.low")}
</button>
</div>
</div>
{t("settings.reset")}
</Button>
</>
)}
</div>
<CaptionPreview
show
styling={props.styling}
onToggle={() => setFullscreenPreview((s) => !s)}
/>
<CaptionPreview
show={fullscreenPreview}
fullscreen
styling={props.styling}
onToggle={() => setFullscreenPreview((s) => !s)}
/>
<Button
className="w-full md:w-auto"
theme="secondary"
onClick={resetSubStyling}
>
{t("settings.reset")}
</Button>
{!enableNativeSubtitles && (
<>
<CaptionPreview
show
styling={props.styling}
onToggle={() => setFullscreenPreview((s) => !s)}
/>
<CaptionPreview
show={fullscreenPreview}
fullscreen
styling={props.styling}
onToggle={() => setFullscreenPreview((s) => !s)}
/>
</>
)}
</div>
</div>
);

View file

@ -18,6 +18,7 @@ export interface PreferencesStore {
febboxKey: string | null;
realDebridKey: string | null;
enableLowPerformanceMode: boolean;
enableNativeSubtitles: boolean;
setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void;
@ -34,6 +35,7 @@ export interface PreferencesStore {
setFebboxKey(v: string | null): void;
setRealDebridKey(v: string | null): void;
setEnableLowPerformanceMode(v: boolean): void;
setEnableNativeSubtitles(v: boolean): void;
}
export const usePreferencesStore = create(
@ -54,6 +56,7 @@ export const usePreferencesStore = create(
febboxKey: null,
realDebridKey: null,
enableLowPerformanceMode: false,
enableNativeSubtitles: false,
setEnableThumbnails(v) {
set((s) => {
s.enableThumbnails = v;
@ -129,6 +132,11 @@ export const usePreferencesStore = create(
s.enableLowPerformanceMode = v;
});
},
setEnableNativeSubtitles(v) {
set((s) => {
s.enableNativeSubtitles = v;
});
},
})),
{
name: "__MW::preferences",