From 621d1644027e9c3ef8aeca69ec8699d0ad35efe4 Mon Sep 17 00:00:00 2001 From: Lance Chant <13349722+lancechant@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:35:00 +0200 Subject: [PATCH] feat: added more subtitle customization options Subtitles can now be customized with the following extra options: - Colour - background opacity/colour - outline opacity/colour - boldness Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com> --- app/(auth)/player/direct-player.tsx | 76 ++++++- components/settings/SubtitleToggles.tsx | 207 ++++++++++++++++++++ components/settings/VLCSubtitleSettings.tsx | 156 +++++++++++++++ translations/en.json | 33 +++- 4 files changed, 470 insertions(+), 2 deletions(-) create mode 100644 components/settings/VLCSubtitleSettings.tsx diff --git a/app/(auth)/player/direct-player.tsx b/app/(auth)/player/direct-player.tsx index ca906a78..10135002 100644 --- a/app/(auth)/player/direct-player.tsx +++ b/app/(auth)/player/direct-player.tsx @@ -37,9 +37,29 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useSettings } from "@/utils/atoms/settings"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { writeToLog } from "@/utils/log"; +import { storage } from "@/utils/mmkv"; import { generateDeviceProfile } from "@/utils/profiles/native"; import { msToTicks, ticksToSeconds } from "@/utils/time"; +type VLCColor = + | "Black" + | "Gray" + | "Silver" + | "White" + | "Maroon" + | "Red" + | "Fuchsia" + | "Yellow" + | "Olive" + | "Green" + | "Teal" + | "Lime" + | "Purple" + | "Navy" + | "Blue" + | "Aqua"; +type OutlineThickness = "None" | "Thin" | "Normal" | "Thick"; + export default function page() { const videoRef = useRef(null); const user = useAtomValue(userAtom); @@ -576,8 +596,62 @@ export default function page() { ? allSubs.indexOf(chosenSubtitleTrack) : [...textSubs].reverse().indexOf(chosenSubtitleTrack); initOptions.push(`--sub-track=${finalIndex}`); - } + // Add VLC subtitle styling options from settings + const textColor = (storage.getString("vlc.textColor") || + "White") as VLCColor; + const backgroundColor = (storage.getString("vlc.backgroundColor") || + "Black") as VLCColor; + const outlineColor = (storage.getString("vlc.outlineColor") || + "Black") as VLCColor; + const outlineThickness = (storage.getString("vlc.outlineThickness") || + "Normal") as OutlineThickness; + const backgroundOpacity = storage.getNumber("vlc.backgroundOpacity") || 128; + const outlineOpacity = storage.getNumber("vlc.outlineOpacity") || 255; + const isBold = storage.getBoolean("vlc.isBold") || false; + + // VLC color values mapping + const VLC_COLORS: Record = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, + }; + + const OUTLINE_THICKNESS: Record = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, + }; + + // Add subtitle styling options + initOptions.push(`--freetype-color=${VLC_COLORS[textColor]}`); + initOptions.push(`--freetype-background-opacity=${backgroundOpacity}`); + initOptions.push( + `--freetype-background-color=${VLC_COLORS[backgroundColor]}`, + ); + initOptions.push(`--freetype-outline-opacity=${outlineOpacity}`); + initOptions.push(`--freetype-outline-color=${VLC_COLORS[outlineColor]}`); + initOptions.push( + `--freetype-outline-thickness=${OUTLINE_THICKNESS[outlineThickness]}`, + ); + if (isBold) { + initOptions.push("--freetype-bold"); + } + } if (notTranscoding && chosenAudioTrack) { initOptions.push(`--audio-track=${allAudio.indexOf(chosenAudioTrack)}`); } diff --git a/components/settings/SubtitleToggles.tsx b/components/settings/SubtitleToggles.tsx index 59ec1570..e196a599 100644 --- a/components/settings/SubtitleToggles.tsx +++ b/components/settings/SubtitleToggles.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { Platform, TouchableOpacity, View, type ViewProps } from "react-native"; const _DropdownMenu = !Platform.isTV ? require("zeego/dropdown-menu") : null; @@ -9,6 +10,7 @@ import { Switch } from "react-native-gesture-handler"; import Dropdown from "@/components/common/Dropdown"; import { Stepper } from "@/components/inputs/Stepper"; import { useSettings } from "@/utils/atoms/settings"; +import { storage } from "@/utils/mmkv"; import { Text } from "../common/Text"; import { ListGroup } from "../list/ListGroup"; import { ListItem } from "../list/ListItem"; @@ -16,6 +18,32 @@ import { useMedia } from "./MediaContext"; interface Props extends ViewProps {} +const VLC_COLORS = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, +}; + +const OUTLINE_THICKNESS = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, +}; + export const SubtitleToggles: React.FC = ({ ...props }) => { const isTv = Platform.isTV; @@ -25,6 +53,52 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { const cultures = media.cultures; const { t } = useTranslation(); + // VLC subtitle styling states + const [textColor, setTextColor] = useState( + storage.getString("vlc.textColor") || "White", + ); + const [backgroundColor, setBackgroundColor] = useState( + storage.getString("vlc.backgroundColor") || "Black", + ); + const [outlineColor, setOutlineColor] = useState( + storage.getString("vlc.outlineColor") || "Black", + ); + const [outlineThickness, setOutlineThickness] = useState( + storage.getString("vlc.outlineThickness") || "Normal", + ); + const [backgroundOpacity, setBackgroundOpacity] = useState( + storage.getNumber("vlc.backgroundOpacity") || 128, + ); + const [outlineOpacity, setOutlineOpacity] = useState( + storage.getNumber("vlc.outlineOpacity") || 255, + ); + const [isBold, setIsBold] = useState( + storage.getBoolean("vlc.isBold") || false, + ); + + // VLC settings effects + useEffect(() => { + storage.set("vlc.textColor", textColor); + }, [textColor]); + useEffect(() => { + storage.set("vlc.backgroundColor", backgroundColor); + }, [backgroundColor]); + useEffect(() => { + storage.set("vlc.outlineColor", outlineColor); + }, [outlineColor]); + useEffect(() => { + storage.set("vlc.outlineThickness", outlineThickness); + }, [outlineThickness]); + useEffect(() => { + storage.set("vlc.backgroundOpacity", backgroundOpacity); + }, [backgroundOpacity]); + useEffect(() => { + storage.set("vlc.outlineOpacity", outlineOpacity); + }, [outlineOpacity]); + useEffect(() => { + storage.set("vlc.isBold", isBold); + }, [isBold]); + if (isTv) return null; if (!settings) return null; @@ -147,6 +221,139 @@ export const SubtitleToggles: React.FC = ({ ...props }) => { onUpdate={(subtitleSize) => updateSettings({ subtitleSize })} /> + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.colors.${item}`) + } + title={ + + + {t(`home.settings.subtitles.colors.${textColor}`)} + + + + } + label={t("home.settings.subtitles.text_color")} + onSelected={setTextColor} + /> + + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.colors.${item}`) + } + title={ + + + {t(`home.settings.subtitles.colors.${backgroundColor}`)} + + + + } + label={t("home.settings.subtitles.background_color")} + onSelected={setBackgroundColor} + /> + + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.colors.${item}`) + } + title={ + + + {t(`home.settings.subtitles.colors.${outlineColor}`)} + + + + } + label={t("home.settings.subtitles.outline_color")} + onSelected={setOutlineColor} + /> + + + item} + titleExtractor={(item) => + t(`home.settings.subtitles.thickness.${item}`) + } + title={ + + + {t(`home.settings.subtitles.thickness.${outlineThickness}`)} + + + + } + label={t("home.settings.subtitles.outline_thickness")} + onSelected={setOutlineThickness} + /> + + + `${Math.round((item / 255) * 100)}%`} + title={ + + {`${Math.round((backgroundOpacity / 255) * 100)}%`} + + + } + label={t("home.settings.subtitles.background_opacity")} + onSelected={setBackgroundOpacity} + /> + + + `${Math.round((item / 255) * 100)}%`} + title={ + + {`${Math.round((outlineOpacity / 255) * 100)}%`} + + + } + label={t("home.settings.subtitles.outline_opacity")} + onSelected={setOutlineOpacity} + /> + + + + ); diff --git a/components/settings/VLCSubtitleSettings.tsx b/components/settings/VLCSubtitleSettings.tsx new file mode 100644 index 00000000..919ee14e --- /dev/null +++ b/components/settings/VLCSubtitleSettings.tsx @@ -0,0 +1,156 @@ +import { t } from "i18next"; +import { useEffect, useState } from "react"; +import { View } from "react-native"; +import { ListGroup } from "@/components/list/ListGroup"; +import { ListItem } from "@/components/list/ListItem"; +import { storage } from "@/utils/mmkv"; + +const VLC_COLORS = { + Black: 0, + Gray: 8421504, + Silver: 12632256, + White: 16777215, + Maroon: 8388608, + Red: 16711680, + Fuchsia: 16711935, + Yellow: 16776960, + Olive: 8421376, + Green: 32768, + Teal: 32896, + Lime: 65280, + Purple: 8388736, + Navy: 128, + Blue: 255, + Aqua: 65535, +}; + +const OUTLINE_THICKNESS = { + None: 0, + Thin: 2, + Normal: 4, + Thick: 6, +}; + +export function VLCSubtitleSettings({ + className = "", +}: { + className?: string; +}) { + const [textColor, setTextColor] = useState( + storage.getString("vlc.textColor") || "White", + ); + const [backgroundColor, setBackgroundColor] = useState( + storage.getString("vlc.backgroundColor") || "Black", + ); + const [outlineColor, setOutlineColor] = useState( + storage.getString("vlc.outlineColor") || "Black", + ); + const [outlineThickness, setOutlineThickness] = useState( + storage.getString("vlc.outlineThickness") || "Normal", + ); + const [backgroundOpacity, setBackgroundOpacity] = useState( + storage.getNumber("vlc.backgroundOpacity") || 128, + ); + const [outlineOpacity, setOutlineOpacity] = useState( + storage.getNumber("vlc.outlineOpacity") || 255, + ); + const [isBold, setIsBold] = useState( + storage.getBoolean("vlc.isBold") || false, + ); + + useEffect(() => { + storage.set("vlc.textColor", textColor); + }, [textColor]); + + useEffect(() => { + storage.set("vlc.backgroundColor", backgroundColor); + }, [backgroundColor]); + + useEffect(() => { + storage.set("vlc.outlineColor", outlineColor); + }, [outlineColor]); + + useEffect(() => { + storage.set("vlc.outlineThickness", outlineThickness); + }, [outlineThickness]); + + useEffect(() => { + storage.set("vlc.backgroundOpacity", backgroundOpacity); + }, [backgroundOpacity]); + + useEffect(() => { + storage.set("vlc.outlineOpacity", outlineOpacity); + }, [outlineOpacity]); + + useEffect(() => { + storage.set("vlc.isBold", isBold); + }, [isBold]); + + return ( + + + { + const colors = Object.keys(VLC_COLORS); + const currentIndex = colors.indexOf(textColor); + const nextIndex = (currentIndex + 1) % colors.length; + setTextColor(colors[nextIndex]); + }} + /> + { + const colors = Object.keys(VLC_COLORS); + const currentIndex = colors.indexOf(backgroundColor); + const nextIndex = (currentIndex + 1) % colors.length; + setBackgroundColor(colors[nextIndex]); + }} + /> + { + const colors = Object.keys(VLC_COLORS); + const currentIndex = colors.indexOf(outlineColor); + const nextIndex = (currentIndex + 1) % colors.length; + setOutlineColor(colors[nextIndex]); + }} + /> + { + const thicknesses = Object.keys(OUTLINE_THICKNESS); + const currentIndex = thicknesses.indexOf(outlineThickness); + const nextIndex = (currentIndex + 1) % thicknesses.length; + setOutlineThickness(thicknesses[nextIndex]); + }} + /> + { + const newOpacity = (backgroundOpacity + 32) % 256; + setBackgroundOpacity(newOpacity); + }} + /> + { + const newOpacity = (outlineOpacity + 32) % 256; + setOutlineOpacity(newOpacity); + }} + /> + setIsBold(!isBold)} + /> + + + ); +} diff --git a/translations/en.json b/translations/en.json index b1ccbb80..af6caf18 100644 --- a/translations/en.json +++ b/translations/en.json @@ -106,11 +106,11 @@ }, "subtitles": { "subtitle_title": "Subtitles", + "subtitle_hint": "Configure how subtitles look and behave.", "subtitle_language": "Subtitle language", "subtitle_mode": "Subtitle Mode", "set_subtitle_track": "Set Subtitle Track From Previous Item", "subtitle_size": "Subtitle Size", - "subtitle_hint": "Configure subtitle preference.", "none": "None", "language": "Language", "loading": "Loading", @@ -120,6 +120,37 @@ "Always": "Always", "None": "None", "OnlyForced": "OnlyForced" + }, + "text_color": "Text Color", + "background_color": "Background Color", + "outline_color": "Outline Color", + "outline_thickness": "Outline Thickness", + "background_opacity": "Background Opacity", + "outline_opacity": "Outline Opacity", + "bold_text": "Bold Text", + "colors": { + "Black": "Black", + "Gray": "Gray", + "Silver": "Silver", + "White": "White", + "Maroon": "Maroon", + "Red": "Red", + "Fuchsia": "Fuchsia", + "Yellow": "Yellow", + "Olive": "Olive", + "Green": "Green", + "Teal": "Teal", + "Lime": "Lime", + "Purple": "Purple", + "Navy": "Navy", + "Blue": "Blue", + "Aqua": "Aqua" + }, + "thickness": { + "None": "None", + "Thin": "Thin", + "Normal": "Normal", + "Thick": "Thick" } }, "other": {