From 46747268eb1a450f4847dd19c8454b93f6653f5e Mon Sep 17 00:00:00 2001 From: Arvin Date: Sun, 5 Apr 2026 15:14:38 +0200 Subject: [PATCH 1/3] feat: Show Budget on Home toggle in Settings (#72) The movie budget line on the home hero banner makes the metadata row noisy, particularly on small screens where it pushes release date, runtime, and rating onto a second line. This adds a new Settings toggle (General > Show Budget on Home, default ON) so users who don't care about movie budgets can hide the field without losing the rest of the hero metadata. Scope of this feature was one of the multi-part requests in #72. Changes: - HomeUiState: new `showBudget: Boolean = true` field. - HomeViewModel.init: loads the new `_show_budget_on_home` key from DataStore alongside the existing trailer_auto_play load, with a default of true so existing users see no change until they explicitly disable it. - HomeScreen: wraps the existing "Budget $budgetText" Text (and its preceding `|` separator) in `if (uiState.showBudget && ...)`. No other visual changes. - SettingsUiState: new `showBudget: Boolean = true` field. - SettingsViewModel: new `showBudgetKey()` helper, loaded into UI state, and `setShowBudget(enabled)` mutator that persists + triggers cloud sync (matches the trailerAutoPlay pattern exactly). - SettingsScreen: new `SettingsToggleRow` for "Show Budget on Home" inserted at focusedIndex == 13 (immediately after "Skip Profile Selection" and before the Network section). DNS Provider shifted from focusedIndex == 13 to focusedIndex == 14, and the max-index clamp in BOTH the auto-scroll LaunchedEffect and the D-pad-down handler bumped from 13 to 14 so DNS Provider remains reachable via remote navigation. The Enter-handler switch now maps 13 -> setShowBudget and 14 -> openDnsProviderPicker. This is the recurring "settings row focus index" footgun that burned PRs #110 and #112 previously \u2014 I've updated both the dynamic max-index AND the scroll auto-scroll max AND the per-index action switch in the same commit. - CloudSyncRepository: adds `showBudget` to the `CloudProfileSettings` data class, `showBudgetKeyFor(profileId)` helper, and push/pull wiring so the setting syncs across devices via the existing account_sync_state snapshot path (the same plumbing that handles trailer_auto_play). Closes #72 (Show Budget part). The other two parts of #72 (auto-hide top bar, native debrid manager) are scoped separately as larger features and are not in this PR. --- .../tv/data/repository/CloudSyncRepository.kt | 5 ++++ .../arflix/tv/ui/screens/home/HomeScreen.kt | 5 +++- .../tv/ui/screens/home/HomeViewModel.kt | 16 ++++++++++-- .../tv/ui/screens/settings/SettingsScreen.kt | 25 ++++++++++++++++--- .../ui/screens/settings/SettingsViewModel.kt | 12 +++++++++ 5 files changed, 56 insertions(+), 7 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 224d97b8d..56bb65f4f 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 @@ -59,6 +59,7 @@ class CloudSyncRepository @Inject constructor( val autoPlaySingleSource: Boolean = true, val autoPlayMinQuality: String = "Any", val trailerAutoPlay: Boolean = false, + val showBudget: Boolean = true, 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 showBudgetKeyFor(profileId: String) = + profileManager.profileBooleanKeyFor(profileId, "show_budget_on_home") private fun subtitleSizeKeyFor(profileId: String) = profileManager.profileStringKeyFor(profileId, "subtitle_size") @@ -151,6 +154,7 @@ class CloudSyncRepository @Inject constructor( contentLanguage = prefs[contentLanguageKeyFor(profile.id)] ?: "en-US", trailerAutoPlay = prefs[trailerAutoPlayKeyFor(profile.id)] ?: false, + showBudget = prefs[showBudgetKeyFor(profile.id)] ?: true, subtitleSize = prefs[subtitleSizeKeyFor(profile.id)] ?: "Medium", subtitleColor = prefs[subtitleColorKeyFor(profile.id)] ?: "White", iptvHiddenGroups = prefs[iptvHiddenGroupsKeyFor(profile.id)] ?: "", @@ -370,6 +374,7 @@ class CloudSyncRepository @Inject constructor( prefs[contentLanguageKeyFor(profileId)] = state.contentLanguage prefs[trailerAutoPlayKeyFor(profileId)] = state.trailerAutoPlay + prefs[showBudgetKeyFor(profileId)] = state.showBudget 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/screens/home/HomeScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt index d5f18b5c0..e4d818f1f 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 @@ -994,7 +994,10 @@ private fun HeroSection( } } - if (!budgetText.isNullOrBlank()) { + // Budget line can be hidden via Settings → General → Show Budget on Home. + // Default is shown (uiState.showBudget = true) to preserve existing behavior. + // Issue #72. + if (uiState.showBudget && !budgetText.isNullOrBlank()) { if (displayDate.isNotEmpty() || hasGenre || hasDuration || ratingValue > 0f) { Text( text = "|", 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..4414be9e6 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 @@ -66,6 +66,8 @@ data class HomeUiState( val heroLogoUrl: String? = null, val heroTrailerKey: String? = null, val trailerAutoPlay: Boolean = false, + // Home hero metadata visibility toggles (issue #72) + val showBudget: Boolean = true, val heroOverviewOverride: String? = null, val cardLogoUrls: Map = emptyMap(), // Previous hero for crossfade (Phase 2.1) @@ -640,7 +642,7 @@ class HomeViewModel @Inject constructor( } init { - // Load trailer auto-play setting + // Load trailer auto-play and show-budget settings viewModelScope.launch { try { val prefs = context.settingsDataStore.data.first() @@ -648,7 +650,17 @@ 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) + // show_budget_on_home defaults to TRUE so existing users see no change + // until they explicitly disable it. We check any active-profile key; if + // none exist yet the default of true is preserved. Issue #72. + val showBudgetExplicit = prefs.asMap().entries + .firstOrNull { (key, _) -> key.name.endsWith("_show_budget_on_home") } + ?.value as? Boolean + val showBudget = showBudgetExplicit ?: true + _uiState.value = _uiState.value.copy( + trailerAutoPlay = trailerEnabled, + showBudget = showBudget + ) } 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 d6f96b516..33877bc3d 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 (added Show Budget toggle for #72) 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 (added Show Budget toggle for #72) 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 @@ -486,7 +486,8 @@ fun SettingsScreen( 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() + 13 -> viewModel.setShowBudget(!uiState.showBudget) + 14 -> openDnsProviderPicker() } } 1 -> { // IPTV @@ -615,6 +616,7 @@ fun SettingsScreen( subtitleColor = uiState.subtitleColor, deviceModeOverride = uiState.deviceModeOverride, skipProfileSelection = uiState.skipProfileSelection, + showBudget = uiState.showBudget, focusedIndex = -1, onSubtitleClick = openSubtitlePicker, onAudioLanguageClick = openAudioLanguagePicker, @@ -633,6 +635,7 @@ fun SettingsScreen( onContentLanguageClick = openContentLanguagePicker, onSubtitleSizeClick = { viewModel.cycleSubtitleSize() }, onSkipProfileSelectionToggle = { viewModel.setSkipProfileSelection(it) }, + onShowBudgetToggle = { viewModel.setShowBudget(it) }, onSubtitleColorClick = { viewModel.cycleSubtitleColor() } ) "iptv" -> IptvSettings( @@ -787,6 +790,7 @@ fun SettingsScreen( subtitleColor = uiState.subtitleColor, deviceModeOverride = uiState.deviceModeOverride, skipProfileSelection = uiState.skipProfileSelection, + showBudget = uiState.showBudget, focusedIndex = if (activeZone == Zone.CONTENT) contentFocusIndex else -1, onSubtitleClick = openSubtitlePicker, onAudioLanguageClick = openAudioLanguagePicker, @@ -804,6 +808,7 @@ fun SettingsScreen( }, onContentLanguageClick = openContentLanguagePicker, onSkipProfileSelectionToggle = { viewModel.setSkipProfileSelection(it) }, + onShowBudgetToggle = { viewModel.setShowBudget(it) }, onSubtitleSizeClick = { viewModel.cycleSubtitleSize() }, onSubtitleColorClick = { viewModel.cycleSubtitleColor() } ) @@ -2115,6 +2120,7 @@ private fun GeneralSettings( subtitleColor: String = "White", deviceModeOverride: String = "auto", skipProfileSelection: Boolean = false, + showBudget: Boolean = true, focusedIndex: Int, onSubtitleClick: () -> Unit, onAudioLanguageClick: () -> Unit, @@ -2127,6 +2133,7 @@ private fun GeneralSettings( onDeviceModeClick: () -> Unit = {}, onContentLanguageClick: () -> Unit = {}, onSkipProfileSelectionToggle: (Boolean) -> Unit = {}, + onShowBudgetToggle: (Boolean) -> Unit = {}, trailerAutoPlay: Boolean = false, onSubtitleSizeClick: () -> Unit = {}, onSubtitleColorClick: () -> Unit = {}, @@ -2276,6 +2283,16 @@ private fun GeneralSettings( isFocused = focusedIndex == 12, onToggle = onSkipProfileSelectionToggle ) + Spacer(modifier = Modifier.height(10.dp)) + // Home hero controls — issue #72. The movie Budget line on the hero banner + // makes the metadata row noisy on small screens and some users want to hide it. + SettingsToggleRow( + title = "Show Budget on Home", + subtitle = "Display the movie budget on the home hero banner", + isEnabled = showBudget, + isFocused = focusedIndex == 13, + onToggle = onShowBudgetToggle + ) // ── Network ── Spacer(modifier = Modifier.height(24.dp)) @@ -2291,7 +2308,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 1ce2c2c27..698e3e351 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 @@ -77,6 +77,7 @@ data class SettingsUiState( val subtitleSize: String = "Medium", val subtitleColor: String = "White", val trailerAutoPlay: Boolean = false, + val showBudget: Boolean = true, val includeSpecials: Boolean = false, val isLoggedIn: Boolean = false, val accountEmail: String? = null, @@ -178,6 +179,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 showBudgetKey() = profileManager.profileBooleanKey("show_budget_on_home") private fun subtitleSizeKey() = profileManager.profileStringKey("subtitle_size") private fun subtitleColorKey() = profileManager.profileStringKey("subtitle_color") @@ -265,6 +267,7 @@ class SettingsViewModel @Inject constructor( } val autoPlayMinQuality = normalizeAutoPlayMinQuality(prefs[autoPlayMinQualityKey()]) val trailerAutoPlay = prefs[trailerAutoPlayKey()] ?: false + val showBudget = prefs[showBudgetKey()] ?: true val subtitleSize = prefs[subtitleSizeKey()] ?: "Medium" val subtitleColor = prefs[subtitleColorKey()] ?: "White" @@ -304,6 +307,7 @@ class SettingsViewModel @Inject constructor( autoPlaySingleSource = autoPlaySingleSource, autoPlayMinQuality = autoPlayMinQuality, trailerAutoPlay = trailerAutoPlay, + showBudget = showBudget, subtitleSize = subtitleSize, subtitleColor = subtitleColor, @@ -784,6 +788,14 @@ class SettingsViewModel @Inject constructor( viewModelScope.launch { context.settingsDataStore.edit { it[trailerAutoPlayKey()] = enabled }; _uiState.value = _uiState.value.copy(trailerAutoPlay = enabled); syncLocalStateToCloud(silent = true) } } + fun setShowBudget(enabled: Boolean) { + viewModelScope.launch { + context.settingsDataStore.edit { it[showBudgetKey()] = enabled } + _uiState.value = _uiState.value.copy(showBudget = 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) } From a65f26ff6353a8cf4a8997408a8377e5673a75f5 Mon Sep 17 00:00:00 2001 From: Arvin Date: Sun, 5 Apr 2026 15:39:09 +0200 Subject: [PATCH 2/3] fix: pass showBudget through to HeroSection composable (scope fix) --- .../com/arflix/tv/ui/screens/home/HomeScreen.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 e4d818f1f..c1b99da1e 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 @@ -733,6 +733,11 @@ private fun HeroSection( item: MediaItem, logoUrl: String?, overviewOverride: String? = null, + // Hide the Budget line on the hero metadata row when false. Plumbed from + // HomeUiState.showBudget, which is loaded from the per-profile + // `show_budget_on_home` DataStore key and defaults to true so existing + // users see no behavior change. Issue #72. + showBudget: Boolean = true, modifier: Modifier = Modifier ) { val context = LocalContext.current @@ -994,10 +999,10 @@ private fun HeroSection( } } - // Budget line can be hidden via Settings → General → Show Budget on Home. - // Default is shown (uiState.showBudget = true) to preserve existing behavior. + // Budget line can be hidden via Settings -> General -> Show Budget on Home. + // Default is shown (showBudget = true) to preserve existing behavior. // Issue #72. - if (uiState.showBudget && !budgetText.isNullOrBlank()) { + if (showBudget && !budgetText.isNullOrBlank()) { if (displayDate.isNotEmpty() || hasGenre || hasDuration || ratingValue > 0f) { Text( text = "|", @@ -1101,6 +1106,7 @@ private fun HomeHeroLayer( item = item, logoUrl = heroLogoUrl, overviewOverride = heroOverviewOverride, + showBudget = uiState.showBudget, modifier = Modifier .align(Alignment.BottomStart) .padding(start = contentStartPadding, end = 400.dp) From ee8740888319826f4b89053ab8adaf93d2102ee2 Mon Sep 17 00:00:00 2001 From: Arvin Date: Sun, 5 Apr 2026 15:42:21 +0200 Subject: [PATCH 3/3] fix: thread showBudget through HomeHeroLayer too (second scope fix) --- .../main/kotlin/com/arflix/tv/ui/screens/home/HomeScreen.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 c1b99da1e..c3298f44a 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 @@ -625,6 +625,7 @@ fun HomeScreen( heroOverviewOverride = displayHeroOverview, contentStartPadding = contentStartPadding, isMobile = isMobile, + showBudget = uiState.showBudget, onNavigateToDetails = onNavigateToDetails, onNavigateToTv = { channelId, streamUrl -> onNavigateToTv(channelId, streamUrl) }, isIptvItem = { item -> viewModel.isIptvItem(item) }, @@ -1078,6 +1079,7 @@ private fun HomeHeroLayer( heroOverviewOverride: String?, contentStartPadding: androidx.compose.ui.unit.Dp, isMobile: Boolean = false, + showBudget: Boolean = true, onNavigateToDetails: (MediaType, Int, Int?, Int?) -> Unit = { _, _, _, _ -> }, onNavigateToTv: (channelId: String?, streamUrl: String?) -> Unit = { _, _ -> }, isIptvItem: (MediaItem) -> Boolean = { false }, @@ -1106,7 +1108,7 @@ private fun HomeHeroLayer( item = item, logoUrl = heroLogoUrl, overviewOverride = heroOverviewOverride, - showBudget = uiState.showBudget, + showBudget = showBudget, modifier = Modifier .align(Alignment.BottomStart) .padding(start = contentStartPadding, end = 400.dp)