From ee440698b00d5498bb62971ea3a97fe73fdb9149 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Wed, 4 Feb 2026 18:22:11 -0500 Subject: [PATCH] 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 --- .../damontecres/wholphin/MainActivity.kt | 26 ++- .../wholphin/preferences/AppPreference.kt | 4 +- .../preferences/AppPreferencesSerializer.kt | 7 + .../wholphin/services/AppUpgradeHandler.kt | 15 ++ .../wholphin/services/RefreshRateService.kt | 2 +- .../wholphin/ui/nav/Destination.kt | 5 + .../wholphin/ui/nav/DestinationContent.kt | 9 + .../wholphin/ui/playback/PlaybackPage.kt | 28 ++- .../ui/preferences/PreferenceUtils.kt | 15 +- .../ui/preferences/PreferencesContent.kt | 13 -- .../subtitle/SubtitlePreferencesContent.kt | 195 ++++++++++++++++++ .../preferences/subtitle/SubtitleSettings.kt | 119 +++++++---- .../preferences/subtitle/SubtitleStylePage.kt | 119 +++++++++-- app/src/main/proto/WholphinDataStore.proto | 2 + app/src/main/res/values/strings.xml | 2 + 15 files changed, 475 insertions(+), 86 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index af88392e..567f8117 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -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 diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt index 70e83692..0472be5a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreference.kt @@ -710,7 +710,7 @@ sealed interface AppPreference { val SubtitleStyle = AppDestinationPreference( 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() - private val ExoPlayerSettings = listOf( AppPreference.FfmpegPreference, diff --git a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt index f84744c7..ca343804 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/preferences/AppPreferencesSerializer.kt @@ -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() } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt index d6fbea40..7e911767 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/AppUpgradeHandler.kt @@ -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() + } + } + } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt index d961d4c5..bc0c8f94 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/RefreshRateService.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 426d5328..e6f3636d 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 70ef71d2..09fd3936 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -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, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index fde12a22..f8b80020 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -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)), ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt index f84d3b89..c9f6f529 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferenceUtils.kt @@ -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( @param:StringRes val title: Int, - val preferences: List>, - val conditionalPreferences: List = listOf(), + val preferences: List>, + val conditionalPreferences: List> = listOf(), ) -data class ConditionalPreferences( - val condition: (AppPreferences) -> Boolean, - val preferences: List>, +data class ConditionalPreferences( + val condition: (T) -> Boolean, + val preferences: List>, ) /** @@ -34,8 +33,6 @@ sealed interface PreferenceValidation { enum class PreferenceScreenOption { BASIC, ADVANCED, - USER_INTERFACE, - SUBTITLES, EXO_PLAYER, MPV, ; diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt index 8fedf3b6..dcbda2fc 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/PreferencesContent.kt @@ -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, - ) - } } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt new file mode 100644 index 00000000..8abc6dbe --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitlePreferencesContent.kt @@ -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>, + 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 + 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), + ), + ) + } + } + } + } + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt index 506649d5..b7f3d764 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleSettings.kt @@ -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( + AppSliderPreference( 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( + AppChoicePreference( 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( + AppSwitchPreference( 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( + AppSwitchPreference( 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( + AppSliderPreference( 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( + AppChoicePreference( 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( + AppChoicePreference( 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( + AppSliderPreference( 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( + AppChoicePreference( 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( + AppSliderPreference( 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( + AppChoicePreference( 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( + AppSliderPreference( 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( + 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( + AppClickablePreference( title = R.string.reset, getter = { }, setter = { prefs, _ -> prefs }, ) + val HdrSettings = + AppDestinationPreference( + 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() diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt index b0bc6426..52b3570a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/preferences/subtitle/SubtitleStylePage.kt @@ -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 diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index 3c7f38ed..eac223b0 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -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 { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c83a3b63..c85ef303 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -462,6 +462,8 @@ Request in 4K AV1 software decoding MPV is now the default player except for HDR.\nYou can change this in settings. + HDR subtitle style + Image subtitle opacity Disabled