From da16bb994e94bd0c74d9f0791cd742751d6add50 Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Thu, 14 May 2026 16:10:59 +0530 Subject: [PATCH 1/2] feat: Add stylized subtitles toggle --- .../tv/ui/screens/player/PlayerScreen.kt | 38 ++-- .../tv/ui/screens/player/PlayerViewModel.kt | 3 + .../tv/ui/screens/settings/SettingsScreen.kt | 174 ++++++++++-------- .../ui/screens/settings/SettingsViewModel.kt | 13 ++ app/src/main/res/values/strings.xml | 2 + 5 files changed, 140 insertions(+), 90 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt index a635548e6..b15bf2520 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt @@ -1566,6 +1566,7 @@ fun PlayerScreen( val subtitleSizePref = uiState.subtitleSize val subtitleColorPref = uiState.subtitleColor val subtitleStylePref = uiState.subtitleStyle + val subtitleStylizedPref = uiState.subtitleStylized val aspectModeLabel = when (playerResizeMode) { AspectRatioFrameLayout.RESIZE_MODE_ZOOM -> "Zoom" AspectRatioFrameLayout.RESIZE_MODE_FILL -> "Fill" @@ -1998,17 +1999,25 @@ fun PlayerScreen( setKeepContentOnPlayerReset(true) resizeMode = playerResizeMode - // Enable subtitle view with Netflix-style: bold white text with black outline + // Enable subtitle view with styling based on user preference subtitleView?.apply { - // Read subtitle appearance from user settings - val subSizeSp = when (subtitleSizePref) { - "Small" -> 18f; "Large" -> 30f; "Extra Large" -> 36f; else -> 24f - } - val subFgColor = when (subtitleColorPref) { - "Yellow" -> android.graphics.Color.YELLOW - "Green" -> android.graphics.Color.GREEN - "Cyan" -> android.graphics.Color.CYAN - else -> android.graphics.Color.WHITE + if (subtitleStylizedPref) { + // Stylized mode: let Media3 render embedded ASS/SSA styles + // (colors, fonts, positioning, z-order). User prefs are + // only used as a fallback CaptionStyle for plain SRT/VTT. + setApplyEmbeddedStyles(true) + setApplyEmbeddedFontSizes(true) + setBottomPaddingFraction(0.08f) + } else { + // Uniform mode: override everything with user preferences + val subSizeSp = when (subtitleSizePref) { + "Small" -> 18f; "Large" -> 30f; "Extra Large" -> 36f; else -> 24f + } + val subFgColor = when (subtitleColorPref) { + "Yellow" -> android.graphics.Color.YELLOW + "Green" -> android.graphics.Color.GREEN + "Cyan" -> android.graphics.Color.CYAN + else -> android.graphics.Color.WHITE } val subTypeface = when (subtitleStylePref) { "Normal" -> android.graphics.Typeface.DEFAULT @@ -2034,10 +2043,11 @@ fun PlayerScreen( subTypeface ) ) - setApplyEmbeddedStyles(false) - setApplyEmbeddedFontSizes(false) - setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, subSizeSp) - setBottomPaddingFraction(0.08f) + setApplyEmbeddedStyles(false) + setApplyEmbeddedFontSizes(false) + setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, subSizeSp) + setBottomPaddingFraction(0.08f) + } } } }, diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerViewModel.kt index f8ce51a4c..0eb72b4c2 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerViewModel.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerViewModel.kt @@ -72,6 +72,7 @@ data class PlayerUiState( val subtitleSize: String = "Medium", val subtitleColor: String = "White", val subtitleStyle: String = "Bold", + val subtitleStylized: Boolean = true, val error: String? = null, val isSetupError: Boolean = false, // true when error is due to missing addons (shows friendly guide instead of red error) // Auto-play next episode at end of current one. Mirrors the profile-scoped @@ -347,6 +348,7 @@ class PlayerViewModel @Inject constructor( val subSize = prefs[profileManager.profileStringKey("subtitle_size")] ?: "Medium" val subColor = prefs[profileManager.profileStringKey("subtitle_color")] ?: "White" val subStyle = prefs[profileManager.profileStringKey("subtitle_style")] ?: "Bold" + val subStylized = prefs[profileManager.profileBooleanKey("subtitle_stylized")] ?: true val autoPlayNext = prefs[autoPlayNextKey()] ?: true val showLoadingStats = prefs[showLoadingStatsKey()] ?: true val volumeBoostDb = prefs[profileManager.profileStringKey("volume_boost_db")] @@ -379,6 +381,7 @@ class PlayerViewModel @Inject constructor( subtitleSize = subSize, subtitleColor = subColor, subtitleStyle = subStyle, + subtitleStylized = subStylized, autoPlayNext = autoPlayNext, showLoadingStats = showLoadingStats, volumeBoostDb = volumeBoostDb diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt index 4ad971dad..4cb558c46 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsScreen.kt @@ -331,7 +331,7 @@ fun SettingsScreen( } val sectionMaxIndex: (String) -> Int = { section -> when (section) { - "general" -> 32 // 33 rows (27 from main + 6 AI subtitle rows) + "general" -> 33 // 34 rows (28 from main + 6 AI subtitle rows) "iptv" -> 2 + uiState.iptvPlaylists.size // Add + rows + refresh + clear "home_server" -> uiState.homeServerConnections.size + 3 "catalogs" -> uiState.catalogs.size // Add + rows @@ -743,31 +743,32 @@ fun SettingsScreen( 5 -> viewModel.cycleSubtitleColor() 6 -> viewModel.cycleSubtitleOffset() 7 -> viewModel.cycleSubtitleStyle() - 8 -> viewModel.setFilterSubtitlesByLanguage(!uiState.filterSubtitlesByLanguage) - 9 -> viewModel.setAutoPlayNext(!uiState.autoPlayNext) - 10 -> viewModel.setAutoPlaySingleSource(!uiState.autoPlaySingleSource) - 11 -> viewModel.cycleAutoPlayMinQuality() - 12 -> viewModel.setTrailerAutoPlay(!uiState.trailerAutoPlay) - 13 -> viewModel.setTrailerSoundEnabled(!uiState.trailerSoundEnabled) - 14 -> viewModel.cycleFrameRateMatchingMode() - 15 -> showQualityFiltersModal = true - 16 -> viewModel.toggleCardLayoutMode() - 17 -> openUiModeWarningDialog() - 18 -> viewModel.setSkipProfileSelection(!uiState.skipProfileSelection) - 19 -> viewModel.setOledBlackBackground(!uiState.oledBlackBackground) - 20 -> viewModel.cycleClockFormat() - 21 -> viewModel.setShowBudget(!uiState.showBudget) - 22 -> viewModel.setSpoilerBlurEnabled(!uiState.spoilerBlurEnabled) - 23 -> viewModel.cycleFocusBorderColor() - 24 -> openDnsProviderPicker() - 25 -> viewModel.setShowLoadingStats(!uiState.showLoadingStats) - 26 -> viewModel.cycleVolumeBoost() - 27 -> viewModel.setSubtitleAiEnabled(!uiState.subtitleAiEnabled) - 28 -> showAiModelDialog = true - 29 -> viewModel.setSubtitleAiAutoSelect(!uiState.subtitleAiAutoSelect) - 30 -> viewModel.setSubtitleRemoveHearingImpaired(!uiState.subtitleRemoveHearingImpaired) - 31 -> showAiApiKeyDialog = true - 32 -> viewModel.startAiKeyServer() + 8 -> viewModel.toggleSubtitleStylized() + 9 -> viewModel.setFilterSubtitlesByLanguage(!uiState.filterSubtitlesByLanguage) + 10 -> viewModel.setAutoPlayNext(!uiState.autoPlayNext) + 11 -> viewModel.setAutoPlaySingleSource(!uiState.autoPlaySingleSource) + 12 -> viewModel.cycleAutoPlayMinQuality() + 13 -> viewModel.setTrailerAutoPlay(!uiState.trailerAutoPlay) + 14 -> viewModel.setTrailerSoundEnabled(!uiState.trailerSoundEnabled) + 15 -> viewModel.cycleFrameRateMatchingMode() + 16 -> showQualityFiltersModal = true + 17 -> viewModel.toggleCardLayoutMode() + 18 -> openUiModeWarningDialog() + 19 -> viewModel.setSkipProfileSelection(!uiState.skipProfileSelection) + 20 -> viewModel.setOledBlackBackground(!uiState.oledBlackBackground) + 21 -> viewModel.cycleClockFormat() + 22 -> viewModel.setShowBudget(!uiState.showBudget) + 23 -> viewModel.setSpoilerBlurEnabled(!uiState.spoilerBlurEnabled) + 24 -> viewModel.cycleFocusBorderColor() + 25 -> openDnsProviderPicker() + 26 -> viewModel.setShowLoadingStats(!uiState.showLoadingStats) + 27 -> viewModel.cycleVolumeBoost() + 28 -> viewModel.setSubtitleAiEnabled(!uiState.subtitleAiEnabled) + 29 -> showAiModelDialog = true + 30 -> viewModel.setSubtitleAiAutoSelect(!uiState.subtitleAiAutoSelect) + 31 -> viewModel.setSubtitleRemoveHearingImpaired(!uiState.subtitleRemoveHearingImpaired) + 32 -> showAiApiKeyDialog = true + 33 -> viewModel.startAiKeyServer() } } "iptv" -> { @@ -1077,6 +1078,7 @@ fun SettingsScreen( subtitleColor = uiState.subtitleColor, subtitleOffset = uiState.subtitleOffset, subtitleStyle = uiState.subtitleStyle, + subtitleStylized = uiState.subtitleStylized, deviceModeOverride = uiState.deviceModeOverride, skipProfileSelection = uiState.skipProfileSelection, oledBlackBackground = uiState.oledBlackBackground, @@ -1114,6 +1116,7 @@ fun SettingsScreen( onSubtitleColorClick = { viewModel.cycleSubtitleColor() }, onSubtitleOffsetClick = { viewModel.cycleSubtitleOffset() }, onSubtitleStyleClick = { viewModel.cycleSubtitleStyle() }, + onSubtitleStylizedToggle = { viewModel.toggleSubtitleStylized() }, filterSubtitlesByLanguage = uiState.filterSubtitlesByLanguage, onFilterSubtitlesByLanguageToggle = { viewModel.setFilterSubtitlesByLanguage(it) }, qualityFilterValue = uiState.qualityFilterPresetLabel, @@ -3238,6 +3241,14 @@ private fun MobileSettingsSubPage( isFocused = false, onClick = { viewModel.cycleSubtitleStyle() } ) + MobileSettingsRow( + icon = Icons.Default.Subtitles, + title = stringResource(R.string.subtitle_stylized), + subtitle = stringResource(R.string.subtitle_stylized_desc), + value = if (uiState.subtitleStylized) "On" else "Off", + isFocused = false, + onClick = { viewModel.toggleSubtitleStylized() } + ) MobileSettingsRow( icon = Icons.Default.Subtitles, title = stringResource(R.string.filter_subtitles), @@ -3925,6 +3936,8 @@ private fun GeneralSettings( onSubtitleColorClick: () -> Unit = {}, onSubtitleOffsetClick: () -> Unit = {}, onSubtitleStyleClick: () -> Unit = {}, + subtitleStylized: Boolean = true, + onSubtitleStylizedToggle: () -> Unit = {}, filterSubtitlesByLanguage: Boolean = true, onFilterSubtitlesByLanguageToggle: (Boolean) -> Unit = {}, onTrailerAutoPlayToggle: (Boolean) -> Unit = {}, @@ -4032,13 +4045,22 @@ private fun GeneralSettings( modifier = Modifier.settingsFocusSlot(7) ) Spacer(modifier = Modifier.height(10.dp)) + SettingsToggleRow( + title = stringResource(R.string.subtitle_stylized), + subtitle = stringResource(R.string.subtitle_stylized_desc), + isEnabled = subtitleStylized, + isFocused = focusedIndex == 8, + onToggle = { onSubtitleStylizedToggle() }, + modifier = Modifier.settingsFocusSlot(8) + ) + Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.filter_subtitles), subtitle = stringResource(R.string.filter_subtitles_desc), isEnabled = filterSubtitlesByLanguage, - isFocused = focusedIndex == 8, + isFocused = focusedIndex == 9, onToggle = onFilterSubtitlesByLanguageToggle, - modifier = Modifier.settingsFocusSlot(8) + modifier = Modifier.settingsFocusSlot(9) ) // ── Playback ── @@ -4054,18 +4076,18 @@ private fun GeneralSettings( title = stringResource(R.string.auto_play_next_title), subtitle = stringResource(R.string.auto_play_desc), isEnabled = autoPlayNext, - isFocused = focusedIndex == 9, + isFocused = focusedIndex == 10, onToggle = onAutoPlayToggle, - modifier = Modifier.settingsFocusSlot(9) + modifier = Modifier.settingsFocusSlot(10) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.autoplay), subtitle = stringResource(R.string.autoplay_desc), isEnabled = autoPlaySingleSource, - isFocused = focusedIndex == 10, + isFocused = focusedIndex == 11, onToggle = onAutoPlaySingleSourceToggle, - modifier = Modifier.settingsFocusSlot(10) + modifier = Modifier.settingsFocusSlot(11) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4073,27 +4095,27 @@ private fun GeneralSettings( title = stringResource(R.string.auto_play_min_quality), subtitle = stringResource(R.string.auto_play_quality_desc), value = autoPlayMinQuality, - isFocused = focusedIndex == 11, + isFocused = focusedIndex == 12, onClick = onAutoPlayMinQualityClick, - modifier = Modifier.settingsFocusSlot(11) + modifier = Modifier.settingsFocusSlot(12) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.trailer_auto_play), subtitle = stringResource(R.string.trailer_desc), isEnabled = trailerAutoPlay, - isFocused = focusedIndex == 12, + isFocused = focusedIndex == 13, onToggle = onTrailerAutoPlayToggle, - modifier = Modifier.settingsFocusSlot(12) + modifier = Modifier.settingsFocusSlot(13) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.trailer_sound), subtitle = stringResource(R.string.trailer_sound_desc), isEnabled = trailerSoundEnabled, - isFocused = focusedIndex == 13, + isFocused = focusedIndex == 14, onToggle = onTrailerSoundEnabledToggle, - modifier = Modifier.settingsFocusSlot(13) + modifier = Modifier.settingsFocusSlot(14) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4101,9 +4123,9 @@ private fun GeneralSettings( title = stringResource(R.string.frame_rate), subtitle = stringResource(R.string.frame_rate_desc), value = frameRateMatchingMode, - isFocused = focusedIndex == 14, + isFocused = focusedIndex == 15, onClick = onFrameRateMatchingClick, - modifier = Modifier.settingsFocusSlot(14) + modifier = Modifier.settingsFocusSlot(15) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4111,9 +4133,9 @@ private fun GeneralSettings( title = stringResource(R.string.quality_filters), subtitle = stringResource(R.string.quality_filters_desc), value = qualityFilterValue, - isFocused = focusedIndex == 15, + isFocused = focusedIndex == 16, onClick = onQualityFiltersClick, - modifier = Modifier.settingsFocusSlot(15) + modifier = Modifier.settingsFocusSlot(16) ) // ── Interface ── @@ -4130,9 +4152,9 @@ private fun GeneralSettings( title = stringResource(R.string.card_layout), subtitle = stringResource(R.string.card_layout_desc), value = cardLayoutMode, - isFocused = focusedIndex == 16, + isFocused = focusedIndex == 17, onClick = onCardLayoutToggle, - modifier = Modifier.settingsFocusSlot(16) + modifier = Modifier.settingsFocusSlot(17) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4145,27 +4167,27 @@ private fun GeneralSettings( "phone" -> "Phone" else -> "Auto" }, - isFocused = focusedIndex == 17, + isFocused = focusedIndex == 18, onClick = onDeviceModeClick, - modifier = Modifier.settingsFocusSlot(17) + modifier = Modifier.settingsFocusSlot(18) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.skip_profile), subtitle = stringResource(R.string.skip_profile_desc), isEnabled = skipProfileSelection, - isFocused = focusedIndex == 18, + isFocused = focusedIndex == 19, onToggle = onSkipProfileSelectionToggle, - modifier = Modifier.settingsFocusSlot(18) + modifier = Modifier.settingsFocusSlot(19) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.oled_black_background), subtitle = stringResource(R.string.oled_black_background_desc), isEnabled = oledBlackBackground, - isFocused = focusedIndex == 19, + isFocused = focusedIndex == 20, onToggle = onOledBlackBackgroundToggle, - modifier = Modifier.settingsFocusSlot(19) + modifier = Modifier.settingsFocusSlot(20) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4173,9 +4195,9 @@ private fun GeneralSettings( title = stringResource(R.string.clock_format), subtitle = stringResource(R.string.clock_format_desc), value = if (clockFormat == "12h") "12-hour" else "24-hour", - isFocused = focusedIndex == 20, + isFocused = focusedIndex == 21, onClick = onClockFormatClick, - modifier = Modifier.settingsFocusSlot(20) + modifier = Modifier.settingsFocusSlot(21) ) Spacer(modifier = Modifier.height(10.dp)) // Home hero controls — issue #72. The movie Budget line on the hero banner @@ -4184,18 +4206,18 @@ private fun GeneralSettings( title = stringResource(R.string.show_budget), subtitle = stringResource(R.string.show_budget_desc), isEnabled = showBudget, - isFocused = focusedIndex == 21, + isFocused = focusedIndex == 22, onToggle = onShowBudgetToggle, - modifier = Modifier.settingsFocusSlot(21) + modifier = Modifier.settingsFocusSlot(22) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.spoiler_blur), subtitle = stringResource(R.string.spoiler_blur_desc), isEnabled = spoilerBlurEnabled, - isFocused = focusedIndex == 22, + isFocused = focusedIndex == 23, onToggle = onSpoilerBlurToggle, - modifier = Modifier.settingsFocusSlot(22) + modifier = Modifier.settingsFocusSlot(23) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4203,9 +4225,9 @@ private fun GeneralSettings( title = stringResource(R.string.focus_border_color), subtitle = stringResource(R.string.focus_border_color_desc), value = focusBorderColor, - isFocused = focusedIndex == 23, + isFocused = focusedIndex == 24, onClick = onFocusBorderColorClick, - modifier = Modifier.settingsFocusSlot(23) + modifier = Modifier.settingsFocusSlot(24) ) // ── Network ── @@ -4222,18 +4244,18 @@ private fun GeneralSettings( title = stringResource(R.string.dns_provider), subtitle = stringResource(R.string.dns_desc), value = dnsProvider, - isFocused = focusedIndex == 24, + isFocused = focusedIndex == 25, onClick = onDnsProviderClick, - modifier = Modifier.settingsFocusSlot(24) + modifier = Modifier.settingsFocusSlot(25) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.show_loading_stats), subtitle = stringResource(R.string.show_loading_stats_desc), isEnabled = showLoadingStats, - isFocused = focusedIndex == 25, + isFocused = focusedIndex == 26, onToggle = onShowLoadingStatsToggle, - modifier = Modifier.settingsFocusSlot(25) + modifier = Modifier.settingsFocusSlot(26) ) // ── Audio ── @@ -4253,9 +4275,9 @@ private fun GeneralSettings( 0 -> "Off" else -> "+${volumeBoostDb} dB" }, - isFocused = focusedIndex == 26, + isFocused = focusedIndex == 27, onClick = onVolumeBoostClick, - modifier = Modifier.settingsFocusSlot(26) + modifier = Modifier.settingsFocusSlot(27) ) // ── AI Subtitles ── @@ -4270,9 +4292,9 @@ private fun GeneralSettings( title = stringResource(R.string.ai_subtitle_translation_title), subtitle = stringResource(R.string.ai_subtitle_translation_desc), isEnabled = subtitleAiEnabled, - isFocused = focusedIndex == 27, + isFocused = focusedIndex == 28, onToggle = onSubtitleAiEnabledToggle, - modifier = Modifier.settingsFocusSlot(27) + modifier = Modifier.settingsFocusSlot(28) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4283,27 +4305,27 @@ private fun GeneralSettings( com.arflix.tv.ui.screens.player.SubtitleAiModel.GROQ_LLAMA_70B -> "Groq – Llama 3.3 70B" com.arflix.tv.ui.screens.player.SubtitleAiModel.GEMINI_FLASH_25 -> "Google – Gemini 2.5 Flash" }, - isFocused = focusedIndex == 28, + isFocused = focusedIndex == 29, onClick = onSubtitleAiModelClick, - modifier = Modifier.settingsFocusSlot(28).alpha(if (subtitleAiEnabled) 1f else 0.4f) + modifier = Modifier.settingsFocusSlot(29).alpha(if (subtitleAiEnabled) 1f else 0.4f) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.ai_auto_select_title), subtitle = stringResource(R.string.ai_auto_select_desc), isEnabled = subtitleAiAutoSelect, - isFocused = focusedIndex == 29, + isFocused = focusedIndex == 30, onToggle = onSubtitleAiAutoSelectToggle, - modifier = Modifier.settingsFocusSlot(29).alpha(if (subtitleAiEnabled) 1f else 0.4f) + modifier = Modifier.settingsFocusSlot(30).alpha(if (subtitleAiEnabled) 1f else 0.4f) ) Spacer(modifier = Modifier.height(10.dp)) SettingsToggleRow( title = stringResource(R.string.ai_remove_hi_title), subtitle = stringResource(R.string.ai_remove_hi_desc), isEnabled = subtitleRemoveHearingImpaired, - isFocused = focusedIndex == 30, + isFocused = focusedIndex == 31, onToggle = onSubtitleRemoveHearingImpairedToggle, - modifier = Modifier.settingsFocusSlot(30).alpha(if (subtitleAiEnabled) 1f else 0.4f) + modifier = Modifier.settingsFocusSlot(31).alpha(if (subtitleAiEnabled) 1f else 0.4f) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4311,9 +4333,9 @@ private fun GeneralSettings( title = stringResource(R.string.ai_api_key_title), subtitle = stringResource(R.string.ai_api_key_desc), value = maskAiApiKey(subtitleAiApiKey, stringResource(R.string.ai_key_not_set)), - isFocused = focusedIndex == 31, + isFocused = focusedIndex == 32, onClick = onSubtitleAiApiKeyClick, - modifier = Modifier.settingsFocusSlot(31).alpha(if (subtitleAiEnabled) 1f else 0.4f) + modifier = Modifier.settingsFocusSlot(32).alpha(if (subtitleAiEnabled) 1f else 0.4f) ) Spacer(modifier = Modifier.height(10.dp)) SettingsRow( @@ -4321,9 +4343,9 @@ private fun GeneralSettings( title = stringResource(R.string.ai_scan_qr_title), subtitle = stringResource(R.string.ai_scan_qr_desc), value = "", - isFocused = focusedIndex == 32, + isFocused = focusedIndex == 33, onClick = onSubtitleAiQrClick, - modifier = Modifier.settingsFocusSlot(32).alpha(if (subtitleAiEnabled) 1f else 0.4f) + modifier = Modifier.settingsFocusSlot(33).alpha(if (subtitleAiEnabled) 1f else 0.4f) ) if (subtitleAiEnabled) { Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt index 75209138b..7d066adc1 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/settings/SettingsViewModel.kt @@ -103,6 +103,7 @@ data class SettingsUiState( val subtitleColor: String = "White", val subtitleStyle: String = "Bold", val subtitleOffset: String = "Low", + val subtitleStylized: Boolean = true, val filterSubtitlesByLanguage: Boolean = true, val secondarySubtitle: String = "Off", val trailerAutoPlay: Boolean = false, @@ -267,6 +268,7 @@ class SettingsViewModel @Inject constructor( private fun subtitleColorKey() = profileManager.profileStringKey("subtitle_color") private fun subtitleOffsetKey() = profileManager.profileStringKey("subtitle_offset") private fun subtitleStyleKey() = profileManager.profileStringKey("subtitle_style") + private fun subtitleStylizedKey() = profileManager.profileBooleanKey("subtitle_stylized") private fun filterSubtitlesByLanguageKey() = profileManager.profileBooleanKey("filter_subtitles_by_lang") private fun secondarySubtitleKey() = profileManager.profileStringKey("secondary_subtitle") private val dnsProviderKey = stringPreferencesKey(OkHttpProvider.DNS_PROVIDER_PREF_KEY) @@ -417,6 +419,7 @@ class SettingsViewModel @Inject constructor( val subtitleColor = prefs[subtitleColorKey()] ?: "White" val subtitleStyle = prefs[subtitleStyleKey()] ?: "Bold" val subtitleOffset = prefs[subtitleOffsetKey()] ?: "Low" + val subtitleStylized = prefs[subtitleStylizedKey()] ?: true val filterSubtitlesByLanguage = prefs[filterSubtitlesByLanguageKey()] ?: true val secondarySubtitle = prefs[secondarySubtitleKey()]?.trim()?.takeIf { it.isNotBlank() } ?: "Off" val dnsProviderValue = normalizeDnsProviderValue(prefs[dnsProviderKey]) @@ -480,6 +483,7 @@ class SettingsViewModel @Inject constructor( subtitleColor = subtitleColor, subtitleStyle = subtitleStyle, subtitleOffset = subtitleOffset, + subtitleStylized = subtitleStylized, filterSubtitlesByLanguage = filterSubtitlesByLanguage, secondarySubtitle = secondarySubtitle, dnsProvider = dnsProviderLabel(dnsProviderValue), @@ -1106,6 +1110,15 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { context.settingsDataStore.edit { it[subtitleStyleKey()] = next }; _uiState.value = _uiState.value.copy(subtitleStyle = next); syncLocalStateToCloud(silent = true) } } + fun toggleSubtitleStylized() { + val next = !_uiState.value.subtitleStylized + viewModelScope.launch { + context.settingsDataStore.edit { it[subtitleStylizedKey()] = next } + _uiState.value = _uiState.value.copy(subtitleStylized = next) + syncLocalStateToCloud(silent = true) + } + } + // ── AI Subtitles ────────────────────────────────────────────────────────── fun setSubtitleAiEnabled(enabled: Boolean) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc3569052..96d453552 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -158,6 +158,8 @@ Text color for subtitles Bold, Normal, or Background style for subtitles Vertical position for subtitles + Stylized Subtitles + Honor embedded styles from ASS/SSA subtitles. When off, all subtitles use your size, color, and style settings. Start next episode automatically Off opens the source picker on Play Min quality for auto-play From 98450868887d2b32c0f2f90a5edfa3afcca0b65b Mon Sep 17 00:00:00 2001 From: Himanth Reddy Date: Fri, 15 May 2026 13:35:40 +0530 Subject: [PATCH 2/2] fix: address stylized subtitle review feedback --- .../tv/data/repository/CloudSyncRepository.kt | 5 ++ .../tv/ui/screens/player/PlayerScreen.kt | 72 +++++++++---------- 2 files changed, 41 insertions(+), 36 deletions(-) diff --git a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt index 9a850d80d..8991e7e2e 100644 --- a/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt +++ b/app/src/main/kotlin/com/arflix/tv/data/repository/CloudSyncRepository.kt @@ -106,6 +106,7 @@ class CloudSyncRepository @Inject constructor( val subtitleColor: String = "White", val subtitleStyle: String = "Bold", val subtitleOffset: String = "Low", + val subtitleStylized: Boolean = true, val cardLayoutMode: String = CARD_LAYOUT_MODE_LANDSCAPE, val frameRateMatchingMode: String = "Off", val autoPlayNext: Boolean = true, @@ -160,6 +161,8 @@ class CloudSyncRepository @Inject constructor( profileManager.profileStringKeyFor(profileId, "subtitle_offset") private fun subtitleStyleKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "subtitle_style") + private fun subtitleStylizedKeyFor(profileId: String) = + profileManager.profileBooleanKeyFor(profileId, "subtitle_stylized") private fun iptvHiddenGroupsKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "iptv_hidden_groups") private fun iptvGroupOrderKeyFor(profileId: String) = @@ -279,6 +282,7 @@ class CloudSyncRepository @Inject constructor( subtitleColor = prefs[subtitleColorKeyFor(profile.id)] ?: "White", subtitleOffset = prefs[subtitleOffsetKeyFor(profile.id)] ?: "Low", subtitleStyle = prefs[subtitleStyleKeyFor(profile.id)] ?: "Bold", + subtitleStylized = prefs[subtitleStylizedKeyFor(profile.id)] ?: true, iptvHiddenGroups = prefs[iptvHiddenGroupsKeyFor(profile.id)] ?: "", iptvGroupOrder = prefs[iptvGroupOrderKeyFor(profile.id)] ?: "", secondarySubtitle = prefs[secondarySubtitleKeyFor(profile.id)] ?: "Off", @@ -695,6 +699,7 @@ class CloudSyncRepository @Inject constructor( prefs[subtitleColorKeyFor(profileId)] = state.subtitleColor prefs[subtitleOffsetKeyFor(profileId)] = state.subtitleOffset prefs[subtitleStyleKeyFor(profileId)] = state.subtitleStyle + prefs[subtitleStylizedKeyFor(profileId)] = state.subtitleStylized if (state.iptvHiddenGroups.isNotBlank()) prefs[iptvHiddenGroupsKeyFor(profileId)] = state.iptvHiddenGroups if (state.iptvGroupOrder.isNotBlank()) prefs[iptvGroupOrderKeyFor(profileId)] = state.iptvGroupOrder prefs[secondarySubtitleKeyFor(profileId)] = state.secondarySubtitle.ifBlank { "Off" } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt index b15bf2520..2b609c1b6 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt @@ -2001,52 +2001,52 @@ fun PlayerScreen( // Enable subtitle view with styling based on user preference subtitleView?.apply { + val subSizeSp = when (subtitleSizePref) { + "Small" -> 18f; "Large" -> 30f; "Extra Large" -> 36f; else -> 24f + } + val subFgColor = when (subtitleColorPref) { + "Yellow" -> android.graphics.Color.YELLOW + "Green" -> android.graphics.Color.GREEN + "Cyan" -> android.graphics.Color.CYAN + else -> android.graphics.Color.WHITE + } + val subTypeface = when (subtitleStylePref) { + "Normal" -> android.graphics.Typeface.DEFAULT + "Background" -> android.graphics.Typeface.DEFAULT_BOLD + else -> android.graphics.Typeface.DEFAULT_BOLD + } + val subEdgeType = when (subtitleStylePref) { + "Normal" -> androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE + "Background" -> androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE + else -> androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE + } + val subBgColor = when (subtitleStylePref) { + "Background" -> android.graphics.Color.argb(180, 0, 0, 0) + else -> android.graphics.Color.TRANSPARENT + } + setStyle( + androidx.media3.ui.CaptionStyleCompat( + subFgColor, + android.graphics.Color.TRANSPARENT, + subBgColor, + subEdgeType, + android.graphics.Color.BLACK, + subTypeface + ) + ) + setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, subSizeSp) + setBottomPaddingFraction(0.08f) + if (subtitleStylizedPref) { // Stylized mode: let Media3 render embedded ASS/SSA styles // (colors, fonts, positioning, z-order). User prefs are // only used as a fallback CaptionStyle for plain SRT/VTT. setApplyEmbeddedStyles(true) setApplyEmbeddedFontSizes(true) - setBottomPaddingFraction(0.08f) } else { // Uniform mode: override everything with user preferences - val subSizeSp = when (subtitleSizePref) { - "Small" -> 18f; "Large" -> 30f; "Extra Large" -> 36f; else -> 24f - } - val subFgColor = when (subtitleColorPref) { - "Yellow" -> android.graphics.Color.YELLOW - "Green" -> android.graphics.Color.GREEN - "Cyan" -> android.graphics.Color.CYAN - else -> android.graphics.Color.WHITE - } - val subTypeface = when (subtitleStylePref) { - "Normal" -> android.graphics.Typeface.DEFAULT - "Background" -> android.graphics.Typeface.DEFAULT_BOLD - else -> android.graphics.Typeface.DEFAULT_BOLD - } - val subEdgeType = when (subtitleStylePref) { - "Normal" -> androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE - "Background" -> androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_NONE - else -> androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE - } - val subBgColor = when (subtitleStylePref) { - "Background" -> android.graphics.Color.argb(180, 0, 0, 0) - else -> android.graphics.Color.TRANSPARENT - } - setStyle( - androidx.media3.ui.CaptionStyleCompat( - subFgColor, - android.graphics.Color.TRANSPARENT, - subBgColor, - subEdgeType, - android.graphics.Color.BLACK, - subTypeface - ) - ) setApplyEmbeddedStyles(false) setApplyEmbeddedFontSizes(false) - setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, subSizeSp) - setBottomPaddingFraction(0.08f) } } }