Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 36 additions & 28 deletions app/src/main/kotlin/com/arflix/tv/ui/screens/player/PlayerScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1342,6 +1342,7 @@ fun PlayerScreen(
// Read subtitle appearance prefs
val subtitleSizePref = uiState.subtitleSize
val subtitleColorPref = uiState.subtitleColor
val subtitleOffsetPref = uiState.subtitleOffset
val aspectModeLabel = when (playerResizeMode) {
AspectRatioFrameLayout.RESIZE_MODE_ZOOM -> "Zoom"
AspectRatioFrameLayout.RESIZE_MODE_FILL -> "Fill"
Expand Down Expand Up @@ -1627,39 +1628,46 @@ fun PlayerScreen(
useController = false
setKeepContentOnPlayerReset(true)
resizeMode = playerResizeMode

// Enable subtitle view with Netflix-style: bold white text with black outline
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
}
setStyle(
androidx.media3.ui.CaptionStyleCompat(
subFgColor,
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT,
androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE,
android.graphics.Color.BLACK,
android.graphics.Typeface.DEFAULT_BOLD
)
)
setApplyEmbeddedStyles(false)
setApplyEmbeddedFontSizes(false)
setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, subSizeSp)
setBottomPaddingFraction(0.08f)
}
}
},
update = { playerView ->
playerView.player = exoPlayer
playerView.resizeMode = playerResizeMode

// Apply subtitle styling in the update block so it reacts to state changes instantly
playerView.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 paddingFraction = when (subtitleOffsetPref) {
"Lowest" -> 0.02f
"Lower" -> 0.05f
"Normal" -> 0.08f
"Higher" -> 0.13f
"Highest" -> 0.18f
else -> 0.08f
}
setStyle(
androidx.media3.ui.CaptionStyleCompat(
subFgColor,
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT,
androidx.media3.ui.CaptionStyleCompat.EDGE_TYPE_OUTLINE,
android.graphics.Color.BLACK,
android.graphics.Typeface.DEFAULT_BOLD
)
)
setApplyEmbeddedStyles(false)
setApplyEmbeddedFontSizes(false)
setFixedTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, subSizeSp)
setBottomPaddingFraction(paddingFraction)
}
},
modifier = Modifier.fillMaxSize()
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ data class PlayerUiState(
val frameRateMatchingMode: String = "Off",
val subtitleSize: String = "Medium",
val subtitleColor: String = "White",
val subtitleOffset: String = "Normal",
val error: String? = null,
val isSetupError: Boolean = false, // true when error is due to missing addons (shows friendly guide instead of red error)
// Skip intro/recap
Expand Down Expand Up @@ -175,17 +176,26 @@ class PlayerViewModel @Inject constructor(
viewModelScope.launch {
val preferredAudioLanguage = resolvePreferredAudioLanguage()
val frameRateMatchingMode = resolveFrameRateMatchingMode()
val subSize = context.settingsDataStore.data.first()[profileManager.profileStringKey("subtitle_size")] ?: "Medium"
val subColor = context.settingsDataStore.data.first()[profileManager.profileStringKey("subtitle_color")] ?: "White"

_uiState.value = PlayerUiState(
isLoading = true,
isLoadingStreams = true,
preferredAudioLanguage = preferredAudioLanguage,
frameRateMatchingMode = frameRateMatchingMode,
subtitleSize = subSize,
subtitleColor = subColor
frameRateMatchingMode = frameRateMatchingMode
// Removed the static subtitle assignments from here
)

// ADD THIS: Continuously listen for subtitle preference changes
launch {
context.settingsDataStore.data.collect { prefs ->
_uiState.value = _uiState.value.copy(
subtitleSize = prefs[profileManager.profileStringKey("subtitle_size")] ?: "Medium",
subtitleColor = prefs[profileManager.profileStringKey("subtitle_color")] ?: "White",
subtitleOffset = prefs[profileManager.profileStringKey("subtitle_offset")] ?: "Normal"
)
}
}

// If stream URL provided, use it directly (except magnet links, which require resolution).
if (providedStreamUrl != null) {
val resumeData = resolveResumeData(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ fun SettingsScreen(
if (scrollState.maxValue <= 0) return@LaunchedEffect

val maxIndex = when (sectionIndex) {
0 -> 13 // General: 14 items
0 -> 14 // General: 15 items
1 -> 3 // IPTV: Configure + Refresh + Delete + Stalker
2 -> uiState.catalogs.size // Catalogs
3 -> uiState.addons.size // Addons
Expand Down Expand Up @@ -433,7 +433,7 @@ fun SettingsScreen(
Zone.CONTENT -> {
// Dynamic max based on current section
val maxIndex = when (sectionIndex) {
0 -> 13 // General: 14 items
0 -> 14 // General: 15 items
1 -> 3 // IPTV: Configure + Refresh + Delete + Stalker
2 -> uiState.catalogs.size // Catalogs: Add + N catalogs
3 -> uiState.addons.size // Addons: N addons + "Add Custom" button
Expand Down Expand Up @@ -478,15 +478,16 @@ fun SettingsScreen(
2 -> openAudioLanguagePicker()
3 -> viewModel.cycleSubtitleSize()
4 -> viewModel.cycleSubtitleColor()
5 -> viewModel.setAutoPlayNext(!uiState.autoPlayNext)
6 -> viewModel.setAutoPlaySingleSource(!uiState.autoPlaySingleSource)
7 -> viewModel.cycleAutoPlayMinQuality()
8 -> viewModel.setTrailerAutoPlay(!uiState.trailerAutoPlay)
9 -> viewModel.cycleFrameRateMatchingMode()
10 -> viewModel.toggleCardLayoutMode()
11 -> { val next = when (uiState.deviceModeOverride) { "auto" -> "tv"; "tv" -> "tablet"; "tablet" -> "phone"; else -> "auto" }; viewModel.setDeviceModeOverride(next) }
12 -> viewModel.setSkipProfileSelection(!uiState.skipProfileSelection)
13 -> openDnsProviderPicker()
5 -> viewModel.cycleSubtitleOffset()
6 -> viewModel.setAutoPlayNext(!uiState.autoPlayNext)
7 -> viewModel.setAutoPlaySingleSource(!uiState.autoPlaySingleSource)
8 -> viewModel.cycleAutoPlayMinQuality()
9 -> viewModel.setTrailerAutoPlay(!uiState.trailerAutoPlay)
10 -> viewModel.cycleFrameRateMatchingMode()
11 -> viewModel.toggleCardLayoutMode()
12 -> { val next = when (uiState.deviceModeOverride) { "auto" -> "tv"; "tv" -> "tablet"; "tablet" -> "phone"; else -> "auto" }; viewModel.setDeviceModeOverride(next) }
13 -> viewModel.setSkipProfileSelection(!uiState.skipProfileSelection)
14 -> openDnsProviderPicker()
}
}
1 -> { // IPTV
Expand Down Expand Up @@ -613,6 +614,7 @@ fun SettingsScreen(
autoPlayMinQuality = uiState.autoPlayMinQuality,
subtitleSize = uiState.subtitleSize,
subtitleColor = uiState.subtitleColor,
subtitleOffset = uiState.subtitleOffset,
deviceModeOverride = uiState.deviceModeOverride,
skipProfileSelection = uiState.skipProfileSelection,
focusedIndex = -1,
Expand Down Expand Up @@ -785,6 +787,7 @@ fun SettingsScreen(
contentLanguage = uiState.contentLanguage,
subtitleSize = uiState.subtitleSize,
subtitleColor = uiState.subtitleColor,
subtitleOffset = uiState.subtitleOffset,
deviceModeOverride = uiState.deviceModeOverride,
skipProfileSelection = uiState.skipProfileSelection,
focusedIndex = if (activeZone == Zone.CONTENT) contentFocusIndex else -1,
Expand All @@ -805,7 +808,8 @@ fun SettingsScreen(
onContentLanguageClick = openContentLanguagePicker,
onSkipProfileSelectionToggle = { viewModel.setSkipProfileSelection(it) },
onSubtitleSizeClick = { viewModel.cycleSubtitleSize() },
onSubtitleColorClick = { viewModel.cycleSubtitleColor() }
onSubtitleColorClick = { viewModel.cycleSubtitleColor() },
onSubtitleOffsetClick = { viewModel.cycleSubtitleOffset() }
)
"iptv" -> IptvSettings(
m3uUrl = uiState.iptvM3uUrl,
Expand Down Expand Up @@ -2112,6 +2116,7 @@ private fun GeneralSettings(
autoPlayMinQuality: String,
subtitleSize: String = "Medium",
subtitleColor: String = "White",
subtitleOffset: String = "Normal",
deviceModeOverride: String = "auto",
skipProfileSelection: Boolean = false,
focusedIndex: Int,
Expand All @@ -2129,6 +2134,7 @@ private fun GeneralSettings(
trailerAutoPlay: Boolean = false,
onSubtitleSizeClick: () -> Unit = {},
onSubtitleColorClick: () -> Unit = {},
onSubtitleOffsetClick: () -> Unit = {},
onTrailerAutoPlayToggle: (Boolean) -> Unit = {}
) {
Column {
Expand Down Expand Up @@ -2184,6 +2190,15 @@ private fun GeneralSettings(
isFocused = focusedIndex == 4,
onClick = onSubtitleColorClick
)
Spacer(modifier = Modifier.height(10.dp))
SettingsRow(
icon = Icons.Default.Subtitles,
title = "Subtitle Offset",
subtitle = "Vertical position for subtitles",
value = subtitleOffset,
isFocused = focusedIndex == 5,
onClick = onSubtitleOffsetClick
)

// ── Playback ──
Spacer(modifier = Modifier.height(24.dp))
Expand All @@ -2198,15 +2213,15 @@ private fun GeneralSettings(
title = "Auto-Play Next",
subtitle = "Start next episode automatically",
isEnabled = autoPlayNext,
isFocused = focusedIndex == 5,
isFocused = focusedIndex == 6,
onToggle = onAutoPlayToggle
)
Spacer(modifier = Modifier.height(10.dp))
SettingsToggleRow(
title = "Auto-Play Single Source",
subtitle = "Skip source picker with one source",
isEnabled = autoPlaySingleSource,
isFocused = focusedIndex == 6,
isFocused = focusedIndex == 7,
onToggle = onAutoPlaySingleSourceToggle
)
Spacer(modifier = Modifier.height(10.dp))
Expand All @@ -2215,15 +2230,15 @@ private fun GeneralSettings(
title = "Auto-Play Min Quality",
subtitle = "Min quality for auto-play",
value = autoPlayMinQuality,
isFocused = focusedIndex == 7,
isFocused = focusedIndex == 8,
onClick = onAutoPlayMinQualityClick
)
Spacer(modifier = Modifier.height(10.dp))
SettingsToggleRow(
title = "Trailer Auto-Play",
subtitle = "Play trailers in hero banner",
isEnabled = trailerAutoPlay,
isFocused = focusedIndex == 8,
isFocused = focusedIndex == 9,
onToggle = onTrailerAutoPlayToggle
)
Spacer(modifier = Modifier.height(10.dp))
Expand All @@ -2232,7 +2247,7 @@ private fun GeneralSettings(
title = "Match Frame Rate",
subtitle = "Off, Seamless, or Always",
value = frameRateMatchingMode,
isFocused = focusedIndex == 9,
isFocused = focusedIndex == 10,
onClick = onFrameRateMatchingClick
)

Expand All @@ -2250,7 +2265,7 @@ private fun GeneralSettings(
title = "Card Layout",
subtitle = "Landscape or poster cards",
value = cardLayoutMode,
isFocused = focusedIndex == 10,
isFocused = focusedIndex == 11,
onClick = onCardLayoutToggle
)
Spacer(modifier = Modifier.height(10.dp))
Expand All @@ -2264,15 +2279,15 @@ private fun GeneralSettings(
"phone" -> "Phone"
else -> "Auto"
},
isFocused = focusedIndex == 11,
isFocused = focusedIndex == 12,
onClick = onDeviceModeClick
)
Spacer(modifier = Modifier.height(10.dp))
SettingsToggleRow(
title = "Skip Profile Selection",
subtitle = "Auto-load last used profile",
isEnabled = skipProfileSelection,
isFocused = focusedIndex == 12,
isFocused = focusedIndex == 13,
onToggle = onSkipProfileSelectionToggle
)

Expand All @@ -2290,7 +2305,7 @@ private fun GeneralSettings(
title = "DNS Provider",
subtitle = "Resolve API and stream requests",
value = dnsProvider,
isFocused = focusedIndex == 13,
isFocused = focusedIndex == 14,
onClick = onDnsProviderClick
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ data class SettingsUiState(
val dnsProviderOptions: List<String> = listOf("System DNS", "Cloudflare", "Google", "AdGuard"),
val subtitleSize: String = "Medium",
val subtitleColor: String = "White",
val subtitleOffset: String = "Normal",
val trailerAutoPlay: Boolean = false,
val includeSpecials: Boolean = false,
val isLoggedIn: Boolean = false,
Expand Down Expand Up @@ -180,6 +181,7 @@ class SettingsViewModel @Inject constructor(

private fun subtitleSizeKey() = profileManager.profileStringKey("subtitle_size")
private fun subtitleColorKey() = profileManager.profileStringKey("subtitle_color")
private fun subtitleOffsetKey() = profileManager.profileStringKey("subtitle_offset")
private fun dnsProviderKey() = profileManager.profileStringKey("dns_provider")
private fun includeSpecialsKey() = profileManager.profileBooleanKey("include_specials")
private fun includeSpecialsKeyFor(profileId: String) = profileManager.profileBooleanKeyFor(profileId, "include_specials")
Expand Down Expand Up @@ -267,6 +269,7 @@ class SettingsViewModel @Inject constructor(

val subtitleSize = prefs[subtitleSizeKey()] ?: "Medium"
val subtitleColor = prefs[subtitleColorKey()] ?: "White"
val subtitleOffset = prefs[subtitleOffsetKey()] ?: "Normal"
val dnsProviderValue = normalizeDnsProviderValue(prefs[dnsProviderKey()])
val includeSpecials = prefs[includeSpecialsKey()] ?: false

Expand Down Expand Up @@ -306,6 +309,7 @@ class SettingsViewModel @Inject constructor(

subtitleSize = subtitleSize,
subtitleColor = subtitleColor,
subtitleOffset = subtitleOffset,
dnsProvider = dnsProviderLabel(dnsProviderValue),
includeSpecials = includeSpecials,
isLoggedIn = isLoggedIn,
Expand Down Expand Up @@ -793,6 +797,11 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { context.settingsDataStore.edit { it[subtitleColorKey()] = next }; _uiState.value = _uiState.value.copy(subtitleColor = next); syncLocalStateToCloud(silent = true) }
}

fun cycleSubtitleOffset() {
val next = when (_uiState.value.subtitleOffset) { "Lowest" -> "Lower"; "Lower" -> "Normal"; "Normal" -> "Higher"; "Higher" -> "Highest"; else -> "Lowest" }
viewModelScope.launch { context.settingsDataStore.edit { it[subtitleOffsetKey()] = next }; _uiState.value = _uiState.value.copy(subtitleOffset = next); syncLocalStateToCloud(silent = true) }
}

private fun normalizeDnsProviderValue(raw: String?): String {
return when (raw?.trim()?.lowercase()) {
"system", "system dns", "system_dns" -> "system"
Expand Down
Loading