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()
backgroundOpacity = SubtitleSettings.BackgroundOpacity.defaultValue.toInt()
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.updateInterfacePreferences
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.subtitle.SubtitleSettings
import com.github.damontecres.wholphin.util.Version
import dagger.hilt.android.qualifiers.ApplicationContext
import timber.log.Timber
@ -97,7 +99,7 @@ suspend fun upgradeApp(
current: Version,
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 {
it.updatePlaybackOverrides {
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 {
it.updateInterfacePreferences {
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 {
it.updateInterfacePreferences {
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)
}
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.RectangleShape
import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.intl.Locale
@ -117,6 +118,7 @@ fun PlaybackPage(
LoadingState.Success -> {
val prefs = preferences.appPreferences.playbackPreferences
val scope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
val density = LocalDensity.current
val player = viewModel.player
@ -142,11 +144,11 @@ fun PlaybackPage(
val subtitleSearchLanguage by viewModel.subtitleSearchLanguage.observeAsState(Locale.current.language)
var playbackDialog by remember { mutableStateOf<PlaybackDialogType?>(null) }
OneTimeLaunchedEffect {
if (prefs.playerBackend == PlayerBackend.MPV) {
scope.launch(Dispatchers.Main + ExceptionHandler()) {
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
configuration,
density,
)
}
@ -322,6 +324,7 @@ fun PlaybackPage(
preferences.appPreferences.interfacePreferences.subtitlesPreferences.let {
setStyle(it.toSubtitleStyle())
setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
setBottomPaddingFraction(it.margin.toFloat() / 100f)
}
}
},

View file

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

View file

@ -1,10 +1,12 @@
package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.content.res.Configuration
import android.graphics.Typeface
import androidx.annotation.OptIn
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.util.UnstableApi
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.util.mpv.MPVLib
import com.github.damontecres.wholphin.util.mpv.setPropertyColor
import timber.log.Timber
object SubtitleSettings {
val FontSize =
@ -183,6 +186,23 @@ object SubtitleSettings {
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 =
AppClickablePreference(
title = R.string.reset,
@ -223,16 +243,23 @@ object SubtitleSettings {
PreferenceGroup(
title = R.string.more,
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)
fun SubtitlePreferences.toSubtitleStyle(): CaptionStyleCompat {
val fo = (fontOpacity / 100.0 * 255).toInt().shl(24)
val bg = (backgroundOpacity / 100.0 * 255).toInt().shl(24).or(backgroundColor)
val bg = combine(backgroundColor, backgroundOpacity)
return CaptionStyleCompat(
fo.or(fontColor),
combine(fontColor, fontOpacity),
if (backgroundStyle == BackgroundStyle.BG_WRAP)bg else 0,
if (backgroundStyle == BackgroundStyle.BG_BOXED) bg else 0,
when (edgeStyle) {
@ -240,7 +267,7 @@ object SubtitleSettings {
EdgeStyle.EDGE_SOLID -> CaptionStyleCompat.EDGE_TYPE_OUTLINE
EdgeStyle.EDGE_SHADOW -> CaptionStyleCompat.EDGE_TYPE_DROP_SHADOW
},
fo.or(edgeColor),
combine(edgeColor, fontOpacity),
when {
fontBold && fontItalic -> Typeface.defaultFromStyle(Typeface.BOLD_ITALIC)
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 fc = Color(fo.or(fontColor))
val bg = Color((backgroundOpacity / 100.0 * 255).toInt().shl(24).or(backgroundColor))
val edge = Color(fo.or(edgeColor))
val fc = Color(combine(fontColor, fontOpacity))
val bg = Color(combine(backgroundColor, backgroundOpacity))
val edge = Color(combine(edgeColor, fontOpacity))
// 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()
@ -262,6 +292,11 @@ object SubtitleSettings {
MPVLib.setPropertyColor("sub-color", fc)
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) {
EdgeStyle.EDGE_NONE,
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.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import timber.log.Timber
@OptIn(UnstableApi::class)
@Composable
@ -48,8 +47,9 @@ fun SubtitleStylePage(
preferences = it
}
}
Timber.v("SubtitleStylePage")
val prefs = preferences.interfacePreferences.subtitlesPreferences
var focusedOnMargin by remember { mutableStateOf(false) }
Row(
modifier = modifier,
) {
@ -68,45 +68,74 @@ fun SubtitleStylePage(
Modifier
.fillMaxSize(),
)
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier
.padding(16.dp)
.fillMaxSize(),
) {
val examples =
mapOf(
"Subtitles will look like this" to 48.dp,
"This is another example" to 24.dp,
"Longer multi line subtitles will\nlook like this" to 0.dp,
)
examples.forEach { (text, padding) ->
AndroidView(
factory = { context ->
SubtitleView(context)
},
update = {
it.setStyle(prefs.toSubtitleStyle())
it.setFixedTextSize(Dimension.SP, prefs.fontSize.toFloat())
it.setCues(
listOf(
Cue.Builder().setText(text).build(),
),
)
},
modifier =
Modifier
.weight(1f)
.fillMaxWidth()
.padding(bottom = padding),
)
if (!focusedOnMargin) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier =
Modifier
.padding(16.dp)
.fillMaxSize(),
) {
val examples =
mapOf(
"Subtitles will look like this" to 48.dp,
"This is another example" to 24.dp,
"Longer multi line subtitles will\nlook like this" to 0.dp,
)
examples.forEach { (text, padding) ->
AndroidView(
factory = { context ->
SubtitleView(context)
},
update = {
it.setStyle(prefs.toSubtitleStyle())
it.setFixedTextSize(Dimension.SP, prefs.fontSize.toFloat())
it.setCues(
listOf(
Cue.Builder().setText(text).build(),
),
)
},
modifier =
Modifier
.weight(1f)
.fillMaxWidth()
.padding(bottom = padding),
)
}
}
} 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(
initialPreferences = preferences,
preferenceScreenOption = PreferenceScreenOption.SUBTITLES,
onFocus = { groupIndex, prefIndex ->
focusedOnMargin =
SubtitleSettings.preferences.getOrNull(groupIndex)?.preferences?.getOrNull(
prefIndex,
) == SubtitleSettings.Margin
},
modifier =
Modifier
.fillMaxHeight()

View file

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

View file

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