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; enableSourceOrder?: boolean;
proxyTmdb?: boolean; proxyTmdb?: boolean;
enableLowPerformanceMode?: boolean; enableLowPerformanceMode?: boolean;
enableNativeSubtitles?: boolean;
} }
export interface SettingsResponse { export interface SettingsResponse {
@ -44,6 +45,7 @@ export interface SettingsResponse {
enableSourceOrder?: boolean; enableSourceOrder?: boolean;
proxyTmdb?: boolean; proxyTmdb?: boolean;
enableLowPerformanceMode?: boolean; enableLowPerformanceMode?: boolean;
enableNativeSubtitles?: boolean;
} }
export function updateSettings( export function updateSettings(

View file

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

View file

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

View file

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

View file

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

View file

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