Set a different style for HDR subtitles & adjust image subtitles opacity (#827)

## Description
Can now configure a different subtitle style for HDR playback.

There is also a new setting to change the opacity/dim image based
subtitles. This is primarily for HDR to dim very bright PGS subtitles,
but the setting is available for SDR too.

Please remember that the styles only apply to unstyled text formats like
SRT.

I haven't been able to consistently get the preview to switch the
display into HDR mode, so the preview is not 100% accurate.

### Related issues
Closes #583

### Testing
Shield pro 2019 w/ LG C2

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-04 18:22:11 -05:00 committed by GitHub
parent b0b8ae3639
commit ee440698b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 475 additions and 86 deletions

View file

@ -21,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp
import androidx.datastore.core.DataStore
@ -57,6 +58,7 @@ import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.services.tvprovider.TvProviderSchedulerService
import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
@ -70,6 +72,7 @@ import com.github.damontecres.wholphin.util.ExceptionHandler
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import okhttp3.OkHttpClient
@ -133,7 +136,9 @@ class MainActivity : AppCompatActivity() {
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver)
if (savedInstanceState == null) {
appUpgradeHandler.copySubfont(false)
lifecycleScope.launchIO {
appUpgradeHandler.copySubfont(false)
}
}
viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) {
@ -148,6 +153,25 @@ class MainActivity : AppCompatActivity() {
viewModel.appStart()
setContent {
val appPreferences by userPreferencesDataStore.data.collectAsState(null)
if (appPreferences == null) {
// Show loading page if it is taking a while to get app preferences
var showLoading by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(500)
Timber.i("Showing loading page")
showLoading = true
}
if (showLoading) {
Box(
modifier =
Modifier
.fillMaxSize()
.background(Color.Black),
) {
LoadingPage()
}
}
}
appPreferences?.let { appPreferences ->
LaunchedEffect(appPreferences.signInAutomatically) {
signInAuto = appPreferences.signInAutomatically

View file

@ -710,7 +710,7 @@ sealed interface AppPreference<Pref, T> {
val SubtitleStyle =
AppDestinationPreference<AppPreferences>(
title = R.string.subtitle_style,
destination = Destination.Settings(PreferenceScreenOption.SUBTITLES),
destination = Destination.SubtitleSettings(false),
)
val RefreshRateSwitching =
@ -969,8 +969,6 @@ val basicPreferences =
),
)
val uiPreferences = listOf<PreferenceGroup>()
private val ExoPlayerSettings =
listOf(
AppPreference.FfmpegPreference,

View file

@ -100,6 +100,12 @@ class AppPreferencesSerializer
.apply {
resetSubtitles()
}.build()
hdrSubtitlesPreferences =
SubtitlePreferences
.newBuilder()
.apply {
resetSubtitles()
}.build()
liveTvPreferences =
LiveTvPreferences
@ -193,4 +199,5 @@ fun SubtitlePreferences.Builder.resetSubtitles() {
backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue
margin = SubtitleSettings.Margin.defaultValue.toInt()
edgeThickness = SubtitleSettings.EdgeThickness.defaultValue.toInt()
imageSubtitleOpacity = SubtitleSettings.ImageOpacity.defaultValue.toInt()
}

View file

@ -213,4 +213,19 @@ suspend fun upgradeApp(
}
}
}
if (previous.isEqualOrBefore(Version.fromString("0.4.1-6-g0"))) {
appPreferences.updateData {
it.updateInterfacePreferences {
subtitlesPreferences =
subtitlesPreferences
.toBuilder()
.apply {
imageSubtitleOpacity = SubtitleSettings.ImageOpacity.defaultValue.toInt()
}.build()
// Copy current subtitle prefs as HDR ones
hdrSubtitlesPreferences = subtitlesPreferences.toBuilder().build()
}
}
}
}

View file

@ -29,7 +29,7 @@ class RefreshRateService
@param:ApplicationContext private val context: Context,
) {
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
private val display get() = displayManager.getDisplay(Display.DEFAULT_DISPLAY)
val supportedDisplayModes get() = display.supportedModes.orEmpty()

View file

@ -38,6 +38,11 @@ sealed class Destination(
val screen: PreferenceScreenOption,
) : Destination(true)
@Serializable
data class SubtitleSettings(
val hdr: Boolean,
) : Destination(true)
@Serializable
data object Search : Destination()

View file

@ -34,6 +34,7 @@ import com.github.damontecres.wholphin.ui.main.HomePage
import com.github.damontecres.wholphin.ui.main.SearchPage
import com.github.damontecres.wholphin.ui.playback.PlaybackPage
import com.github.damontecres.wholphin.ui.preferences.PreferencesPage
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
import com.github.damontecres.wholphin.ui.setup.InstallUpdatePage
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
@ -78,6 +79,14 @@ fun DestinationContent(
)
}
is Destination.SubtitleSettings -> {
SubtitleStylePage(
preferences.appPreferences,
destination.hdr,
modifier,
)
}
is Destination.SeriesOverview -> {
SeriesOverview(
preferences = preferences,

View file

@ -34,6 +34,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
@ -70,6 +71,7 @@ import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TextButton
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize
@ -85,6 +87,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
import java.util.UUID
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
@ -176,6 +179,7 @@ fun PlaybackPageContent(
LaunchedEffect(player) {
if (playerBackend == PlayerBackend.MPV) {
scope.launch(Dispatchers.IO + ExceptionHandler()) {
// MPV can't play HDR, so always use regular settings
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
configuration,
density,
@ -401,13 +405,26 @@ fun PlaybackPageContent(
)
}
val subtitleSettings =
remember(mediaInfo) {
Timber.v("subtitle choice: ${mediaInfo?.videoStream?.hdr}")
if (mediaInfo?.videoStream?.hdr == true) {
preferences.appPreferences.interfacePreferences.hdrSubtitlesPreferences
} else {
preferences.appPreferences.interfacePreferences.subtitlesPreferences
}
}
val subtitleImageOpacity =
remember(subtitleSettings) { subtitleSettings.imageSubtitleOpacity / 100f }
// Subtitles
if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled) {
val maxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f)
val isImageSubtitles = remember(cues) { cues.firstOrNull()?.bitmap != null }
AndroidView(
factory = { context ->
SubtitleView(context).apply {
preferences.appPreferences.interfacePreferences.subtitlesPreferences.let {
subtitleSettings.let {
setStyle(it.toSubtitleStyle())
setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
setBottomPaddingFraction(it.margin.toFloat() / 100f)
@ -416,10 +433,8 @@ fun PlaybackPageContent(
},
update = {
it.setCues(cues)
Media3SubtitleOverride(
preferences.appPreferences.interfacePreferences.subtitlesPreferences
.calculateEdgeSize(density),
).apply(it)
Media3SubtitleOverride(subtitleSettings.calculateEdgeSize(density))
.apply(it)
},
onReset = {
it.setCues(null)
@ -428,7 +443,8 @@ fun PlaybackPageContent(
Modifier
.fillMaxSize(maxSize)
.align(Alignment.TopCenter)
.background(Color.Transparent),
.background(Color.Transparent)
.ifElse(isImageSubtitles, Modifier.alpha(subtitleImageOpacity)),
)
}
}

View file

@ -2,21 +2,20 @@ package com.github.damontecres.wholphin.ui.preferences
import androidx.annotation.StringRes
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import kotlinx.serialization.Serializable
/**
* A group of preferences
*/
data class PreferenceGroup(
data class PreferenceGroup<T>(
@param:StringRes val title: Int,
val preferences: List<AppPreference<AppPreferences, out Any?>>,
val conditionalPreferences: List<ConditionalPreferences> = listOf(),
val preferences: List<AppPreference<T, out Any?>>,
val conditionalPreferences: List<ConditionalPreferences<T>> = listOf(),
)
data class ConditionalPreferences(
val condition: (AppPreferences) -> Boolean,
val preferences: List<AppPreference<AppPreferences, out Any?>>,
data class ConditionalPreferences<T>(
val condition: (T) -> Boolean,
val preferences: List<AppPreference<T, out Any?>>,
)
/**
@ -34,8 +33,6 @@ sealed interface PreferenceValidation {
enum class PreferenceScreenOption {
BASIC,
ADVANCED,
USER_INTERFACE,
SUBTITLES,
EXO_PLAYER,
MPV,
;

View file

@ -50,7 +50,6 @@ import com.github.damontecres.wholphin.preferences.MpvPreferences
import com.github.damontecres.wholphin.preferences.PlayerBackend
import com.github.damontecres.wholphin.preferences.advancedPreferences
import com.github.damontecres.wholphin.preferences.basicPreferences
import com.github.damontecres.wholphin.preferences.uiPreferences
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.ui.components.ConfirmDialog
@ -60,7 +59,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleStylePage
import com.github.damontecres.wholphin.ui.setup.UpdateViewModel
import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog
import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel
@ -125,8 +123,6 @@ fun PreferencesContent(
when (preferenceScreenOption) {
PreferenceScreenOption.BASIC -> basicPreferences
PreferenceScreenOption.ADVANCED -> advancedPreferences
PreferenceScreenOption.USER_INTERFACE -> uiPreferences
PreferenceScreenOption.SUBTITLES -> SubtitleSettings.preferences
PreferenceScreenOption.EXO_PLAYER -> ExoPlayerPreferences
PreferenceScreenOption.MPV -> MpvPreferences
}
@ -134,8 +130,6 @@ fun PreferencesContent(
when (preferenceScreenOption) {
PreferenceScreenOption.BASIC -> R.string.settings
PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
PreferenceScreenOption.USER_INTERFACE -> R.string.ui_interface
PreferenceScreenOption.SUBTITLES -> R.string.subtitle_style
PreferenceScreenOption.EXO_PLAYER -> R.string.exoplayer_options
PreferenceScreenOption.MPV -> R.string.mpv_options
}
@ -526,7 +520,6 @@ fun PreferencesPage(
when (preferenceScreenOption) {
PreferenceScreenOption.BASIC,
PreferenceScreenOption.ADVANCED,
PreferenceScreenOption.USER_INTERFACE,
PreferenceScreenOption.EXO_PLAYER,
PreferenceScreenOption.MPV,
-> {
@ -539,12 +532,6 @@ fun PreferencesPage(
.align(Alignment.TopEnd),
)
}
PreferenceScreenOption.SUBTITLES -> {
SubtitleStylePage(
initialPreferences,
)
}
}
}
}

View file

@ -0,0 +1,195 @@
package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.SubtitlePreferences
import com.github.damontecres.wholphin.preferences.resetSubtitles
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.preferences.ClickPreference
import com.github.damontecres.wholphin.ui.preferences.ComposablePreference
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
import com.github.damontecres.wholphin.ui.preferences.PreferenceValidation
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import kotlinx.coroutines.launch
@Composable
fun SubtitlePreferencesContent(
title: String,
preferences: SubtitlePreferences,
prefList: List<PreferenceGroup<SubtitlePreferences>>,
onPreferenceChange: suspend (SubtitlePreferences) -> Unit,
modifier: Modifier = Modifier,
viewModel: PreferencesViewModel = hiltViewModel(),
onFocus: (Int, Int) -> Unit = { _, _ -> },
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val focusRequester = remember { FocusRequester() }
var focusedIndex by rememberSaveable { mutableStateOf(Pair(0, 0)) }
val state = rememberLazyListState()
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
// Forces the animated to trigger
visible = true
}
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInHorizontally { it / 2 },
exit = fadeOut() + slideOutHorizontally { it / 2 },
modifier = modifier,
) {
LaunchedEffect(Unit) {
focusRequester.tryRequestFocus()
}
LazyColumn(
state = state,
horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(0.dp),
contentPadding = PaddingValues(16.dp),
modifier = Modifier.background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)),
) {
stickyHeader {
Text(
text = title,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
)
}
prefList.forEachIndexed { groupIndex, group ->
item {
Text(
text = stringResource(group.title),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Start,
modifier =
Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 4.dp),
)
}
val groupPreferences =
group.preferences +
group.conditionalPreferences
.filter { it.condition.invoke(preferences) }
.map { it.preferences }
.flatten()
groupPreferences.forEachIndexed { prefIndex, pref ->
pref as AppPreference<SubtitlePreferences, Any>
item {
val interactionSource = remember { MutableInteractionSource() }
val focused = interactionSource.collectIsFocusedAsState().value
LaunchedEffect(focused) {
if (focused) {
focusedIndex = Pair(groupIndex, prefIndex)
onFocus.invoke(groupIndex, prefIndex)
}
}
when (pref) {
SubtitleSettings.Reset -> {
ClickPreference(
title = stringResource(pref.title),
onClick = {
scope.launch(ExceptionHandler()) {
val newValue =
SubtitlePreferences
.newBuilder()
.apply { resetSubtitles() }
.build()
onPreferenceChange.invoke(newValue)
}
},
interactionSource = interactionSource,
)
}
else -> {
val value = pref.getter.invoke(preferences)
ComposablePreference(
preference = pref,
value = value,
onNavigate = viewModel.navigationManager::navigateTo,
onValueChange = { newValue ->
val validation = pref.validate(newValue)
when (validation) {
is PreferenceValidation.Invalid -> {
// TODO?
Toast
.makeText(
context,
validation.message,
Toast.LENGTH_SHORT,
).show()
}
PreferenceValidation.Valid -> {
scope.launch(ExceptionHandler()) {
onPreferenceChange.invoke(
pref.setter(
preferences,
newValue,
),
)
}
}
}
},
interactionSource = interactionSource,
modifier =
Modifier
.ifElse(
groupIndex == focusedIndex.first && prefIndex == focusedIndex.second,
Modifier.focusRequester(focusRequester),
),
)
}
}
}
}
}
}
}
}

View file

@ -2,6 +2,8 @@ package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Build
import android.view.Display
import androidx.annotation.OptIn
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
@ -13,14 +15,14 @@ import androidx.media3.ui.CaptionStyleCompat
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppChoicePreference
import com.github.damontecres.wholphin.preferences.AppClickablePreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.AppDestinationPreference
import com.github.damontecres.wholphin.preferences.AppSliderPreference
import com.github.damontecres.wholphin.preferences.AppSwitchPreference
import com.github.damontecres.wholphin.preferences.BackgroundStyle
import com.github.damontecres.wholphin.preferences.EdgeStyle
import com.github.damontecres.wholphin.preferences.SubtitlePreferences
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
import com.github.damontecres.wholphin.ui.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferenceGroup
import com.github.damontecres.wholphin.util.mpv.MPVLib
import com.github.damontecres.wholphin.util.mpv.setPropertyColor
@ -28,18 +30,17 @@ import timber.log.Timber
object SubtitleSettings {
val FontSize =
AppSliderPreference<AppPreferences>(
AppSliderPreference<SubtitlePreferences>(
title = R.string.font_size,
defaultValue = 24,
min = 8,
max = 70,
interval = 2,
getter = {
it.interfacePreferences.subtitlesPreferences.fontSize
.toLong()
it.fontSize.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontSize = value.toInt() }
prefs.update { fontSize = value.toInt() }
},
summarizer = { value -> value?.toString() },
)
@ -59,12 +60,12 @@ object SubtitleSettings {
)
val FontColor =
AppChoicePreference<AppPreferences, Color>(
AppChoicePreference<SubtitlePreferences, Color>(
title = R.string.font_color,
defaultValue = Color.White,
getter = { Color(it.interfacePreferences.subtitlesPreferences.fontColor) },
getter = { Color(it.fontColor) },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontColor = value.toArgb().and(0x00FFFFFF) }
prefs.update { fontColor = value.toArgb().and(0x00FFFFFF) }
},
displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White },
@ -75,49 +76,49 @@ object SubtitleSettings {
)
val FontBold =
AppSwitchPreference<AppPreferences>(
AppSwitchPreference<SubtitlePreferences>(
title = R.string.bold_font,
defaultValue = false,
getter = { it.interfacePreferences.subtitlesPreferences.fontBold },
getter = { it.fontBold },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontBold = value }
prefs.update { fontBold = value }
},
)
val FontItalic =
AppSwitchPreference<AppPreferences>(
AppSwitchPreference<SubtitlePreferences>(
title = R.string.italic_font,
defaultValue = false,
getter = { it.interfacePreferences.subtitlesPreferences.fontItalic },
getter = { it.fontItalic },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontItalic = value }
prefs.update { fontItalic = value }
},
)
val FontOpacity =
AppSliderPreference<AppPreferences>(
AppSliderPreference<SubtitlePreferences>(
title = R.string.font_opacity,
defaultValue = 100,
min = 10,
max = 100,
interval = 10,
getter = {
it.interfacePreferences.subtitlesPreferences.fontOpacity
it.fontOpacity
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontOpacity = value.toInt() }
prefs.update { fontOpacity = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val EdgeStylePref =
AppChoicePreference<AppPreferences, EdgeStyle>(
AppChoicePreference<SubtitlePreferences, EdgeStyle>(
title =
R.string.edge_style,
defaultValue = EdgeStyle.EDGE_SOLID,
getter = { it.interfacePreferences.subtitlesPreferences.edgeStyle },
getter = { it.edgeStyle },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeStyle = value }
prefs.update { edgeStyle = value }
},
displayValues = R.array.subtitle_edge,
indexToValue = { EdgeStyle.forNumber(it) },
@ -125,12 +126,12 @@ object SubtitleSettings {
)
val EdgeColor =
AppChoicePreference<AppPreferences, Color>(
AppChoicePreference<SubtitlePreferences, Color>(
title = R.string.edge_color,
defaultValue = Color.Black,
getter = { Color(it.interfacePreferences.subtitlesPreferences.edgeColor) },
getter = { Color(it.edgeColor) },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeColor = value.toArgb().and(0x00FFFFFF) }
prefs.update { edgeColor = value.toArgb().and(0x00FFFFFF) }
},
displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White },
@ -141,29 +142,29 @@ object SubtitleSettings {
)
val EdgeThickness =
AppSliderPreference<AppPreferences>(
AppSliderPreference<SubtitlePreferences>(
title = R.string.edge_size,
defaultValue = 4,
min = 1,
max = 32,
interval = 1,
getter = {
it.interfacePreferences.subtitlesPreferences.edgeThickness
it.edgeThickness
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeThickness = value.toInt() }
prefs.update { edgeThickness = value.toInt() }
},
summarizer = { value -> value?.let { "${it / 2.0}" } },
)
val BackgroundColor =
AppChoicePreference<AppPreferences, Color>(
AppChoicePreference<SubtitlePreferences, Color>(
title = R.string.background_color,
defaultValue = Color.Transparent,
getter = { Color(it.interfacePreferences.subtitlesPreferences.backgroundColor) },
getter = { Color(it.backgroundColor) },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundColor = value.toArgb().and(0x00FFFFFF) }
prefs.update { backgroundColor = value.toArgb().and(0x00FFFFFF) }
},
displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White },
@ -174,30 +175,30 @@ object SubtitleSettings {
)
val BackgroundOpacity =
AppSliderPreference<AppPreferences>(
AppSliderPreference<SubtitlePreferences>(
title = R.string.background_opacity,
defaultValue = 50,
min = 10,
max = 100,
interval = 10,
getter = {
it.interfacePreferences.subtitlesPreferences.backgroundOpacity
it.backgroundOpacity
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundOpacity = value.toInt() }
prefs.update { backgroundOpacity = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val BackgroundStylePref =
AppChoicePreference<AppPreferences, BackgroundStyle>(
AppChoicePreference<SubtitlePreferences, BackgroundStyle>(
title =
R.string.background_style,
defaultValue = BackgroundStyle.BG_NONE,
getter = { it.interfacePreferences.subtitlesPreferences.backgroundStyle },
getter = { it.backgroundStyle },
setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundStyle = value }
prefs.update { backgroundStyle = value }
},
displayValues = R.array.background_style,
indexToValue = { BackgroundStyle.forNumber(it) },
@ -205,29 +206,51 @@ object SubtitleSettings {
)
val Margin =
AppSliderPreference<AppPreferences>(
AppSliderPreference<SubtitlePreferences>(
title = R.string.subtitle_margin,
defaultValue = 8,
min = 0,
max = 100,
interval = 1,
getter = {
it.interfacePreferences.subtitlesPreferences.margin
it.margin
.toLong()
},
setter = { prefs, value ->
prefs.updateSubtitlePreferences { margin = value.toInt() }
prefs.update { margin = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val ImageOpacity =
AppSliderPreference<SubtitlePreferences>(
title = R.string.image_subtitle_opacity,
defaultValue = 100,
min = 10,
max = 100,
interval = 5,
getter = {
it.imageSubtitleOpacity.toLong()
},
setter = { prefs, value ->
prefs.update { imageSubtitleOpacity = value.toInt() }
},
summarizer = { value -> value?.let { "$it%" } },
)
val Reset =
AppClickablePreference<AppPreferences>(
AppClickablePreference<SubtitlePreferences>(
title = R.string.reset,
getter = { },
setter = { prefs, _ -> prefs },
)
val HdrSettings =
AppDestinationPreference<SubtitlePreferences>(
title = R.string.hdr_subtitle_style,
destination = Destination.SubtitleSettings(true),
)
val preferences =
listOf(
PreferenceGroup(
@ -264,11 +287,25 @@ object SubtitleSettings {
preferences =
listOf(
Margin,
ImageOpacity,
Reset,
),
),
)
val hdrPreferenceGroup =
listOf(
PreferenceGroup(
title = R.string.hdr,
preferences =
listOf(
HdrSettings,
),
),
)
fun shouldShowHdr(display: Display): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && display.isHdr
private fun combine(
color: Int,
opacity: Int,
@ -361,3 +398,5 @@ object SubtitleSettings {
MPVLib.setPropertyString("sub-border-style", borderStyle)
}
}
inline fun SubtitlePreferences.update(block: SubtitlePreferences.Builder.() -> Unit): SubtitlePreferences = toBuilder().apply(block).build()

View file

@ -1,5 +1,7 @@
package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.content.pm.ActivityInfo
import android.os.Build
import androidx.annotation.Dimension
import androidx.annotation.OptIn
import androidx.compose.foundation.Image
@ -12,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -19,9 +22,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -30,20 +37,25 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.SubtitleView
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption
import com.github.damontecres.wholphin.ui.preferences.PreferencesContent
import com.github.damontecres.wholphin.preferences.SubtitlePreferences
import com.github.damontecres.wholphin.preferences.resetSubtitles
import com.github.damontecres.wholphin.preferences.updateInterfacePreferences
import com.github.damontecres.wholphin.ui.findActivity
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import com.github.damontecres.wholphin.util.Media3SubtitleOverride
import timber.log.Timber
@OptIn(UnstableApi::class)
@Composable
fun SubtitleStylePage(
initialPreferences: AppPreferences,
hdrSettings: Boolean,
modifier: Modifier = Modifier,
viewModel: PreferencesViewModel = hiltViewModel(),
) {
val context = LocalContext.current
val density = LocalDensity.current
var preferences by remember { mutableStateOf(initialPreferences) }
LaunchedEffect(Unit) {
@ -51,8 +63,27 @@ fun SubtitleStylePage(
preferences = it
}
}
val prefs = preferences.interfacePreferences.subtitlesPreferences
val display = LocalView.current.display
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
DisposableEffect(context) {
if (hdrSettings) {
Timber.v("Switching color mode to HDR")
context.findActivity()?.window?.colorMode = ActivityInfo.COLOR_MODE_HDR
}
onDispose {
context.findActivity()?.window?.colorMode = ActivityInfo.COLOR_MODE_DEFAULT
}
}
}
val prefs =
if (hdrSettings) {
preferences.interfacePreferences.hdrSubtitlesPreferences
} else {
preferences.interfacePreferences.subtitlesPreferences
}
var focusedOnMargin by remember { mutableStateOf(false) }
var focusedOnImageOpacity by remember { mutableStateOf(false) }
Row(
modifier = modifier,
@ -72,7 +103,7 @@ fun SubtitleStylePage(
Modifier
.fillMaxSize(),
)
if (!focusedOnMargin) {
if (!focusedOnMargin && !focusedOnImageOpacity) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier =
@ -109,7 +140,7 @@ fun SubtitleStylePage(
)
}
}
} else {
} else if (focusedOnMargin) {
// Margin
AndroidView(
factory = { context ->
@ -129,17 +160,79 @@ fun SubtitleStylePage(
Modifier
.fillMaxSize(),
)
} else if (focusedOnImageOpacity) {
AndroidView(
factory = { context ->
SubtitleView(context)
},
update = {
it.setStyle(
SubtitlePreferences
.newBuilder()
.apply {
resetSubtitles()
}.build()
.toSubtitleStyle(),
)
it.setCues(
listOf(
Cue
.Builder()
.setText("ExoPlayer only:\nImage based subtitles can be dimmed.")
.build(),
),
)
},
modifier =
Modifier
.fillMaxSize()
.alpha(prefs.imageSubtitleOpacity / 100f),
)
}
}
PreferencesContent(
initialPreferences = preferences,
preferenceScreenOption = PreferenceScreenOption.SUBTITLES,
val display = LocalView.current.display
val prefList =
remember(hdrSettings, display) {
if (!hdrSettings && SubtitleSettings.shouldShowHdr(display)) {
// If not on HDR page and display is HDR capable, then show the HDR button
SubtitleSettings.preferences + SubtitleSettings.hdrPreferenceGroup
} else {
SubtitleSettings.preferences
}
}
SubtitlePreferencesContent(
title =
if (hdrSettings) {
stringResource(R.string.hdr_subtitle_style)
} else {
stringResource(R.string.subtitle_style)
},
preferences =
if (hdrSettings) {
preferences.interfacePreferences.hdrSubtitlesPreferences
} else {
preferences.interfacePreferences.subtitlesPreferences
},
prefList = prefList,
onPreferenceChange = { newSubtitlePrefs ->
viewModel.preferenceDataStore.updateData {
it.updateInterfacePreferences {
if (hdrSettings) {
hdrSubtitlesPreferences = newSubtitlePrefs
} else {
subtitlesPreferences = newSubtitlePrefs
}
}
}
},
onFocus = { groupIndex, prefIndex ->
focusedOnMargin =
SubtitleSettings.preferences.getOrNull(groupIndex)?.preferences?.getOrNull(
prefIndex,
) == SubtitleSettings.Margin
val focusedPref =
SubtitleSettings.preferences
.getOrNull(groupIndex)
?.preferences
?.getOrNull(prefIndex)
focusedOnMargin = focusedPref == SubtitleSettings.Margin
focusedOnImageOpacity = focusedPref == SubtitleSettings.ImageOpacity
},
modifier =
Modifier

View file

@ -127,6 +127,7 @@ message SubtitlePreferences{
bool font_italic = 10;
int32 margin = 11;
int32 edge_thickness = 12;
int32 image_subtitle_opacity = 13;
}
message LiveTvPreferences {
@ -152,6 +153,7 @@ message InterfacePreferences {
SubtitlePreferences subtitles_preferences = 7;
LiveTvPreferences live_tv_preferences = 8;
BackdropStyle backdrop_style = 9;
SubtitlePreferences hdr_subtitles_preferences = 10;
}
message AdvancedPreferences {

View file

@ -462,6 +462,8 @@
<string name="request_4k">Request in 4K</string>
<string name="software_decoding_av1">AV1 software decoding</string>
<string name="upgrade_mpv_toast">MPV is now the default player except for HDR.\nYou can change this in settings.</string>
<string name="hdr_subtitle_style">HDR subtitle style</string>
<string name="image_subtitle_opacity">Image subtitle opacity</string>
<string-array name="theme_song_volume">
<item>Disabled</item>