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.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.datastore.core.DataStore 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.services.tvprovider.TvProviderSchedulerService
import com.github.damontecres.wholphin.ui.CoilConfig import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService 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.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ApplicationContent 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.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@ -133,7 +136,9 @@ class MainActivity : AppCompatActivity() {
Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}") Timber.i("MainActivity.onCreate: savedInstanceState is null=${savedInstanceState == null}")
lifecycle.addObserver(playbackLifecycleObserver) lifecycle.addObserver(playbackLifecycleObserver)
if (savedInstanceState == null) { if (savedInstanceState == null) {
appUpgradeHandler.copySubfont(false) lifecycleScope.launchIO {
appUpgradeHandler.copySubfont(false)
}
} }
viewModel.serverRepository.currentUser.observe(this) { user -> viewModel.serverRepository.currentUser.observe(this) { user ->
if (user?.hasPin == true) { if (user?.hasPin == true) {
@ -148,6 +153,25 @@ class MainActivity : AppCompatActivity() {
viewModel.appStart() viewModel.appStart()
setContent { setContent {
val appPreferences by userPreferencesDataStore.data.collectAsState(null) 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 -> appPreferences?.let { appPreferences ->
LaunchedEffect(appPreferences.signInAutomatically) { LaunchedEffect(appPreferences.signInAutomatically) {
signInAuto = appPreferences.signInAutomatically signInAuto = appPreferences.signInAutomatically

View file

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

View file

@ -100,6 +100,12 @@ class AppPreferencesSerializer
.apply { .apply {
resetSubtitles() resetSubtitles()
}.build() }.build()
hdrSubtitlesPreferences =
SubtitlePreferences
.newBuilder()
.apply {
resetSubtitles()
}.build()
liveTvPreferences = liveTvPreferences =
LiveTvPreferences LiveTvPreferences
@ -193,4 +199,5 @@ fun SubtitlePreferences.Builder.resetSubtitles() {
backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue backgroundStyle = SubtitleSettings.BackgroundStylePref.defaultValue
margin = SubtitleSettings.Margin.defaultValue.toInt() margin = SubtitleSettings.Margin.defaultValue.toInt()
edgeThickness = SubtitleSettings.EdgeThickness.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, @param:ApplicationContext private val context: Context,
) { ) {
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager 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() val supportedDisplayModes get() = display.supportedModes.orEmpty()

View file

@ -38,6 +38,11 @@ sealed class Destination(
val screen: PreferenceScreenOption, val screen: PreferenceScreenOption,
) : Destination(true) ) : Destination(true)
@Serializable
data class SubtitleSettings(
val hdr: Boolean,
) : Destination(true)
@Serializable @Serializable
data object Search : Destination() 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.main.SearchPage
import com.github.damontecres.wholphin.ui.playback.PlaybackPage import com.github.damontecres.wholphin.ui.playback.PlaybackPage
import com.github.damontecres.wholphin.ui.preferences.PreferencesPage 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 com.github.damontecres.wholphin.ui.setup.InstallUpdatePage
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType 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 -> { is Destination.SeriesOverview -> {
SeriesOverview( SeriesOverview(
preferences = preferences, preferences = preferences,

View file

@ -34,6 +34,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.ErrorMessage
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.components.TextButton 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.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.applyToMpv
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize 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.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
import timber.log.Timber
import java.util.UUID import java.util.UUID
import kotlin.time.Duration import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
@ -176,6 +179,7 @@ fun PlaybackPageContent(
LaunchedEffect(player) { LaunchedEffect(player) {
if (playerBackend == PlayerBackend.MPV) { if (playerBackend == PlayerBackend.MPV) {
scope.launch(Dispatchers.IO + ExceptionHandler()) { scope.launch(Dispatchers.IO + ExceptionHandler()) {
// MPV can't play HDR, so always use regular settings
preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv( preferences.appPreferences.interfacePreferences.subtitlesPreferences.applyToMpv(
configuration, configuration,
density, 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 // Subtitles
if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled) { if (skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled) {
val maxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f) val maxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f)
val isImageSubtitles = remember(cues) { cues.firstOrNull()?.bitmap != null }
AndroidView( AndroidView(
factory = { context -> factory = { context ->
SubtitleView(context).apply { SubtitleView(context).apply {
preferences.appPreferences.interfacePreferences.subtitlesPreferences.let { subtitleSettings.let {
setStyle(it.toSubtitleStyle()) setStyle(it.toSubtitleStyle())
setFixedTextSize(Dimension.SP, it.fontSize.toFloat()) setFixedTextSize(Dimension.SP, it.fontSize.toFloat())
setBottomPaddingFraction(it.margin.toFloat() / 100f) setBottomPaddingFraction(it.margin.toFloat() / 100f)
@ -416,10 +433,8 @@ fun PlaybackPageContent(
}, },
update = { update = {
it.setCues(cues) it.setCues(cues)
Media3SubtitleOverride( Media3SubtitleOverride(subtitleSettings.calculateEdgeSize(density))
preferences.appPreferences.interfacePreferences.subtitlesPreferences .apply(it)
.calculateEdgeSize(density),
).apply(it)
}, },
onReset = { onReset = {
it.setCues(null) it.setCues(null)
@ -428,7 +443,8 @@ fun PlaybackPageContent(
Modifier Modifier
.fillMaxSize(maxSize) .fillMaxSize(maxSize)
.align(Alignment.TopCenter) .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 androidx.annotation.StringRes
import com.github.damontecres.wholphin.preferences.AppPreference import com.github.damontecres.wholphin.preferences.AppPreference
import com.github.damontecres.wholphin.preferences.AppPreferences
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
/** /**
* A group of preferences * A group of preferences
*/ */
data class PreferenceGroup( data class PreferenceGroup<T>(
@param:StringRes val title: Int, @param:StringRes val title: Int,
val preferences: List<AppPreference<AppPreferences, out Any?>>, val preferences: List<AppPreference<T, out Any?>>,
val conditionalPreferences: List<ConditionalPreferences> = listOf(), val conditionalPreferences: List<ConditionalPreferences<T>> = listOf(),
) )
data class ConditionalPreferences( data class ConditionalPreferences<T>(
val condition: (AppPreferences) -> Boolean, val condition: (T) -> Boolean,
val preferences: List<AppPreference<AppPreferences, out Any?>>, val preferences: List<AppPreference<T, out Any?>>,
) )
/** /**
@ -34,8 +33,6 @@ sealed interface PreferenceValidation {
enum class PreferenceScreenOption { enum class PreferenceScreenOption {
BASIC, BASIC,
ADVANCED, ADVANCED,
USER_INTERFACE,
SUBTITLES,
EXO_PLAYER, EXO_PLAYER,
MPV, 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.PlayerBackend
import com.github.damontecres.wholphin.preferences.advancedPreferences import com.github.damontecres.wholphin.preferences.advancedPreferences
import com.github.damontecres.wholphin.preferences.basicPreferences 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.preferences.updatePlaybackPreferences
import com.github.damontecres.wholphin.services.UpdateChecker import com.github.damontecres.wholphin.services.UpdateChecker
import com.github.damontecres.wholphin.ui.components.ConfirmDialog 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.playOnClickSound
import com.github.damontecres.wholphin.ui.playSoundOnFocus import com.github.damontecres.wholphin.ui.playSoundOnFocus
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings 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.UpdateViewModel
import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog import com.github.damontecres.wholphin.ui.setup.seerr.AddSeerServerDialog
import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel import com.github.damontecres.wholphin.ui.setup.seerr.SwitchSeerrViewModel
@ -125,8 +123,6 @@ fun PreferencesContent(
when (preferenceScreenOption) { when (preferenceScreenOption) {
PreferenceScreenOption.BASIC -> basicPreferences PreferenceScreenOption.BASIC -> basicPreferences
PreferenceScreenOption.ADVANCED -> advancedPreferences PreferenceScreenOption.ADVANCED -> advancedPreferences
PreferenceScreenOption.USER_INTERFACE -> uiPreferences
PreferenceScreenOption.SUBTITLES -> SubtitleSettings.preferences
PreferenceScreenOption.EXO_PLAYER -> ExoPlayerPreferences PreferenceScreenOption.EXO_PLAYER -> ExoPlayerPreferences
PreferenceScreenOption.MPV -> MpvPreferences PreferenceScreenOption.MPV -> MpvPreferences
} }
@ -134,8 +130,6 @@ fun PreferencesContent(
when (preferenceScreenOption) { when (preferenceScreenOption) {
PreferenceScreenOption.BASIC -> R.string.settings PreferenceScreenOption.BASIC -> R.string.settings
PreferenceScreenOption.ADVANCED -> R.string.advanced_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.EXO_PLAYER -> R.string.exoplayer_options
PreferenceScreenOption.MPV -> R.string.mpv_options PreferenceScreenOption.MPV -> R.string.mpv_options
} }
@ -526,7 +520,6 @@ fun PreferencesPage(
when (preferenceScreenOption) { when (preferenceScreenOption) {
PreferenceScreenOption.BASIC, PreferenceScreenOption.BASIC,
PreferenceScreenOption.ADVANCED, PreferenceScreenOption.ADVANCED,
PreferenceScreenOption.USER_INTERFACE,
PreferenceScreenOption.EXO_PLAYER, PreferenceScreenOption.EXO_PLAYER,
PreferenceScreenOption.MPV, PreferenceScreenOption.MPV,
-> { -> {
@ -539,12 +532,6 @@ fun PreferencesPage(
.align(Alignment.TopEnd), .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.content.res.Configuration
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Build
import android.view.Display
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
@ -13,14 +15,14 @@ import androidx.media3.ui.CaptionStyleCompat
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppChoicePreference import com.github.damontecres.wholphin.preferences.AppChoicePreference
import com.github.damontecres.wholphin.preferences.AppClickablePreference 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.AppSliderPreference
import com.github.damontecres.wholphin.preferences.AppSwitchPreference import com.github.damontecres.wholphin.preferences.AppSwitchPreference
import com.github.damontecres.wholphin.preferences.BackgroundStyle import com.github.damontecres.wholphin.preferences.BackgroundStyle
import com.github.damontecres.wholphin.preferences.EdgeStyle import com.github.damontecres.wholphin.preferences.EdgeStyle
import com.github.damontecres.wholphin.preferences.SubtitlePreferences 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.indexOfFirstOrNull
import com.github.damontecres.wholphin.ui.nav.Destination
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
@ -28,18 +30,17 @@ import timber.log.Timber
object SubtitleSettings { object SubtitleSettings {
val FontSize = val FontSize =
AppSliderPreference<AppPreferences>( AppSliderPreference<SubtitlePreferences>(
title = R.string.font_size, title = R.string.font_size,
defaultValue = 24, defaultValue = 24,
min = 8, min = 8,
max = 70, max = 70,
interval = 2, interval = 2,
getter = { getter = {
it.interfacePreferences.subtitlesPreferences.fontSize it.fontSize.toLong()
.toLong()
}, },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontSize = value.toInt() } prefs.update { fontSize = value.toInt() }
}, },
summarizer = { value -> value?.toString() }, summarizer = { value -> value?.toString() },
) )
@ -59,12 +60,12 @@ object SubtitleSettings {
) )
val FontColor = val FontColor =
AppChoicePreference<AppPreferences, Color>( AppChoicePreference<SubtitlePreferences, Color>(
title = R.string.font_color, title = R.string.font_color,
defaultValue = Color.White, defaultValue = Color.White,
getter = { Color(it.interfacePreferences.subtitlesPreferences.fontColor) }, getter = { Color(it.fontColor) },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontColor = value.toArgb().and(0x00FFFFFF) } prefs.update { fontColor = value.toArgb().and(0x00FFFFFF) }
}, },
displayValues = R.array.font_colors, displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White }, indexToValue = { colorList.getOrNull(it) ?: Color.White },
@ -75,49 +76,49 @@ object SubtitleSettings {
) )
val FontBold = val FontBold =
AppSwitchPreference<AppPreferences>( AppSwitchPreference<SubtitlePreferences>(
title = R.string.bold_font, title = R.string.bold_font,
defaultValue = false, defaultValue = false,
getter = { it.interfacePreferences.subtitlesPreferences.fontBold }, getter = { it.fontBold },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontBold = value } prefs.update { fontBold = value }
}, },
) )
val FontItalic = val FontItalic =
AppSwitchPreference<AppPreferences>( AppSwitchPreference<SubtitlePreferences>(
title = R.string.italic_font, title = R.string.italic_font,
defaultValue = false, defaultValue = false,
getter = { it.interfacePreferences.subtitlesPreferences.fontItalic }, getter = { it.fontItalic },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontItalic = value } prefs.update { fontItalic = value }
}, },
) )
val FontOpacity = val FontOpacity =
AppSliderPreference<AppPreferences>( AppSliderPreference<SubtitlePreferences>(
title = R.string.font_opacity, title = R.string.font_opacity,
defaultValue = 100, defaultValue = 100,
min = 10, min = 10,
max = 100, max = 100,
interval = 10, interval = 10,
getter = { getter = {
it.interfacePreferences.subtitlesPreferences.fontOpacity it.fontOpacity
.toLong() .toLong()
}, },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { fontOpacity = value.toInt() } prefs.update { fontOpacity = value.toInt() }
}, },
summarizer = { value -> value?.let { "$it%" } }, summarizer = { value -> value?.let { "$it%" } },
) )
val EdgeStylePref = val EdgeStylePref =
AppChoicePreference<AppPreferences, EdgeStyle>( AppChoicePreference<SubtitlePreferences, EdgeStyle>(
title = title =
R.string.edge_style, R.string.edge_style,
defaultValue = EdgeStyle.EDGE_SOLID, defaultValue = EdgeStyle.EDGE_SOLID,
getter = { it.interfacePreferences.subtitlesPreferences.edgeStyle }, getter = { it.edgeStyle },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeStyle = value } prefs.update { edgeStyle = value }
}, },
displayValues = R.array.subtitle_edge, displayValues = R.array.subtitle_edge,
indexToValue = { EdgeStyle.forNumber(it) }, indexToValue = { EdgeStyle.forNumber(it) },
@ -125,12 +126,12 @@ object SubtitleSettings {
) )
val EdgeColor = val EdgeColor =
AppChoicePreference<AppPreferences, Color>( AppChoicePreference<SubtitlePreferences, Color>(
title = R.string.edge_color, title = R.string.edge_color,
defaultValue = Color.Black, defaultValue = Color.Black,
getter = { Color(it.interfacePreferences.subtitlesPreferences.edgeColor) }, getter = { Color(it.edgeColor) },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeColor = value.toArgb().and(0x00FFFFFF) } prefs.update { edgeColor = value.toArgb().and(0x00FFFFFF) }
}, },
displayValues = R.array.font_colors, displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White }, indexToValue = { colorList.getOrNull(it) ?: Color.White },
@ -141,29 +142,29 @@ object SubtitleSettings {
) )
val EdgeThickness = val EdgeThickness =
AppSliderPreference<AppPreferences>( AppSliderPreference<SubtitlePreferences>(
title = R.string.edge_size, title = R.string.edge_size,
defaultValue = 4, defaultValue = 4,
min = 1, min = 1,
max = 32, max = 32,
interval = 1, interval = 1,
getter = { getter = {
it.interfacePreferences.subtitlesPreferences.edgeThickness it.edgeThickness
.toLong() .toLong()
}, },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { edgeThickness = value.toInt() } prefs.update { edgeThickness = value.toInt() }
}, },
summarizer = { value -> value?.let { "${it / 2.0}" } }, summarizer = { value -> value?.let { "${it / 2.0}" } },
) )
val BackgroundColor = val BackgroundColor =
AppChoicePreference<AppPreferences, Color>( AppChoicePreference<SubtitlePreferences, Color>(
title = R.string.background_color, title = R.string.background_color,
defaultValue = Color.Transparent, defaultValue = Color.Transparent,
getter = { Color(it.interfacePreferences.subtitlesPreferences.backgroundColor) }, getter = { Color(it.backgroundColor) },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundColor = value.toArgb().and(0x00FFFFFF) } prefs.update { backgroundColor = value.toArgb().and(0x00FFFFFF) }
}, },
displayValues = R.array.font_colors, displayValues = R.array.font_colors,
indexToValue = { colorList.getOrNull(it) ?: Color.White }, indexToValue = { colorList.getOrNull(it) ?: Color.White },
@ -174,30 +175,30 @@ object SubtitleSettings {
) )
val BackgroundOpacity = val BackgroundOpacity =
AppSliderPreference<AppPreferences>( AppSliderPreference<SubtitlePreferences>(
title = R.string.background_opacity, title = R.string.background_opacity,
defaultValue = 50, defaultValue = 50,
min = 10, min = 10,
max = 100, max = 100,
interval = 10, interval = 10,
getter = { getter = {
it.interfacePreferences.subtitlesPreferences.backgroundOpacity it.backgroundOpacity
.toLong() .toLong()
}, },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundOpacity = value.toInt() } prefs.update { backgroundOpacity = value.toInt() }
}, },
summarizer = { value -> value?.let { "$it%" } }, summarizer = { value -> value?.let { "$it%" } },
) )
val BackgroundStylePref = val BackgroundStylePref =
AppChoicePreference<AppPreferences, BackgroundStyle>( AppChoicePreference<SubtitlePreferences, BackgroundStyle>(
title = title =
R.string.background_style, R.string.background_style,
defaultValue = BackgroundStyle.BG_NONE, defaultValue = BackgroundStyle.BG_NONE,
getter = { it.interfacePreferences.subtitlesPreferences.backgroundStyle }, getter = { it.backgroundStyle },
setter = { prefs, value -> setter = { prefs, value ->
prefs.updateSubtitlePreferences { backgroundStyle = value } prefs.update { backgroundStyle = value }
}, },
displayValues = R.array.background_style, displayValues = R.array.background_style,
indexToValue = { BackgroundStyle.forNumber(it) }, indexToValue = { BackgroundStyle.forNumber(it) },
@ -205,29 +206,51 @@ object SubtitleSettings {
) )
val Margin = val Margin =
AppSliderPreference<AppPreferences>( AppSliderPreference<SubtitlePreferences>(
title = R.string.subtitle_margin, title = R.string.subtitle_margin,
defaultValue = 8, defaultValue = 8,
min = 0, min = 0,
max = 100, max = 100,
interval = 1, interval = 1,
getter = { getter = {
it.interfacePreferences.subtitlesPreferences.margin it.margin
.toLong() .toLong()
}, },
setter = { prefs, value -> 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%" } }, summarizer = { value -> value?.let { "$it%" } },
) )
val Reset = val Reset =
AppClickablePreference<AppPreferences>( AppClickablePreference<SubtitlePreferences>(
title = R.string.reset, title = R.string.reset,
getter = { }, getter = { },
setter = { prefs, _ -> prefs }, setter = { prefs, _ -> prefs },
) )
val HdrSettings =
AppDestinationPreference<SubtitlePreferences>(
title = R.string.hdr_subtitle_style,
destination = Destination.SubtitleSettings(true),
)
val preferences = val preferences =
listOf( listOf(
PreferenceGroup( PreferenceGroup(
@ -264,11 +287,25 @@ object SubtitleSettings {
preferences = preferences =
listOf( listOf(
Margin, Margin,
ImageOpacity,
Reset, 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( private fun combine(
color: Int, color: Int,
opacity: Int, opacity: Int,
@ -361,3 +398,5 @@ object SubtitleSettings {
MPVLib.setPropertyString("sub-border-style", borderStyle) 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 package com.github.damontecres.wholphin.ui.preferences.subtitle
import android.content.pm.ActivityInfo
import android.os.Build
import androidx.annotation.Dimension import androidx.annotation.Dimension
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.foundation.Image 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.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -19,9 +22,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
@ -30,20 +37,25 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.SubtitleView import androidx.media3.ui.SubtitleView
import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.ui.preferences.PreferenceScreenOption import com.github.damontecres.wholphin.preferences.SubtitlePreferences
import com.github.damontecres.wholphin.ui.preferences.PreferencesContent 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.PreferencesViewModel
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings.calculateEdgeSize 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.ui.preferences.subtitle.SubtitleSettings.toSubtitleStyle
import com.github.damontecres.wholphin.util.Media3SubtitleOverride import com.github.damontecres.wholphin.util.Media3SubtitleOverride
import timber.log.Timber
@OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
fun SubtitleStylePage( fun SubtitleStylePage(
initialPreferences: AppPreferences, initialPreferences: AppPreferences,
hdrSettings: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: PreferencesViewModel = hiltViewModel(), viewModel: PreferencesViewModel = hiltViewModel(),
) { ) {
val context = LocalContext.current
val density = LocalDensity.current val density = LocalDensity.current
var preferences by remember { mutableStateOf(initialPreferences) } var preferences by remember { mutableStateOf(initialPreferences) }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -51,8 +63,27 @@ fun SubtitleStylePage(
preferences = it 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 focusedOnMargin by remember { mutableStateOf(false) }
var focusedOnImageOpacity by remember { mutableStateOf(false) }
Row( Row(
modifier = modifier, modifier = modifier,
@ -72,7 +103,7 @@ fun SubtitleStylePage(
Modifier Modifier
.fillMaxSize(), .fillMaxSize(),
) )
if (!focusedOnMargin) { if (!focusedOnMargin && !focusedOnImageOpacity) {
Column( Column(
verticalArrangement = Arrangement.SpaceBetween, verticalArrangement = Arrangement.SpaceBetween,
modifier = modifier =
@ -109,7 +140,7 @@ fun SubtitleStylePage(
) )
} }
} }
} else { } else if (focusedOnMargin) {
// Margin // Margin
AndroidView( AndroidView(
factory = { context -> factory = { context ->
@ -129,17 +160,79 @@ fun SubtitleStylePage(
Modifier Modifier
.fillMaxSize(), .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( val display = LocalView.current.display
initialPreferences = preferences, val prefList =
preferenceScreenOption = PreferenceScreenOption.SUBTITLES, 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 -> onFocus = { groupIndex, prefIndex ->
val focusedPref =
focusedOnMargin = SubtitleSettings.preferences
SubtitleSettings.preferences.getOrNull(groupIndex)?.preferences?.getOrNull( .getOrNull(groupIndex)
prefIndex, ?.preferences
) == SubtitleSettings.Margin ?.getOrNull(prefIndex)
focusedOnMargin = focusedPref == SubtitleSettings.Margin
focusedOnImageOpacity = focusedPref == SubtitleSettings.ImageOpacity
}, },
modifier = modifier =
Modifier Modifier

View file

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

View file

@ -462,6 +462,8 @@
<string name="request_4k">Request in 4K</string> <string name="request_4k">Request in 4K</string>
<string name="software_decoding_av1">AV1 software decoding</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="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"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>