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 224d97b8d..c50d1a483 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 @@ -57,8 +57,9 @@ class CloudSyncRepository @Inject constructor( val frameRateMatchingMode: String = "Off", val autoPlayNext: Boolean = true, val autoPlaySingleSource: Boolean = true, - val autoPlayMinQuality: String = "Any", + val autoPlayMinQuality: String = "Any", // Ensure this name is exact val trailerAutoPlay: Boolean = false, + val trailerAudioEnabled: Boolean = false, val includeSpecials: Boolean = false, val iptvHiddenGroups: String = "", val iptvGroupOrder: String = "" @@ -70,6 +71,8 @@ class CloudSyncRepository @Inject constructor( profileManager.profileStringKeyFor(profileId, "content_language") private fun trailerAutoPlayKeyFor(profileId: String) = profileManager.profileBooleanKeyFor(profileId, "trailer_auto_play") + private fun trailerAudioEnabledKeyFor(profileId: String) = + profileManager.profileBooleanKeyFor(profileId, "trailer_audio_enabled") private fun subtitleSizeKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "subtitle_size") @@ -149,8 +152,8 @@ class CloudSyncRepository @Inject constructor( defaultSubtitle = prefs[defaultSubtitleKeyFor(profile.id)] ?: "Off", defaultAudioLanguage = prefs[defaultAudioLanguageKeyFor(profile.id)] ?: "Auto (Original)", contentLanguage = prefs[contentLanguageKeyFor(profile.id)] ?: "en-US", - trailerAutoPlay = prefs[trailerAutoPlayKeyFor(profile.id)] ?: false, + trailerAudioEnabled = prefs[trailerAudioEnabledKeyFor(profile.id)] ?: false, subtitleSize = prefs[subtitleSizeKeyFor(profile.id)] ?: "Medium", subtitleColor = prefs[subtitleColorKeyFor(profile.id)] ?: "White", iptvHiddenGroups = prefs[iptvHiddenGroupsKeyFor(profile.id)] ?: "", @@ -368,8 +371,8 @@ class CloudSyncRepository @Inject constructor( prefs[defaultSubtitleKeyFor(profileId)] = state.defaultSubtitle prefs[defaultAudioLanguageKeyFor(profileId)] = state.defaultAudioLanguage prefs[contentLanguageKeyFor(profileId)] = state.contentLanguage - prefs[trailerAutoPlayKeyFor(profileId)] = state.trailerAutoPlay + prefs[trailerAudioEnabledKeyFor(profileId)] = state.trailerAudioEnabled prefs[subtitleSizeKeyFor(profileId)] = state.subtitleSize prefs[subtitleColorKeyFor(profileId)] = state.subtitleColor if (state.iptvHiddenGroups.isNotBlank()) prefs[iptvHiddenGroupsKeyFor(profileId)] = state.iptvHiddenGroups diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt index c22f79b1b..579bcaa88 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/TrailerPlayer.kt @@ -38,7 +38,8 @@ import kotlinx.coroutines.withContext fun TrailerPlayer( youtubeKey: String, modifier: Modifier = Modifier, - delayMs: Long = 3000L + delayMs: Long = 3000L, + isMuted: Boolean = true ) { val context = LocalContext.current var shouldPlay by remember { mutableStateOf(false) } @@ -72,7 +73,7 @@ fun TrailerPlayer( ) { val player = remember(youtubeKey) { ExoPlayer.Builder(context).build().apply { - volume = 0.5f // Half volume for background trailer + volume = if (isMuted) 0f else 0.5f // Muted or half volume for background trailer repeatMode = Player.REPEAT_MODE_ONE playWhenReady = true } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt index d5f18b5c0..53612f413 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt @@ -556,6 +556,7 @@ fun HomeScreen( if (heroVideoUrl == null && uiState.trailerAutoPlay && uiState.heroTrailerKey != null) { TrailerPlayer( youtubeKey = uiState.heroTrailerKey!!, + isMuted = !uiState.trailerAudioEnabled, modifier = Modifier.fillMaxSize() ) } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt index 5f85353d1..19e7fe36f 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeViewModel.kt @@ -1,5 +1,6 @@ package com.arflix.tv.ui.screens.home +import com.arflix.tv.data.repository.ProfileManager import android.app.ActivityManager import android.content.Context import com.arflix.tv.util.settingsDataStore @@ -66,6 +67,7 @@ data class HomeUiState( val heroLogoUrl: String? = null, val heroTrailerKey: String? = null, val trailerAutoPlay: Boolean = false, + val trailerAudioEnabled: Boolean = false, val heroOverviewOverride: String? = null, val cardLogoUrls: Map = emptyMap(), // Previous hero for crossfade (Phase 2.1) @@ -96,6 +98,7 @@ class HomeViewModel @Inject constructor( private val watchlistRepository: WatchlistRepository, private val cloudSyncRepository: CloudSyncRepository, private val launcherContinueWatchingRepository: LauncherContinueWatchingRepository, + private val profileManager: ProfileManager, @ApplicationContext private val context: Context ) : ViewModel() { private val imageLoader: ImageLoader by lazy(LazyThreadSafetyMode.NONE) { @@ -648,7 +651,14 @@ class HomeViewModel @Inject constructor( val trailerEnabled = prefs.asMap().any { (key, value) -> key.name.endsWith("_trailer_auto_play") && value == true } - _uiState.value = _uiState.value.copy(trailerAutoPlay = trailerEnabled) + + // Scope trailer audio strictly to the active profile + val trailerAudio = prefs[profileManager.profileBooleanKey("trailer_audio_enabled")] ?: false + + _uiState.value = _uiState.value.copy( + trailerAutoPlay = trailerEnabled, + trailerAudioEnabled = trailerAudio + ) } catch (_: Exception) {} } // Restore logo URL cache from disk for instant clearlogos on cold start 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..9dd1f8ed5 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 @@ -482,11 +482,12 @@ fun SettingsScreen( 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() + 9 -> viewModel.setTrailerAudioEnabled(!uiState.trailerAudioEnabled) + 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 @@ -625,7 +626,9 @@ fun SettingsScreen( onAutoPlaySingleSourceToggle = { viewModel.setAutoPlaySingleSource(it) }, onAutoPlayMinQualityClick = { viewModel.cycleAutoPlayMinQuality() }, trailerAutoPlay = uiState.trailerAutoPlay, + trailerAudioEnabled = uiState.trailerAudioEnabled, onTrailerAutoPlayToggle = { viewModel.setTrailerAutoPlay(it) }, + onTrailerAudioToggle = { viewModel.setTrailerAudioEnabled(it) }, onDeviceModeClick = { val next = when (uiState.deviceModeOverride) { "auto" -> "tv"; "tv" -> "tablet"; "tablet" -> "phone"; else -> "auto" } viewModel.setDeviceModeOverride(next) @@ -797,7 +800,9 @@ fun SettingsScreen( onAutoPlaySingleSourceToggle = { viewModel.setAutoPlaySingleSource(it) }, onAutoPlayMinQualityClick = { viewModel.cycleAutoPlayMinQuality() }, trailerAutoPlay = uiState.trailerAutoPlay, + trailerAudioEnabled = uiState.trailerAudioEnabled, onTrailerAutoPlayToggle = { viewModel.setTrailerAutoPlay(it) }, + onTrailerAudioToggle = { viewModel.setTrailerAudioEnabled(it) }, onDeviceModeClick = { val next = when (uiState.deviceModeOverride) { "auto" -> "tv"; "tv" -> "tablet"; "tablet" -> "phone"; else -> "auto" } viewModel.setDeviceModeOverride(next) @@ -2127,9 +2132,11 @@ private fun GeneralSettings( onContentLanguageClick: () -> Unit = {}, onSkipProfileSelectionToggle: (Boolean) -> Unit = {}, trailerAutoPlay: Boolean = false, + trailerAudioEnabled: Boolean = false, onSubtitleSizeClick: () -> Unit = {}, onSubtitleColorClick: () -> Unit = {}, - onTrailerAutoPlayToggle: (Boolean) -> Unit = {} + onTrailerAutoPlayToggle: (Boolean) -> Unit = {}, + onTrailerAudioToggle: (Boolean) -> Unit = {} ) { Column { // ── Language & Subtitles ── @@ -2227,12 +2234,20 @@ private fun GeneralSettings( onToggle = onTrailerAutoPlayToggle ) Spacer(modifier = Modifier.height(10.dp)) + SettingsToggleRow( + title = "Trailer Audio", + subtitle = "Enable sound for banners", + isEnabled = trailerAudioEnabled, + isFocused = focusedIndex == 9, + onToggle = onTrailerAudioToggle + ) + Spacer(modifier = Modifier.height(10.dp)) SettingsRow( icon = Icons.Default.Movie, 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..09a8d6861 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 @@ -76,6 +76,7 @@ data class SettingsUiState( val subtitleSize: String = "Medium", val subtitleColor: String = "White", val trailerAutoPlay: Boolean = false, + val trailerAudioEnabled: Boolean = false, val includeSpecials: Boolean = false, val isLoggedIn: Boolean = false, val accountEmail: String? = null, @@ -177,6 +178,7 @@ class SettingsViewModel @Inject constructor( private fun autoPlayMinQualityKey() = profileManager.profileStringKey("auto_play_min_quality") private fun autoPlayMinQualityKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "auto_play_min_quality") private fun trailerAutoPlayKey() = profileManager.profileBooleanKey("trailer_auto_play") + private fun trailerAudioEnabledKey() = profileManager.profileBooleanKey("trailer_audio_enabled") private fun subtitleSizeKey() = profileManager.profileStringKey("subtitle_size") private fun subtitleColorKey() = profileManager.profileStringKey("subtitle_color") @@ -264,6 +266,7 @@ class SettingsViewModel @Inject constructor( } val autoPlayMinQuality = normalizeAutoPlayMinQuality(prefs[autoPlayMinQualityKey()]) val trailerAutoPlay = prefs[trailerAutoPlayKey()] ?: false + val trailerAudioEnabled = prefs[trailerAudioEnabledKey()] ?: false val subtitleSize = prefs[subtitleSizeKey()] ?: "Medium" val subtitleColor = prefs[subtitleColorKey()] ?: "White" @@ -303,6 +306,7 @@ class SettingsViewModel @Inject constructor( autoPlaySingleSource = autoPlaySingleSource, autoPlayMinQuality = autoPlayMinQuality, trailerAutoPlay = trailerAutoPlay, + trailerAudioEnabled = trailerAudioEnabled, subtitleSize = subtitleSize, subtitleColor = subtitleColor, @@ -783,6 +787,10 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { context.settingsDataStore.edit { it[trailerAutoPlayKey()] = enabled }; _uiState.value = _uiState.value.copy(trailerAutoPlay = enabled); syncLocalStateToCloud(silent = true) } } + fun setTrailerAudioEnabled(enabled: Boolean) { + viewModelScope.launch { context.settingsDataStore.edit { it[trailerAudioEnabledKey()] = enabled }; _uiState.value = _uiState.value.copy(trailerAudioEnabled = enabled); syncLocalStateToCloud(silent = true) } + } + fun cycleSubtitleSize() { val next = when (_uiState.value.subtitleSize) { "Small" -> "Medium"; "Medium" -> "Large"; "Large" -> "Extra Large"; else -> "Small" } viewModelScope.launch { context.settingsDataStore.edit { it[subtitleSizeKey()] = next }; _uiState.value = _uiState.value.copy(subtitleSize = next); syncLocalStateToCloud(silent = true) }