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 fdfe35b07..31a658ee2 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 @@ -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" @@ -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() ) 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 c27188fe9..0d6912c96 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 @@ -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 @@ -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( 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 86b1b166c..b8d8e7e03 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 @@ -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 @@ -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 @@ -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 @@ -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, @@ -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, @@ -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, @@ -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, @@ -2129,6 +2134,7 @@ private fun GeneralSettings( trailerAutoPlay: Boolean = false, onSubtitleSizeClick: () -> Unit = {}, onSubtitleColorClick: () -> Unit = {}, + onSubtitleOffsetClick: () -> Unit = {}, onTrailerAutoPlayToggle: (Boolean) -> Unit = {} ) { Column { @@ -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)) @@ -2198,7 +2213,7 @@ 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)) @@ -2206,7 +2221,7 @@ private fun GeneralSettings( 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)) @@ -2215,7 +2230,7 @@ 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)) @@ -2223,7 +2238,7 @@ private fun GeneralSettings( 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)) @@ -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 ) @@ -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)) @@ -2264,7 +2279,7 @@ private fun GeneralSettings( "phone" -> "Phone" else -> "Auto" }, - isFocused = focusedIndex == 11, + isFocused = focusedIndex == 12, onClick = onDeviceModeClick ) Spacer(modifier = Modifier.height(10.dp)) @@ -2272,7 +2287,7 @@ private fun GeneralSettings( title = "Skip Profile Selection", subtitle = "Auto-load last used profile", isEnabled = skipProfileSelection, - isFocused = focusedIndex == 12, + isFocused = focusedIndex == 13, onToggle = onSkipProfileSelectionToggle ) @@ -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 ) } 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 820cc2c46..119057b55 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 @@ -75,6 +75,7 @@ data class SettingsUiState( val dnsProviderOptions: List = 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, @@ -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") @@ -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 @@ -306,6 +309,7 @@ class SettingsViewModel @Inject constructor( subtitleSize = subtitleSize, subtitleColor = subtitleColor, + subtitleOffset = subtitleOffset, dnsProvider = dnsProviderLabel(dnsProviderValue), includeSpecials = includeSpecials, isLoggedIn = isLoggedIn, @@ -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"