Setting for subtitle margin & fix subtitle opacity settings (#338)

Adds a subtitle style option to set the margin below unstyled subtitles.
This is roughly the percent of the screen below the subtitles. It
applies to both ExoPlayer & MPV.

Also fixes font & background opacity not being applied in some cases.

Fixes #225
Closes #178
This commit is contained in:
damontecres 2025-11-28 15:57:11 -05:00 committed by GitHub
parent 7f50cd83ee
commit 296d7b9226
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 130 additions and 49 deletions

View file

@ -152,4 +152,5 @@ fun SubtitlePreferences.Builder.resetSubtitles() {
backgroundColor = SubtitleSettings.BackgroundColor.defaultValue.toArgb() backgroundColor = SubtitleSettings.BackgroundColor.defaultValue.toArgb()
backgroundOpacity = SubtitleSettings.BackgroundOpacity.defaultValue.toInt() backgroundOpacity = SubtitleSettings.BackgroundOpacity.defaultValue.toInt()
backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue
margin = SubtitleSettings.Margin.defaultValue.toInt()
} }

View file

@ -10,7 +10,9 @@ import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
import com.github.damontecres.wholphin.util.Version import com.github.damontecres.wholphin.util.Version
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber import timber.log.Timber
@ -97,7 +99,7 @@ suspend fun upgradeApp(
current: Version, current: Version,
appPreferences: DataStore<AppPreferences>, appPreferences: DataStore<AppPreferences>,
) { ) {
if (previous.isEqualOrBefore(Version.Companion.fromString("0.1.0-2-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.1.0-2-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updatePlaybackOverrides { it.updatePlaybackOverrides {
ac3Supported = AppPreference.Ac3Supported.defaultValue ac3Supported = AppPreference.Ac3Supported.defaultValue
@ -107,21 +109,28 @@ suspend fun upgradeApp(
} }
} }
} }
if (previous.isEqualOrBefore(Version.Companion.fromString("0.2.3-6-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.2.3-6-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updateInterfacePreferences { it.updateInterfacePreferences {
navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue navDrawerSwitchOnFocus = AppPreference.NavDrawerSwitchOnFocus.defaultValue
} }
} }
} }
if (previous.isEqualOrBefore(Version.Companion.fromString("0.2.5-11-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.2.5-11-g0"))) {
appPreferences.updateData { appPreferences.updateData {
it.updateInterfacePreferences { it.updateInterfacePreferences {
showClock = AppPreference.ShowClock.defaultValue showClock = AppPreference.ShowClock.defaultValue
} }
} }
} }
if (previous.isEqualOrBefore(Version.Companion.fromString("0.2.7-1-g0"))) { if (previous.isEqualOrBefore(Version.fromString("0.2.7-1-g0"))) {
PreferencesViewModel.resetSubtitleSettings(appPreferences) PreferencesViewModel.resetSubtitleSettings(appPreferences)
} }
if (previous.isEqualOrBefore(Version.fromString("0.3.2-4-g0"))) {
appPreferences.updateData {
it.updateSubtitlePreferences {
margin = SubtitleSettings.Margin.defaultValue.toInt()
}
}
}
} }

View file

@ -40,6 +40,7 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.Locale
@ -117,6 +118,7 @@ fun PlaybackPage(
LoadingState.Success -> { LoadingState.Success -> {
val prefs = preferences.appPreferences.playbackPreferences val prefs = preferences.appPreferences.playbackPreferences
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
val density = LocalDensity.current val density = LocalDensity.current
val player = viewModel.player val player = viewModel.player
@ -142,11 +144,11 @@ fun PlaybackPage(
val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language) val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language)
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) } var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }
OneTimeLaunchedEffect { OneTimeLaunchedEffect {
if (prefs.playerBackend == PlayerBackend.MPV) { if (prefs.playerBackend == PlayerBackend.MPV) {
scope.launch(Dispatchers.Main + ExceptionHandler()) { scope.launch(Dispatchers.Main + ExceptionHandler()) {
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv( preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
configuration,
density, density,
) )
} }
@ -322,6 +324,7 @@ fun PlaybackPage(
preferences.appPreferences.interfacePreferences.subtitlesPreferences.let { preferences.appPreferences.interfacePreferences.subtitlesPreferences.let {
setStyle(it.toSubtitleStyle()) setStyle(it.toSubtitleStyle())
setFixedTextSize(Dimension.SP, it.fontSize.toFloat()) setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
setBottomPaddingFraction(it.margin.toFloat() / 100f)
} }
} }
}, },

View file

@ -69,6 +69,7 @@ fun PreferencesContent(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: PreferencesViewModel = hiltViewModel(), viewModel: PreferencesViewModel = hiltViewModel(),
updateVM: UpdateViewModel = hiltViewModel(), updateVM: UpdateViewModel = hiltViewModel(),
onFocus: (Int, Int) -> Unit = { _, _ -> },
) { ) {
val context = LocalContext.current val context = LocalContext.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -216,6 +217,7 @@ fun PreferencesContent(
if (focused) { if (focused) {
focusedIndex = Pair(groupIndex, prefIndex) focusedIndex = Pair(groupIndex, prefIndex)
if (movementSounds) playOnClickSound(context) if (movementSounds) playOnClickSound(context)
onFocus.invoke(groupIndex, prefIndex)
} }
} }
when (pref) { when (pref) {

View file

@ -1,10 +1,12 @@
package com.github.damontecres.wholphin.ui.preferences.subtitle package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.content.res.Configuration
import android.graphics.Typeface import android.graphics.Typeface
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.CaptionStyleCompat import androidx.media3.ui.CaptionStyleCompat
@ -21,6 +23,7 @@ import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
import com.github.damontecres.wholphin.util.mpv.MPVLib import com.github.damontecres.wholphin.util.mpv.MPVLib
import com.github.damontecres.wholphin.util.mpv.setPropertyColor import com.github.damontecres.wholphin.util.mpv.setPropertyColor
import timber.log.Timber
object SubtitleSettings { object SubtitleSettings {
val FontSize = val FontSize =
@ -183,6 +186,23 @@ object SubtitleSettings {
valueToIndex = { it.number }, valueToIndex = { it.number },
) )
val Margin =
AppSliderPreference(
title = R.string.subtitle_margin,
defaultValue = 8,
min = 0,
max = 100,
interval = 1,
getter = {
it.interfacePreferences.subtitlesPreferences.margin
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { margin = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val Reset = val Reset =
AppClickablePreference( AppClickablePreference(
title = R.string.reset, title = R.string.reset,
@ -223,16 +243,23 @@ object SubtitleSettings {
PreferenceGroup( PreferenceGroup(
title = R.string.more, title = R.string.more,
preferences = preferences =
listOf(Reset), listOf(
Margin,
Reset,
),
), ),
) )
private fun combine(
color: Int,
opacity: Int,
) = ((opacity / 100.0 * 255).toInt().shl(24)).or(color.and(0x00FFFFFF))
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
fun SubtitlePreferences.toSubtitleStyle(): CaptionStyleCompat { fun SubtitlePreferences.toSubtitleStyle(): CaptionStyleCompat {
val fo = (fontOpacity / 100.0 * 255).toInt().shl(24) val bg = combine(backgroundColor, backgroundOpacity)
val bg = (backgroundOpacity / 100.0 * 255).toInt().shl(24).or(backgroundColor)
return CaptionStyleCompat( return CaptionStyleCompat(
fo.or(fontColor), combine(fontColor, fontOpacity),
if (backgroundStyle == BackgroundStyle.BG_WRAP)bg else 0, if (backgroundStyle == BackgroundStyle.BG_WRAP)bg else 0,
if (backgroundStyle == BackgroundStyle.BG_BOXED) bg else 0, if (backgroundStyle == BackgroundStyle.BG_BOXED) bg else 0,
when (edgeStyle) { when (edgeStyle) {
@ -240,7 +267,7 @@ object SubtitleSettings {
EdgeStyle.EDGE_SOLID -> CaptionStyleCompat.EDGE_TYPE_OUTLINE EdgeStyle.EDGE_SOLID -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
EdgeStyle.EDGE_SHADOW -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW EdgeStyle.EDGE_SHADOW -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
}, },
fo.or(edgeColor), combine(edgeColor, fontOpacity),
when { when {
fontBold && fontItalic -> Typeface.defaultFromStyle(Typeface.BOLD_ITALIC) fontBold && fontItalic -> Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)
fontBold -> Typeface.defaultFromStyle(Typeface.BOLD) fontBold -> Typeface.defaultFromStyle(Typeface.BOLD)
@ -250,11 +277,14 @@ object SubtitleSettings {
) )
} }
fun SubtitlePreferences.applyToMpv(density: Density) { fun SubtitlePreferences.applyToMpv(
configuration: Configuration,
density: Density,
) {
val fo = (fontOpacity / 100.0 * 255).toInt().shl(24) val fo = (fontOpacity / 100.0 * 255).toInt().shl(24)
val fc = Color(fo.or(fontColor)) val fc = Color(combine(fontColor, fontOpacity))
val bg = Color((backgroundOpacity / 100.0 * 255).toInt().shl(24).or(backgroundColor)) val bg = Color(combine(backgroundColor, backgroundOpacity))
val edge = Color(fo.or(edgeColor)) val edge = Color(combine(edgeColor, fontOpacity))
// TODO weird, but seems to get the size to be very close to matching sizes between renderers // TODO weird, but seems to get the size to be very close to matching sizes between renderers
val fontSizePx = with(density) { fontSize.sp.toPx() * .8 }.toInt() val fontSizePx = with(density) { fontSize.sp.toPx() * .8 }.toInt()
@ -262,6 +292,11 @@ object SubtitleSettings {
MPVLib.setPropertyColor("sub-color", fc) MPVLib.setPropertyColor("sub-color", fc)
MPVLib.setPropertyColor("sub-outline-color", edge) MPVLib.setPropertyColor("sub-outline-color", edge)
val heightInPx = with(density) { configuration.screenHeightDp.dp.toPx() }
val margin = (heightInPx * (margin.toFloat() / 100f) * .8).toInt()
MPVLib.setPropertyInt("sub-margin-y", margin)
Timber.d("MPV subtitles: fontSizePx=%s, margin=$margin", fontSizePx, margin)
when (edgeStyle) { when (edgeStyle) {
EdgeStyle.EDGE_NONE, EdgeStyle.EDGE_NONE,
EdgeStyle.UNRECOGNIZED, EdgeStyle.UNRECOGNIZED,

View file

@ -33,7 +33,6 @@ import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
import com.github.damontecres.wholphin.ui.preferences.PreferencesContent import com.github.damontecres.wholphin.ui.preferences.PreferencesContent
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import timber.log.Timber
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
@ -48,8 +47,9 @@ fun SubtitleStylePage(
preferences = it preferences = it
} }
} }
Timber.v("SubtitleStylePage")
val prefs = preferences.interfacePreferences.subtitlesPreferences val prefs = preferences.interfacePreferences.subtitlesPreferences
var focusedOnMargin by remember { mutableStateOf(false) }
Row( Row(
modifier = modifier, modifier = modifier,
) { ) {
@ -68,6 +68,7 @@ fun SubtitleStylePage(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
) )
if (!focusedOnMargin) {
Column( Column(
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
modifier = modifier =
@ -103,10 +104,38 @@ fun SubtitleStylePage(
) )
} }
} }
} else {
// Margin
AndroidView(
factory = { context ->
SubtitleView(context)
},
update = {
it.setStyle(prefs.toSubtitleStyle())
it.setFixedTextSize(Dimension.SP, prefs.fontSize.toFloat())
it.setCues(
listOf(
Cue.Builder().setText("Subtitles margin below here").build(),
),
)
it.setBottomPaddingFraction(prefs.margin.toFloat() / 100f)
},
modifier =
Modifier
.fillMaxSize(),
)
}
} }
PreferencesContent( PreferencesContent(
initialPreferences = preferences, initialPreferences = preferences,
preferenceScreenOption = PreferenceScreenOption.SUBTITLES, preferenceScreenOption = PreferenceScreenOption.SUBTITLES,
onFocus = { groupIndex, prefIndex ->
focusedOnMargin =
SubtitleSettings.preferences.getOrNull(groupIndex)?.preferences?.getOrNull(
prefIndex,
) == SubtitleSettings.Margin
},
modifier = modifier =
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()

View file

@ -119,6 +119,7 @@ message SubtitlePreferences{
BackgroundStyle background_style = 8; BackgroundStyle background_style = 8;
bool font_bold = 9; bool font_bold = 9;
bool font_italic = 10; bool font_italic = 10;
int32 margin = 11;
} }
message InterfacePreferences { message InterfacePreferences {

View file

@ -326,6 +326,7 @@
<string name="remove">Remove</string> <string name="remove">Remove</string>
<string name="dolby_vision">Dolby Vision</string> <string name="dolby_vision">Dolby Vision</string>
<string name="dolby_atmos">Dolby Atmos</string> <string name="dolby_atmos">Dolby Atmos</string>
<string name="subtitle_margin">Margin</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>