diff --git a/app/src/main/kotlin/com/arflix/tv/MainActivity.kt b/app/src/main/kotlin/com/arflix/tv/MainActivity.kt index 9601129b..8db0a96c 100644 --- a/app/src/main/kotlin/com/arflix/tv/MainActivity.kt +++ b/app/src/main/kotlin/com/arflix/tv/MainActivity.kt @@ -65,7 +65,7 @@ import com.arflix.tv.util.DeviceType import com.arflix.tv.util.DEVICE_MODE_OVERRIDE_KEY import com.arflix.tv.util.SKIP_PROFILE_SELECTION_KEY import com.arflix.tv.util.OLED_BLACK_BACKGROUND_KEY -import com.arflix.tv.util.FOCUS_BORDER_COLOR_KEY +import com.arflix.tv.util.ACCENT_COLOR_KEY import com.arflix.tv.util.LocalDeviceType import com.arflix.tv.util.LocalHasTouchScreen import com.arflix.tv.util.LocalAppLanguage @@ -266,8 +266,8 @@ class MainActivity : ComponentActivity() { val oledBlackBackground by remember { this@MainActivity.settingsDataStore.data.map { it[OLED_BLACK_BACKGROUND_KEY] ?: false } }.collectAsStateWithLifecycle(initialValue = false) - val focusBorderColorName by remember { - this@MainActivity.settingsDataStore.data.map { it[FOCUS_BORDER_COLOR_KEY] } + val accentColorName by remember { + this@MainActivity.settingsDataStore.data.map { it[ACCENT_COLOR_KEY] } }.collectAsStateWithLifecycle(initialValue = null) val activeProfileId by remember { profileRepository.get().activeProfileId @@ -324,7 +324,7 @@ class MainActivity : ComponentActivity() { ) { ArflixTvTheme( oledBlackBackground = oledBlackBackground, - focusBorderColorName = focusBorderColorName + accentColorName = accentColorName ) { val startupState by startupViewModel.state.collectAsStateWithLifecycle() ArflixApp( 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 4cbc421c..abd107f7 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 @@ -16,7 +16,7 @@ import com.arflix.tv.ui.components.normalizeCardLayoutMode import com.arflix.tv.ui.components.profileCatalogueRowLayoutModeKey import com.arflix.tv.util.LAST_APP_LANGUAGE_KEY import com.arflix.tv.util.AppLogger -import com.arflix.tv.util.FOCUS_BORDER_COLOR_KEY +import com.arflix.tv.util.ACCENT_COLOR_KEY import com.arflix.tv.util.OLED_BLACK_BACKGROUND_KEY import com.arflix.tv.util.SKIP_PROFILE_SELECTION_KEY import com.arflix.tv.util.settingsDataStore @@ -396,7 +396,9 @@ class CloudSyncRepository @Inject constructor( root.put("dnsProvider", globalDnsProvider) root.put("customUserAgent", prefs[customUserAgentKey] ?: "") root.put("oledBlackBackground", prefs[OLED_BLACK_BACKGROUND_KEY] ?: false) - root.put("focusBorderColor", prefs[FOCUS_BORDER_COLOR_KEY] ?: "White") + val accentColor = prefs[ACCENT_COLOR_KEY] ?: "White" + root.put("accentColor", accentColor) + root.put("focusBorderColor", accentColor) root.put("subtitleUsageJson", prefs[subtitleUsageKey()] ?: "") root.put("subtitleSettingsUpdatedAt", prefs[subtitleSettingsUpdatedAtKey()]?.toLongOrNull() ?: 0L) root.put("skipProfileSelection", prefs[SKIP_PROFILE_SELECTION_KEY] ?: false) @@ -880,6 +882,7 @@ class CloudSyncRepository @Inject constructor( root.has("dnsProvider") || root.has("customUserAgent") || root.has("oledBlackBackground") || + root.has("accentColor") || root.has("focusBorderColor") ) { context.settingsDataStore.edit { prefs -> @@ -901,8 +904,11 @@ class CloudSyncRepository @Inject constructor( if (root.has("oledBlackBackground")) { prefs[OLED_BLACK_BACKGROUND_KEY] = root.optBoolean("oledBlackBackground", false) } - if (root.has("focusBorderColor")) { - prefs[FOCUS_BORDER_COLOR_KEY] = root.optString("focusBorderColor", "White").ifBlank { "White" } + if (root.has("accentColor") || root.has("focusBorderColor")) { + prefs[ACCENT_COLOR_KEY] = root.optString( + "accentColor", + root.optString("focusBorderColor", "White") + ).ifBlank { "White" } } } restoredDnsProvider?.let { OkHttpProvider.setDnsProvider(OkHttpProvider.parseDnsProvider(it)) } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt index 64d2a660..c7a5946c 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/AppTopBar.kt @@ -41,6 +41,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Text import com.arflix.tv.data.model.Profile import com.arflix.tv.ui.skin.ArvioSkin +import com.arflix.tv.ui.skin.resolveAccentColor import com.arflix.tv.ui.theme.AnimationConstants import com.arflix.tv.ui.theme.ArflixTypography import androidx.compose.ui.res.stringResource @@ -193,6 +194,8 @@ private fun TopBarNavChip( isFocused: Boolean, isSelected: Boolean ) { + val accent = resolveAccentColor(fallback = Color.White) + val containerColor by animateColorAsState( targetValue = when { isFocused -> Color.White.copy(alpha = 0.2f) @@ -204,8 +207,8 @@ private fun TopBarNavChip( ) val iconColor by animateColorAsState( targetValue = when { - isFocused -> Color.White - isSelected -> Color.White.copy(alpha = 0.92f) + isFocused -> Color.White // focused icon stays white (wins over selected) + isSelected -> accent // selected icon gets accent else -> Color.White.copy(alpha = 0.62f) }, animationSpec = tween(AnimationConstants.DURATION_FAST), @@ -213,8 +216,8 @@ private fun TopBarNavChip( ) val textColor by animateColorAsState( targetValue = when { - isFocused -> Color.White - isSelected -> Color.White.copy(alpha = 0.92f) + isFocused -> Color.White // focused text stays white (wins over selected) + isSelected -> accent // selected text gets accent else -> Color.White.copy(alpha = 0.68f) }, animationSpec = tween(AnimationConstants.DURATION_FAST), @@ -270,10 +273,12 @@ private fun TopBarSettingsGear( isSelected: Boolean, hasBadge: Boolean = false ) { + val accent = resolveAccentColor(fallback = Color.White) + val iconColor by animateColorAsState( targetValue = when { - isFocused -> Color.White - isSelected -> Color.White.copy(alpha = 0.92f) + isFocused -> Color.White // focused stays white (wins over selected) + isSelected -> accent // selected settings gear gets accent else -> Color.White.copy(alpha = 0.5f) }, animationSpec = tween(AnimationConstants.DURATION_FAST), diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/PersonModal.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/PersonModal.kt index 924a8162..f1f88d9a 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/PersonModal.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/PersonModal.kt @@ -47,7 +47,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key -import com.arflix.tv.ui.skin.resolveFocusBorderColor +import com.arflix.tv.ui.skin.resolveAccentColor import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent @@ -545,7 +545,7 @@ private fun HorizontalKnownForCard( ) { val cardWidth = 220.dp val cardHeight = 124.dp // 16:9 aspect ratio - val focusRingColor = resolveFocusBorderColor(fallback = Color.White) + val focusRingColor = resolveAccentColor(fallback = Color.White) Column( modifier = Modifier.width(cardWidth), diff --git a/app/src/main/kotlin/com/arflix/tv/ui/components/Sidebar.kt b/app/src/main/kotlin/com/arflix/tv/ui/components/Sidebar.kt index 8c0ad091..021c8c7e 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/components/Sidebar.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/components/Sidebar.kt @@ -46,6 +46,7 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Text import com.arflix.tv.data.model.Profile import com.arflix.tv.ui.skin.ArvioSkin +import com.arflix.tv.ui.skin.resolveAccentColor import com.arflix.tv.ui.theme.AnimationConstants import androidx.annotation.StringRes import androidx.compose.ui.res.stringResource @@ -211,11 +212,13 @@ private fun SidebarIcon( isFocused: Boolean, hasBadge: Boolean = false ) { - // Animated icon color - dark grey normally, pure white when focused + val accent = resolveAccentColor(fallback = Color.White) + + // Animated icon color - white when focused, accent when selected-only val iconColor by animateColorAsState( targetValue = when { - isFocused -> Color.White // Pure white when focused - isSelected -> Color(0xFF666666) // Dark grey when selected + isFocused -> Color.White // white when D-pad navigating (wins over selected) + isSelected -> accent // ROYGBIV accent when selected (current screen) else -> Color(0xFF444444) // Darker grey when unfocused }, animationSpec = tween( diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt index c60ff423..eb68b2c5 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/details/DetailsScreen.kt @@ -160,6 +160,7 @@ import com.arflix.tv.ui.focus.rememberArvioDpadRepeatGate import com.arflix.tv.ui.skin.ArvioFocusableSurface import com.arflix.tv.ui.skin.ArvioSkin import com.arflix.tv.ui.skin.rememberArvioCardShape +import com.arflix.tv.ui.skin.resolveAccentColor import com.arflix.tv.ui.theme.AnimationConstants import com.arflix.tv.ui.theme.ArflixTypography import com.arflix.tv.ui.theme.BackgroundCard @@ -3138,20 +3139,27 @@ private fun PremiumActionButton( label = "button_scale" ) - // Animated background color - buttons only glow when focused + // Resolve accent color for focused button backgrounds + val accent = resolveAccentColor(fallback = Color.White) + + // Animated background color - button fills with accent when focused val backgroundColor by animateColorAsState( targetValue = when { - isFocused && isPrimary -> Color.White - isFocused -> Color.White.copy(alpha = 0.95f) + isFocused && isPrimary -> accent + isFocused -> accent else -> Color.Transparent }, animationSpec = tween(150), label = "button_bg" ) - // Animated text/icon color - black when focused (on white bg), white otherwise + // Animated text/icon color - white on accent bg when focused, white otherwise + // Use dark text for light accent colors (White, Yellow) to ensure contrast val contentColor by animateColorAsState( - targetValue = if (isFocused) Color.Black else Color.White.copy(alpha = 0.9f), + targetValue = if (isFocused) { + val l = 0.299f * accent.red + 0.587f * accent.green + 0.114f * accent.blue + if (l > 0.5f) Color.Black else Color.White + } else Color.White.copy(alpha = 0.9f), animationSpec = tween(150), label = "button_content" ) 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 2e0fbb5d..105ae099 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 @@ -146,7 +146,7 @@ import androidx.compose.ui.text.style.TextOverflow import com.arflix.tv.util.LocalDeviceType import com.arflix.tv.util.settingsDataStore import com.arflix.tv.util.weightedSubtitleScore -import com.arflix.tv.ui.skin.LocalFocusBorderColorOverride +import com.arflix.tv.ui.skin.LocalAccentColorOverride import com.arflix.tv.ui.theme.ArflixTypography import com.arflix.tv.ui.theme.Pink import com.arflix.tv.ui.theme.PurpleDark @@ -199,7 +199,7 @@ fun PlayerScreen( onBack: () -> Unit = {}, onPlayNext: (Int, Int, String?, String?, String?) -> Unit = { _, _, _, _, _ -> } ) { - val playerAccent = LocalFocusBorderColorOverride.current ?: Color.White + val playerAccent = LocalAccentColorOverride.current ?: Color.White val context = LocalContext.current val activity = remember(context) { context.findActivity() } val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -3181,7 +3181,7 @@ private fun PlayerIconButton( onUpKey: () -> Unit = {}, onDownKey: () -> Unit = {} ) { - val btnAccent = LocalFocusBorderColorOverride.current ?: Color.White + val btnAccent = LocalAccentColorOverride.current ?: Color.White var focused by remember { mutableStateOf(false) } val scale by animateFloatAsState(if (focused) 1.15f else 1f, label = "iconScale") @@ -3342,7 +3342,7 @@ private fun ErrorButton( isPrimary: Boolean, onClick: () -> Unit ) { - val btnAccent = LocalFocusBorderColorOverride.current ?: Color.White + val btnAccent = LocalAccentColorOverride.current ?: Color.White val scale by animateFloatAsState(if (isFocused) 1.05f else 1f, label = "scale") Box( @@ -4853,7 +4853,7 @@ private fun PlayerSubtitleSettingsPanel( onVerticalDecrease: () -> Unit, onVerticalIncrease: () -> Unit ) { - val accent = LocalFocusBorderColorOverride.current ?: Color.White + val accent = LocalAccentColorOverride.current ?: Color.White val absMs = if (syncOffsetMs < 0) -syncOffsetMs else syncOffsetMs val offsetLabel = if (syncOffsetMs == 0L) "0.0s" diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt index cf6b12c1..8b76322b 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/search/SearchScreen.kt @@ -90,7 +90,7 @@ import com.arflix.tv.ui.focus.arvioDpadFocusGroup import com.arflix.tv.ui.skin.ArvioFocusableSurface import com.arflix.tv.ui.skin.ArvioSkin import com.arflix.tv.ui.skin.rememberArvioCardShape -import com.arflix.tv.ui.skin.resolveFocusBorderColor +import com.arflix.tv.ui.skin.resolveAccentColor import com.arflix.tv.ui.theme.ArflixTypography import com.arflix.tv.ui.theme.BackgroundCard import com.arflix.tv.ui.theme.appBackgroundDark @@ -804,14 +804,14 @@ private fun GlowChip( val focused = isVisuallyFocused || (useSystemFocusForVisuals && systemFocused) val active = focused || isSelected val chipShape = RoundedCornerShape(7.dp) - val focusBorderColor = resolveFocusBorderColor(fallback = Color.White) + val accentColor = resolveAccentColor(fallback = Color.White) val backgroundColor = when { focused -> Color.White.copy(alpha = 0.12f) isSelected -> Color.White.copy(alpha = 0.92f) else -> Color.White.copy(alpha = 0.075f) } val borderColor = when { - focused -> focusBorderColor + focused -> accentColor isSelected -> Color.White.copy(alpha = 0.92f) else -> Color.White.copy(alpha = 0.24f) } 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 71d912e4..d75bb23f 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 @@ -162,7 +162,7 @@ import com.arflix.tv.ui.components.toggleCatalogueRowLayoutMode import com.arflix.tv.ui.components.topBarFocusedItem import com.arflix.tv.ui.components.topBarMaxIndex import com.arflix.tv.ui.focus.arvioDpadFocusGroup -import com.arflix.tv.ui.skin.resolveFocusBorderColor +import com.arflix.tv.ui.skin.resolveAccentColor import com.arflix.tv.ui.theme.ArflixTypography import com.arflix.tv.ui.theme.appBackgroundDark import com.arflix.tv.ui.theme.BackgroundElevated @@ -832,7 +832,7 @@ fun SettingsScreen( 21 -> viewModel.cycleClockFormat() 22 -> viewModel.setShowBudget(!uiState.showBudget) 23 -> viewModel.setSpoilerBlurEnabled(!uiState.spoilerBlurEnabled) - 24 -> viewModel.cycleFocusBorderColor() + 24 -> viewModel.cycleAccentColor() 25 -> openDnsProviderPicker() 26 -> viewModel.setShowLoadingStats(!uiState.showLoadingStats) 35 -> showCustomUserAgentDialog = true @@ -1241,8 +1241,8 @@ fun SettingsScreen( onShowBudgetToggle = { viewModel.setShowBudget(it) }, spoilerBlurEnabled = uiState.spoilerBlurEnabled, onSpoilerBlurToggle = { viewModel.setSpoilerBlurEnabled(it) }, - focusBorderColor = uiState.focusBorderColor, - onFocusBorderColorClick = { viewModel.cycleFocusBorderColor() }, + accentColor = uiState.accentColor, + onAccentColorClick = { viewModel.cycleAccentColor() }, showLoadingStats = uiState.showLoadingStats, onShowLoadingStatsToggle = { viewModel.setShowLoadingStats(it) }, onVolumeBoostClick = { viewModel.cycleVolumeBoost() }, @@ -2231,7 +2231,7 @@ private fun SettingsChip( isFocused: Boolean = false, modifier: Modifier = Modifier ) { - val chipFocusColor = resolveFocusBorderColor(fallback = Color.White) + val chipFocusColor = resolveAccentColor(fallback = Color.White) Box( modifier = modifier .clip(RoundedCornerShape(10.dp)) @@ -3670,11 +3670,11 @@ private fun MobileSettingsSubPage( ) MobileSettingsRow( icon = Icons.Default.Palette, - title = stringResource(R.string.focus_border_color), - value = uiState.focusBorderColor, + title = stringResource(R.string.accent_color), + value = uiState.accentColor, isFocused = false, showDivider = false, - onClick = { viewModel.cycleFocusBorderColor() } + onClick = { viewModel.cycleAccentColor() } ) } } @@ -4056,7 +4056,7 @@ private fun SettingsSectionItem( isSelected -> TextPrimary else -> TextSecondary } - val accentColor = resolveFocusBorderColor(fallback = Pink) + val accentColor = resolveAccentColor(fallback = Pink) Row( modifier = Modifier @@ -4162,7 +4162,7 @@ private fun TvSettingsStatusPill(label: String) { Box( modifier = Modifier .size(6.dp) - .background(resolveFocusBorderColor(fallback = Pink), RoundedCornerShape(99.dp)) + .background(resolveAccentColor(fallback = Pink), RoundedCornerShape(99.dp)) ) Spacer(modifier = Modifier.width(8.dp)) Text( @@ -4219,7 +4219,7 @@ private fun TvSettingsInsightPanel( Text( text = if (focusedIndex >= 0) "Focused setting" else "Section", style = ArflixTypography.caption, - color = resolveFocusBorderColor(fallback = Pink), + color = resolveAccentColor(fallback = Pink), maxLines = 1 ) Spacer(modifier = Modifier.height(6.dp)) @@ -4398,7 +4398,7 @@ private fun tvSettingsPanelFacts( ) "appearance" -> listOf( "OLED" to if (uiState.oledBlackBackground) "On" else "Off", - "Focus border" to uiState.focusBorderColor + "Accent color" to uiState.accentColor ) "profiles" -> listOf( "Startup" to if (uiState.skipProfileSelection) "Skip picker" else "Show picker" @@ -4519,7 +4519,7 @@ private fun TvGeneralSettingsRows( clockFormat: String = "24h", showBudget: Boolean = true, spoilerBlurEnabled: Boolean = false, - focusBorderColor: String = "White", + accentColor: String = "White", volumeBoostDb: Int = 0, focusedIndex: Int, onSubtitleClick: () -> Unit, @@ -4538,7 +4538,7 @@ private fun TvGeneralSettingsRows( onClockFormatClick: () -> Unit = {}, onShowBudgetToggle: (Boolean) -> Unit = {}, onSpoilerBlurToggle: (Boolean) -> Unit = {}, - onFocusBorderColorClick: () -> Unit = {}, + onAccentColorClick: () -> Unit = {}, showLoadingStats: Boolean = true, onShowLoadingStatsToggle: (Boolean) -> Unit = {}, onVolumeBoostClick: () -> Unit = {}, @@ -4645,7 +4645,7 @@ private fun TvGeneralSettingsRows( 21 -> SettingsRow(Icons.Default.Schedule, stringResource(R.string.clock_format), stringResource(R.string.clock_format_desc), if (clockFormat == "12h") "12-hour" else "24-hour", focusedIndex == localIndex, onClockFormatClick, Modifier.settingsFocusSlot(localIndex)) 22 -> SettingsToggleRow(stringResource(R.string.show_budget), stringResource(R.string.show_budget_desc), showBudget, focusedIndex == localIndex, onShowBudgetToggle, Modifier.settingsFocusSlot(localIndex)) 23 -> SettingsToggleRow(stringResource(R.string.spoiler_blur), stringResource(R.string.spoiler_blur_desc), spoilerBlurEnabled, focusedIndex == localIndex, onSpoilerBlurToggle, Modifier.settingsFocusSlot(localIndex)) - 24 -> SettingsRow(Icons.Default.Palette, stringResource(R.string.focus_border_color), stringResource(R.string.focus_border_color_desc), focusBorderColor, focusedIndex == localIndex, onFocusBorderColorClick, Modifier.settingsFocusSlot(localIndex)) + 24 -> SettingsRow(Icons.Default.Palette, stringResource(R.string.accent_color), stringResource(R.string.accent_color_desc), accentColor, focusedIndex == localIndex, onAccentColorClick, Modifier.settingsFocusSlot(localIndex)) 25 -> SettingsRow(Icons.Default.Language, stringResource(R.string.dns_provider), stringResource(R.string.dns_desc), dnsProvider, focusedIndex == localIndex, onDnsProviderClick, Modifier.settingsFocusSlot(localIndex)) 26 -> SettingsToggleRow(stringResource(R.string.show_loading_stats), stringResource(R.string.show_loading_stats_desc), showLoadingStats, focusedIndex == localIndex, onShowLoadingStatsToggle, Modifier.settingsFocusSlot(localIndex)) 27 -> SettingsRow( @@ -4704,7 +4704,7 @@ private fun GeneralSettings( clockFormat: String = "24h", showBudget: Boolean = true, spoilerBlurEnabled: Boolean = false, - focusBorderColor: String = "White", + accentColor: String = "White", volumeBoostDb: Int = 0, focusedIndex: Int, onSubtitleClick: () -> Unit, @@ -4723,7 +4723,7 @@ private fun GeneralSettings( onClockFormatClick: () -> Unit = {}, onShowBudgetToggle: (Boolean) -> Unit = {}, onSpoilerBlurToggle: (Boolean) -> Unit = {}, - onFocusBorderColorClick: () -> Unit = {}, + onAccentColorClick: () -> Unit = {}, showLoadingStats: Boolean = true, onShowLoadingStatsToggle: (Boolean) -> Unit = {}, onVolumeBoostClick: () -> Unit = {}, @@ -5021,11 +5021,11 @@ private fun GeneralSettings( Spacer(modifier = Modifier.height(10.dp)) SettingsRow( icon = Icons.Default.Palette, - title = stringResource(R.string.focus_border_color), - subtitle = stringResource(R.string.focus_border_color_desc), - value = focusBorderColor, + title = stringResource(R.string.accent_color), + subtitle = stringResource(R.string.accent_color_desc), + value = accentColor, isFocused = focusedIndex == 24, - onClick = onFocusBorderColorClick, + onClick = onAccentColorClick, modifier = Modifier.settingsFocusSlot(24) ) @@ -5827,7 +5827,7 @@ private fun IptvSettings( playlists.forEachIndexed { index, playlist -> val rowIndex = index + 1 val epgSourceCount = playlist.settingsEpgInput().lineSequence().count { it.isNotBlank() } - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row(modifier = Modifier.settingsFocusSlot(rowIndex).fillMaxWidth().background(if (focusedIndex == rowIndex) Color.White.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.05f), RoundedCornerShape(12.dp)).border(width = if (focusedIndex == rowIndex) 2.dp else 0.dp, color = if (focusedIndex == rowIndex) focusRingColor else Color.Transparent, shape = RoundedCornerShape(12.dp)).clickable { onEditPlaylist(index) }.padding(horizontal = 16.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text(playlist.name, style = ArflixTypography.cardTitle.copy(fontSize = 16.sp), color = if (focusedIndex == rowIndex) TextPrimary else TextSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis) @@ -5905,7 +5905,7 @@ private fun SettingsRow( modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row( modifier = modifier .fillMaxWidth() @@ -5987,7 +5987,7 @@ private fun SettingsToggleRow( modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row( modifier = modifier .fillMaxWidth() @@ -6359,7 +6359,7 @@ private fun CatalogDiscoveryInputButton( onClick: () -> Unit ) { var isFocused by remember { mutableStateOf(false) } - val inputFocusColor = resolveFocusBorderColor(fallback = Color.White) + val inputFocusColor = resolveAccentColor(fallback = Color.White) Column( modifier = modifier .clip(RoundedCornerShape(12.dp)) @@ -6491,7 +6491,7 @@ private fun CatalogDiscoveryResultRow( compact: Boolean = false ) { var isFocused by remember { mutableStateOf(false) } - val compactFocusColor = resolveFocusBorderColor(fallback = Color.White) + val compactFocusColor = resolveAccentColor(fallback = Color.White) val creator = result.creatorName ?: result.creatorHandle val creatorMeta = creator?.let { "by $it" } val itemCountMeta = result.itemCount?.let { "$it items" } @@ -6611,7 +6611,7 @@ private fun CatalogDiscoveryResultRow( return } - val resultFocusColor = resolveFocusBorderColor(fallback = Color.White) + val resultFocusColor = resolveAccentColor(fallback = Color.White) Row( modifier = Modifier .fillMaxWidth() @@ -7013,7 +7013,7 @@ private fun CatalogsSettings( val isSelected = selectedIds.contains(catalog.id) val layoutToggleEnabled = catalog.kind != CatalogKind.COLLECTION_RAIL val layoutRowKey = remember(catalog.id, catalog.kind) { catalogueLayoutRowKey(catalog) } - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row(modifier = Modifier.settingsFocusSlot(rowFocusIndex).fillMaxWidth().background(if (isSelected) Pink.copy(alpha = 0.2f) else if (isRowFocused) Color.White.copy(alpha = 0.12f) else Color.White.copy(alpha = 0.05f), RoundedCornerShape(12.dp)).border(width = if (isRowFocused) 2.dp else 0.dp, color = if (isRowFocused) focusRingColor else Color.Transparent, shape = RoundedCornerShape(12.dp)).clickable { onRenameCatalog(catalog) }.padding(horizontal = 16.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text(title, style = ArflixTypography.cardTitle.copy(fontSize = 16.sp), color = if (isRowFocused || isSelected) TextPrimary else TextSecondary, maxLines = 1, overflow = TextOverflow.Ellipsis) @@ -7055,18 +7055,24 @@ private fun CatalogActionChip( onClick: () -> Unit = {} ) { // Support both D-pad focus AND touch pressed state + val accent = resolveAccentColor(fallback = Color.White) var isPressed by remember { mutableStateOf(false) } val visualActive = isFocused || isPressed val bgColor = when { !enabled -> Color.Black.copy(alpha = 0.4f) visualActive && isDestructive -> Color(0xFFDC2626) - visualActive -> Color.White + visualActive -> accent else -> Color.White.copy(alpha = 0.08f) } + // Choose foreground (icon) color based on accent luminance for contrast + val accentFg = if (visualActive) { + val l = 0.299f * accent.red + 0.587f * accent.green + 0.114f * accent.blue + if (l > 0.5f) Color.Black else Color.White + } else Color.White val fgColor = when { !enabled -> Color.White.copy(alpha = 0.5f) visualActive && isDestructive -> Color.White - visualActive -> Color.Black + visualActive -> accentFg else -> Color.White.copy(alpha = 0.7f) } Box( @@ -7076,7 +7082,7 @@ private fun CatalogActionChip( .background(bgColor, RoundedCornerShape(8.dp)) .border( width = if (visualActive) 1.5.dp else 1.dp, - color = if (visualActive) Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.15f), + color = if (visualActive) accent else Color.White.copy(alpha = 0.15f), shape = RoundedCornerShape(8.dp) ), contentAlignment = Alignment.Center @@ -7199,7 +7205,7 @@ private fun AddonRow( val isToggleFocused = isFocused && focusedAction == 0 val isDeleteFocused = canDelete && isFocused && focusedAction == 1 val isEnabled = addon.isEnabled - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row( modifier = modifier @@ -7466,7 +7472,7 @@ private fun AccountActionRow( isEnabled: Boolean, isFocused: Boolean ) { - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row( modifier = Modifier .fillMaxWidth() @@ -7536,7 +7542,7 @@ private fun SettingsActionRow( onClick: () -> Unit, modifier: Modifier = Modifier ) { - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Row( modifier = modifier .fillMaxWidth() @@ -7608,7 +7614,7 @@ private fun AccountRow( secondaryActionLabel: String? = null, expirationText: String? = null ) { - val focusRingColor = resolveFocusBorderColor(fallback = Pink) + val focusRingColor = resolveAccentColor(fallback = Pink) Column( modifier = modifier .fillMaxWidth() @@ -7924,7 +7930,7 @@ private fun InputModalLegacy( .focusRequester(fieldFocusRequesters[index]) .border( width = if (isFocused) 2.dp else 1.dp, - color = if (isFocused) resolveFocusBorderColor(fallback = Pink) else Color.White.copy(alpha = 0.2f), + color = if (isFocused) resolveAccentColor(fallback = Pink) else Color.White.copy(alpha = 0.2f), shape = RoundedCornerShape(8.dp) ) ) @@ -7939,7 +7945,7 @@ private fun InputModalLegacy( // Paste button val isPasteFocused = focusedIndex == fields.size - val pasteFocusRingColor = resolveFocusBorderColor(fallback = Pink) + val pasteFocusRingColor = resolveAccentColor(fallback = Pink) Row( modifier = Modifier .fillMaxWidth() @@ -8291,7 +8297,7 @@ private fun InputModal( ) } - val regexFieldFocusColor = resolveFocusBorderColor(fallback = Pink) + val regexFieldFocusColor = resolveAccentColor(fallback = Pink) Box( modifier = Modifier .fillMaxWidth() @@ -8620,7 +8626,7 @@ private fun SubtitlePickerModal( itemsIndexed(options) { index, option -> val isFocused = index == safeIndex val isSelected = option.equals(selected, ignoreCase = true) - val optionFocusColor = resolveFocusBorderColor(fallback = Pink) + val optionFocusColor = resolveAccentColor(fallback = Pink) Row( modifier = Modifier .fillMaxWidth() 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 f32f5405..48fd5198 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 @@ -183,10 +183,10 @@ data class SettingsUiState( val oledBlackBackground: Boolean = false, val clockFormat: String = "24h", val qualityFilters: List = emptyList(), - // Spoiler blur — blur unwatched episode card images and hide synopsis + // Spoiler blur — blur unwatched episode card images and hide synopsis val spoilerBlurEnabled: Boolean = false, - // Focus border color — user-selectable theme colour for the D-pad focus ring - val focusBorderColor: String = "White", + // Accent color — user-selectable theme colour for focus rings, buttons, and selected items + val accentColor: String = "White", val qualityFilterPresetLabel: String = "OFF", // Toast val toastMessage: String? = null, @@ -278,7 +278,7 @@ class SettingsViewModel @Inject constructor( private fun includeSpecialsKey() = profileManager.profileBooleanKey("include_specials") private val qualityFiltersKey = stringPreferencesKey("quality_filters") - // Global (non-profile-scoped) AI subtitle settings — device-wide, not per-profile + // Global (non-profile-scoped) AI subtitle settings — device-wide, not per-profile private val subtitleAiEnabledKey = booleanPreferencesKey("subtitle_ai_enabled") private val subtitleAiAutoSelectKey = booleanPreferencesKey("subtitle_ai_auto_select") private val subtitleAiApiKeyKey = stringPreferencesKey("subtitle_ai_api_key") @@ -439,7 +439,20 @@ class SettingsViewModel @Inject constructor( val spoilerBlurEnabled = prefs[spoilerBlurKey()] ?: false val showBudget = prefs[showBudgetKey()] ?: true val clockFormat = prefs[clockFormatKey()] ?: "24h" - val focusBorderColor = prefs[com.arflix.tv.util.FOCUS_BORDER_COLOR_KEY] ?: "White" + // One-time migration: read old "focus_border_color" key if new "accent_color" is absent + val OLD_FOCUS_BORDER_COLOR_KEY = stringPreferencesKey("focus_border_color") + val legacyColor = prefs[OLD_FOCUS_BORDER_COLOR_KEY] + val accentColor = prefs[com.arflix.tv.util.ACCENT_COLOR_KEY] ?: legacyColor ?: "White" + // Schedule async migration to copy old key → new key and delete old + if (legacyColor != null) { + viewModelScope.launch { + context.settingsDataStore.edit { + val old = it[OLD_FOCUS_BORDER_COLOR_KEY] ?: return@edit + it[com.arflix.tv.util.ACCENT_COLOR_KEY] = old + it.remove(OLD_FOCUS_BORDER_COLOR_KEY) + } + } + } val volumeBoostDb = prefs[volumeBoostDbKey()]?.toIntOrNull()?.coerceIn(0, 15) ?: 0 val showLoadingStats = prefs[showLoadingStatsKey()] ?: true @@ -531,7 +544,7 @@ class SettingsViewModel @Inject constructor( skipProfileSelection = skipProfileSelection, oledBlackBackground = oledBlackBackground, clockFormat = clockFormat, - focusBorderColor = focusBorderColor, + accentColor = accentColor, qualityFilters = qualityFilters, qualityFilterPresetLabel = detectQualityFilterPreset(qualityFilters).label, subtitleAiEnabled = subtitleAiEnabled, @@ -1147,17 +1160,17 @@ class SettingsViewModel @Inject constructor( } /** - * Cycle the focus border color through the rainbow palette. + * Cycle the accent color through the rainbow palette. * Order: White → Red → Orange → Yellow → Green → Blue → Indigo → Violet → White */ - fun cycleFocusBorderColor() { + fun cycleAccentColor() { val colors = listOf("White", "Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet") - val current = _uiState.value.focusBorderColor + val current = _uiState.value.accentColor val nextIndex = (colors.indexOf(current) + 1) % colors.size val next = colors[nextIndex] viewModelScope.launch { - context.settingsDataStore.edit { it[com.arflix.tv.util.FOCUS_BORDER_COLOR_KEY] = next } - _uiState.value = _uiState.value.copy(focusBorderColor = next) + context.settingsDataStore.edit { it[com.arflix.tv.util.ACCENT_COLOR_KEY] = next } + _uiState.value = _uiState.value.copy(accentColor = next) syncLocalStateToCloud(silent = true) } } @@ -1214,7 +1227,7 @@ class SettingsViewModel @Inject constructor( } } - // ── AI Subtitles ────────────────────────────────────────────────────────── + // -- AI Subtitles --------------------------------------------------------- fun setSubtitleAiEnabled(enabled: Boolean) { viewModelScope.launch { diff --git a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt index f12a54b2..54b53b44 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/screens/tv/TvScreen.kt @@ -1,4 +1,4 @@ - + @file:Suppress("UnsafeOptInUsageError") package com.arflix.tv.ui.screens.tv @@ -53,6 +53,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.ui.res.stringResource import com.arflix.tv.R +import com.arflix.tv.ui.skin.resolveAccentColor import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -1496,13 +1497,15 @@ private fun GroupRailItem( ) { var ignoreMenuSelectUntilRelease by remember(showMenu) { mutableStateOf(showMenu) } + val accent = resolveAccentColor(fallback = Color.White) + Box { Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(6.dp)) - .background(if (isFocused) Color.White.copy(alpha = 0.07f) else Color.Transparent) - .then(if (isFocused) Modifier.border(1.dp, Color.White.copy(alpha = 0.4f), RoundedCornerShape(6.dp)) else Modifier) + .background(if (isFocused) accent.copy(alpha = 0.2f) else Color.Transparent) + .then(if (isFocused) Modifier.border(1.5.dp, accent, RoundedCornerShape(6.dp)) else Modifier) .combinedClickable(onClick = onClick, onLongClick = onLongPress) .padding(horizontal = 8.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically diff --git a/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt b/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt index ec0ebde6..04dd8a6d 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioFocus.kt @@ -55,8 +55,8 @@ fun Modifier.arvioFocusable( onFocusChanged: (Boolean) -> Unit = {}, ): Modifier = composed { val interactionSource = remember { MutableInteractionSource() } - // Allow the user's "Focus border colour" setting to override the default - val resolvedOutlineColor = LocalFocusBorderColorOverride.current ?: outlineColor + // Allow the user's "Accent Color" setting to override the default + val resolvedOutlineColor = LocalAccentColorOverride.current ?: outlineColor val isPressed by interactionSource.collectIsPressedAsState() var isFocused by remember { mutableStateOf(false) } diff --git a/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioSkin.kt b/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioSkin.kt index ae6fc07b..39dd0bb2 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioSkin.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/skin/ArvioSkin.kt @@ -9,14 +9,14 @@ import androidx.compose.ui.graphics.Color val LocalArvioSkinTokens = staticCompositionLocalOf { ArvioSkinTokens.defaults() } /** - * Optional override for the focus border colour, driven by the user's - * "Focus border colour" setting. When non-null every [arvioFocusable] + * Optional override for the accent colour, driven by the user's + * "Accent Color" setting. When non-null every [arvioFocusable] * composable uses this colour instead of [ArvioColorTokens.focusOutline]. */ -val LocalFocusBorderColorOverride = staticCompositionLocalOf { null } +val LocalAccentColorOverride = staticCompositionLocalOf { null } /** - * Resolves the effective focus border colour for a component that draws its + * Resolves the effective accent colour for a component that draws its * own focus border (for example, settings rows and glow chips) instead of * using the [arvioFocusable] modifier. Returns the user's chosen override * when set, otherwise the provided fallback color. @@ -24,15 +24,15 @@ val LocalFocusBorderColorOverride = staticCompositionLocalOf { null } * Call this inside a `@Composable` lambda to read the CompositionLocal. */ @Composable -fun resolveFocusBorderColor(fallback: Color): Color { - return LocalFocusBorderColorOverride.current ?: fallback +fun resolveAccentColor(fallback: Color): Color { + return LocalAccentColorOverride.current ?: fallback } /** * Maps a user-facing colour name to its [Color] value. - * Used by the focus border colour setting and the colour picker. + * Used by the accent colour setting and the colour picker. */ -fun focusBorderColorFromName(name: String): Color = when (name) { +fun accentColorFromName(name: String): Color = when (name) { "Red" -> Color(0xFFFF4444) "Orange" -> Color(0xFFFF8800) "Yellow" -> Color(0xFFFFDD44) diff --git a/app/src/main/kotlin/com/arflix/tv/ui/theme/Theme.kt b/app/src/main/kotlin/com/arflix/tv/ui/theme/Theme.kt index e25da589..3d55bee2 100644 --- a/app/src/main/kotlin/com/arflix/tv/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/arflix/tv/ui/theme/Theme.kt @@ -7,9 +7,9 @@ import androidx.compose.ui.graphics.Color import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.darkColorScheme -import com.arflix.tv.ui.skin.LocalFocusBorderColorOverride +import com.arflix.tv.ui.skin.LocalAccentColorOverride import com.arflix.tv.ui.skin.ProvideArvioSkin -import com.arflix.tv.ui.skin.focusBorderColorFromName +import com.arflix.tv.ui.skin.accentColorFromName /** * ARVIO Color scheme holder - Arctic Fuse 2 inspired @@ -89,11 +89,11 @@ val LocalArflixColors = LocalArvioColors @Composable fun ArvioTvTheme( oledBlackBackground: Boolean = false, - focusBorderColorName: String? = null, + accentColorName: String? = null, content: @Composable () -> Unit ) { val backgroundDark = if (oledBlackBackground) Color.Black else BackgroundDark - val focusBorderColor = focusBorderColorName?.let { focusBorderColorFromName(it) } + val accentColor = accentColorName?.let { accentColorFromName(it) } val colorScheme = darkColorScheme( primary = ArcticWhite, onPrimary = ArcticBlack, @@ -123,7 +123,7 @@ fun ArvioTvTheme( CompositionLocalProvider( LocalArvioColors provides arvioColors, LocalOledBlackBackground provides oledBlackBackground, - LocalFocusBorderColorOverride provides focusBorderColor + LocalAccentColorOverride provides accentColor ) { ProvideArvioSkin { MaterialTheme( @@ -139,11 +139,11 @@ fun ArvioTvTheme( @Composable fun ArflixTvTheme( oledBlackBackground: Boolean = false, - focusBorderColorName: String? = null, + accentColorName: String? = null, content: @Composable () -> Unit ) = ArvioTvTheme( oledBlackBackground = oledBlackBackground, - focusBorderColorName = focusBorderColorName, + accentColorName = accentColorName, content = content ) diff --git a/app/src/main/kotlin/com/arflix/tv/util/DeviceType.kt b/app/src/main/kotlin/com/arflix/tv/util/DeviceType.kt index d7923bfc..494baf31 100644 --- a/app/src/main/kotlin/com/arflix/tv/util/DeviceType.kt +++ b/app/src/main/kotlin/com/arflix/tv/util/DeviceType.kt @@ -35,8 +35,8 @@ val SKIP_PROFILE_SELECTION_KEY = booleanPreferencesKey("skip_profile_selection") /** Key for forcing pure-black (OLED) app background */ val OLED_BLACK_BACKGROUND_KEY = booleanPreferencesKey("oled_black_background") -/** Key for the user-selected focus border colour (e.g. "White", "Red", "Blue") */ -val FOCUS_BORDER_COLOR_KEY = stringPreferencesKey("focus_border_color") +/** Key for the user-selected accent colour (e.g. "White", "Red", "Blue") */ +val ACCENT_COLOR_KEY = stringPreferencesKey("accent_color") /** * Fast-path cache for the device-mode override. Read before onCreate() during diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 53d46726..8b3ca630 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -188,8 +188,8 @@ השאר ריק לשימוש בברירת המחדל של ARVIO. ערכים מותאמים חלים כאשר מקור לא מספק User-Agent משלו. טשטוש ספוילרים טשטש כרטיסי פרקים שלא נצפו כדי למנוע ספוילרים - צבע מסגרת הפוקוס - בחר את צבע טבעת הפוקוס של D-pad + צבע הדגשה + בחר את צבע ההדגשה לטבעות פוקוס, כפתורים ופריטים נבחרים צפה באוסף diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ee6ac4d0..5f06f460 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -182,8 +182,8 @@ Display the movie budget on the home hero banner Spoiler Blur Blur unwatched episode cards to avoid spoilers - Focus Border Color - Choose the D-pad focus ring color + Accent Color + Choose the accent color for focus rings, buttons, and selected items Display stream resolution progress Amplify quiet sources (via system LoudnessEnhancer) Resolve API and stream requests