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 a635548e6..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
@@ -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,9 +1999,8 @@ 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
}
@@ -2009,35 +2009,45 @@ fun PlayerScreen(
"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
- )
+ }
+ 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)
+
+ 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)
+ } else {
+ // Uniform mode: override everything with user preferences
+ setApplyEmbeddedStyles(false)
+ setApplyEmbeddedFontSizes(false)
+ }
}
}
},
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