From 5b24848820f8dbf04c1f90a4b667893d6f8689bd Mon Sep 17 00:00:00 2001 From: Nuvio CI Date: Sun, 1 Mar 2026 17:09:21 -0500 Subject: [PATCH] Unify settings toggles/chips across playback/profile and fix white-theme selected styling --- .idea/vcs.xml | 12 - app/build.gradle.kts | 3 +- .../main/java/com/nuvio/tv/MainActivity.kt | 76 ++--- .../ui/components/SourceStatusFilterChip.kt | 9 +- .../nuvio/tv/ui/navigation/NuvioNavHost.kt | 40 ++- .../tv/ui/screens/addon/AddonManagerScreen.kt | 265 ++++++++++-------- .../screens/settings/PlaybackAudioSettings.kt | 20 +- .../settings/PlaybackSettingsScreen.kt | 34 +-- .../settings/PlaybackSubtitleSettings.kt | 11 +- .../settings/ProfileSettingsViewModel.kt | 8 + .../screens/settings/SettingsDesignSystem.kt | 16 +- .../tv/ui/screens/settings/SettingsScreen.kt | 54 +++- app/src/main/res/values/strings.xml | 2 + 13 files changed, 335 insertions(+), 215 deletions(-) diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 59fb4985..94a25f7f 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,17 +2,5 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4a71693c..2f2baea7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -63,8 +63,7 @@ android { buildTypes { debug { - signingConfig = signingConfigs.getByName("release") - isDebuggable = false + isDebuggable = true isMinifyEnabled = false buildConfigField("boolean", "IS_DEBUG_BUILD", "true") diff --git a/app/src/main/java/com/nuvio/tv/MainActivity.kt b/app/src/main/java/com/nuvio/tv/MainActivity.kt index 6c9efdba..6ea33426 100644 --- a/app/src/main/java/com/nuvio/tv/MainActivity.kt +++ b/app/src/main/java/com/nuvio/tv/MainActivity.kt @@ -351,50 +351,50 @@ class MainActivity : ComponentActivity() { Screen.Home.route, Screen.Search.route, Screen.Library.route, - Screen.Settings.route, - Screen.AddonManager.route + Screen.Settings.route ) } val strNavHome = stringResource(R.string.nav_home) val strNavSearch = stringResource(R.string.nav_search) val strNavLibrary = stringResource(R.string.nav_library) - val strNavAddons = stringResource(R.string.nav_addons) val strNavSettings = stringResource(R.string.nav_settings) val drawerItems = remember( strNavHome, strNavSearch, strNavLibrary, - strNavAddons, strNavSettings ) { - listOf( - DrawerItem( - route = Screen.Home.route, - label = strNavHome, - icon = Icons.Default.Home - ), - DrawerItem( - route = Screen.Search.route, - label = strNavSearch, - iconRes = R.raw.sidebar_search - ), - DrawerItem( - route = Screen.Library.route, - label = strNavLibrary, - iconRes = R.raw.sidebar_library - ), - DrawerItem( - route = Screen.AddonManager.route, - label = strNavAddons, - iconRes = R.raw.sidebar_plugin - ), - DrawerItem( - route = Screen.Settings.route, - label = strNavSettings, - iconRes = R.raw.sidebar_settings + buildList { + add( + DrawerItem( + route = Screen.Home.route, + label = strNavHome, + icon = Icons.Default.Home + ) ) - ) + add( + DrawerItem( + route = Screen.Search.route, + label = strNavSearch, + iconRes = R.raw.sidebar_search + ) + ) + add( + DrawerItem( + route = Screen.Library.route, + label = strNavLibrary, + iconRes = R.raw.sidebar_library + ) + ) + add( + DrawerItem( + route = Screen.Settings.route, + label = strNavSettings, + iconRes = R.raw.sidebar_settings + ) + ) + } } val selectedDrawerRoute = drawerItems.firstOrNull { item -> currentRoute == item.route || currentRoute?.startsWith("${item.route}/") == true @@ -413,6 +413,8 @@ class MainActivity : ComponentActivity() { sidebarCollapsed = sidebarCollapsed, modernSidebarBlurEnabled = modernSidebarBlurEnabled, hideBuiltInHeaders = hideBuiltInHeadersForFloatingPill, + activeProfileUsesPrimaryAddons = activeProfile?.usesPrimaryAddons == true, + activeProfileUsesPrimaryPlugins = activeProfile?.usesPrimaryPlugins == true, activeProfileName = activeProfile?.name ?: "", activeProfileColorHex = activeProfile?.avatarColorHex ?: "#1E88E5", showProfileSelector = profiles.size > 1, @@ -432,6 +434,8 @@ class MainActivity : ComponentActivity() { selectedDrawerRoute = selectedDrawerRoute, sidebarCollapsed = sidebarCollapsed, hideBuiltInHeaders = false, + activeProfileUsesPrimaryAddons = activeProfile?.usesPrimaryAddons == true, + activeProfileUsesPrimaryPlugins = activeProfile?.usesPrimaryPlugins == true, activeProfileName = activeProfile?.name ?: "", activeProfileColorHex = activeProfile?.avatarColorHex ?: "#1E88E5", showProfileSelector = profiles.size > 1, @@ -496,6 +500,8 @@ private fun LegacySidebarScaffold( selectedDrawerRoute: String?, sidebarCollapsed: Boolean, hideBuiltInHeaders: Boolean, + activeProfileUsesPrimaryAddons: Boolean, + activeProfileUsesPrimaryPlugins: Boolean, activeProfileName: String, activeProfileColorHex: String, showProfileSelector: Boolean, @@ -693,7 +699,9 @@ private fun LegacySidebarScaffold( NuvioNavHost( navController = navController, startDestination = startDestination, - hideBuiltInHeaders = hideBuiltInHeaders + hideBuiltInHeaders = hideBuiltInHeaders, + activeProfileUsesPrimaryAddons = activeProfileUsesPrimaryAddons, + activeProfileUsesPrimaryPlugins = activeProfileUsesPrimaryPlugins ) } } @@ -780,6 +788,8 @@ private fun ModernSidebarScaffold( sidebarCollapsed: Boolean, modernSidebarBlurEnabled: Boolean, hideBuiltInHeaders: Boolean, + activeProfileUsesPrimaryAddons: Boolean, + activeProfileUsesPrimaryPlugins: Boolean, activeProfileName: String, activeProfileColorHex: String, showProfileSelector: Boolean, @@ -1032,7 +1042,9 @@ private fun ModernSidebarScaffold( NuvioNavHost( navController = navController, startDestination = startDestination, - hideBuiltInHeaders = hideBuiltInHeaders + hideBuiltInHeaders = hideBuiltInHeaders, + activeProfileUsesPrimaryAddons = activeProfileUsesPrimaryAddons, + activeProfileUsesPrimaryPlugins = activeProfileUsesPrimaryPlugins ) } diff --git a/app/src/main/java/com/nuvio/tv/ui/components/SourceStatusFilterChip.kt b/app/src/main/java/com/nuvio/tv/ui/components/SourceStatusFilterChip.kt index d618379c..72aaf9cd 100644 --- a/app/src/main/java/com/nuvio/tv/ui/components/SourceStatusFilterChip.kt +++ b/app/src/main/java/com/nuvio/tv/ui/components/SourceStatusFilterChip.kt @@ -26,7 +26,9 @@ import androidx.tv.material3.FilterChip import androidx.tv.material3.FilterChipDefaults import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import com.nuvio.tv.domain.model.AppTheme import com.nuvio.tv.ui.theme.NuvioColors +import com.nuvio.tv.ui.theme.NuvioTheme enum class SourceChipStatus { LOADING, @@ -54,6 +56,7 @@ fun SourceStatusFilterChip( val isError = status == SourceChipStatus.ERROR val isLoading = status == SourceChipStatus.LOADING val shouldUseNormalColors = !isError + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) androidx.compose.ui.graphics.Color.White else NuvioColors.Secondary val shakeOffsetPx = remember { Animatable(0f) } val alphaPulse = remember { Animatable(1f) } @@ -102,9 +105,9 @@ fun SourceStatusFilterChip( }, colors = FilterChipDefaults.colors( containerColor = if (shouldUseNormalColors) NuvioColors.BackgroundCard else NuvioColors.Error.copy(alpha = 0.05f), - focusedContainerColor = if (shouldUseNormalColors) NuvioColors.Secondary else NuvioColors.Error.copy(alpha = 0.08f), - selectedContainerColor = if (shouldUseNormalColors) NuvioColors.Secondary else NuvioColors.Error.copy(alpha = 0.07f), - focusedSelectedContainerColor = if (shouldUseNormalColors) NuvioColors.Secondary else NuvioColors.Error.copy(alpha = 0.09f), + focusedContainerColor = if (shouldUseNormalColors) selectedAccent else NuvioColors.Error.copy(alpha = 0.08f), + selectedContainerColor = if (shouldUseNormalColors) selectedAccent else NuvioColors.Error.copy(alpha = 0.07f), + focusedSelectedContainerColor = if (shouldUseNormalColors) selectedAccent else NuvioColors.Error.copy(alpha = 0.09f), contentColor = textColor, focusedContentColor = textColor, selectedContentColor = textColor, diff --git a/app/src/main/java/com/nuvio/tv/ui/navigation/NuvioNavHost.kt b/app/src/main/java/com/nuvio/tv/ui/navigation/NuvioNavHost.kt index bc725add..600e9589 100644 --- a/app/src/main/java/com/nuvio/tv/ui/navigation/NuvioNavHost.kt +++ b/app/src/main/java/com/nuvio/tv/ui/navigation/NuvioNavHost.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.ExitTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost @@ -39,7 +40,9 @@ import com.nuvio.tv.ui.screens.cast.CastDetailScreen fun NuvioNavHost( navController: NavHostController, startDestination: String = Screen.Home.route, - hideBuiltInHeaders: Boolean = false + hideBuiltInHeaders: Boolean = false, + activeProfileUsesPrimaryAddons: Boolean = false, + activeProfileUsesPrimaryPlugins: Boolean = false ) { fun isStreamToPlayer(from: String, to: String): Boolean { return from.startsWith("stream/") && to.startsWith("player/") @@ -659,7 +662,8 @@ fun NuvioNavHost( SettingsScreen( showBuiltInHeader = !hideBuiltInHeaders, onNavigateToTrakt = { navController.navigate(Screen.Trakt.route) }, - onNavigateToAuthQrSignIn = { navController.navigate(Screen.AuthQrSignIn.route) } + onNavigateToAuthQrSignIn = { navController.navigate(Screen.AuthQrSignIn.route) }, + onNavigateToCatalogOrder = { navController.navigate(Screen.CatalogOrder.route) } ) } @@ -694,10 +698,19 @@ fun NuvioNavHost( } composable(Screen.AddonManager.route) { - AddonManagerScreen( - showBuiltInHeader = !hideBuiltInHeaders, - onNavigateToCatalogOrder = { navController.navigate(Screen.CatalogOrder.route) } - ) + if (activeProfileUsesPrimaryAddons) { + LaunchedEffect(Unit) { + navController.navigate(Screen.Settings.route) { + popUpTo(Screen.AddonManager.route) { inclusive = true } + launchSingleTop = true + } + } + } else { + AddonManagerScreen( + showBuiltInHeader = !hideBuiltInHeaders, + onNavigateToCatalogOrder = { navController.navigate(Screen.CatalogOrder.route) } + ) + } } composable(Screen.CatalogOrder.route) { @@ -707,9 +720,18 @@ fun NuvioNavHost( } composable(Screen.Plugins.route) { - PluginScreen( - onBackPress = { navController.popBackStack() } - ) + if (activeProfileUsesPrimaryPlugins) { + LaunchedEffect(Unit) { + navController.navigate(Screen.Settings.route) { + popUpTo(Screen.Plugins.route) { inclusive = true } + launchSingleTop = true + } + } + } else { + PluginScreen( + onBackPress = { navController.popBackStack() } + ) + } } composable(Screen.Account.route) { diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/addon/AddonManagerScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/addon/AddonManagerScreen.kt index a931e30b..7a54d83f 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/addon/AddonManagerScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/addon/AddonManagerScreen.kt @@ -95,6 +95,7 @@ import com.nuvio.tv.R fun AddonManagerScreen( viewModel: AddonManagerViewModel = hiltViewModel(), showBuiltInHeader: Boolean = true, + embeddedInSettings: Boolean = false, onNavigateToCatalogOrder: () -> Unit = {} ) { val uiState by viewModel.uiState.collectAsState() @@ -156,156 +157,182 @@ fun AddonManagerScreen( Box( modifier = Modifier .fillMaxSize() - .background(NuvioColors.Background) + .let { + if (embeddedInSettings) it else it.background(NuvioColors.Background) + } ) { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 36.dp, vertical = 28.dp), - verticalArrangement = Arrangement.spacedBy(20.dp) + contentPadding = PaddingValues( + start = if (embeddedInSettings) 0.dp else 36.dp, + end = if (embeddedInSettings) 0.dp else 36.dp, + top = if (showBuiltInHeader) 28.dp else 0.dp, + bottom = if (embeddedInSettings) 32.dp else 28.dp + ), + verticalArrangement = Arrangement.spacedBy(if (embeddedInSettings) 16.dp else 20.dp) ) { - item { - Text( - text = stringResource(R.string.addon_title), - style = MaterialTheme.typography.headlineMedium, - color = if (showBuiltInHeader) NuvioColors.TextPrimary else Color.Transparent - ) + if (showBuiltInHeader) { + item { + Text( + text = stringResource(R.string.addon_title), + style = MaterialTheme.typography.headlineMedium, + color = NuvioColors.TextPrimary + ) + } } if (viewModel.isReadOnly) { item { - Card( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = Color(0xFF1A3A5C)), - shape = RoundedCornerShape(12.dp) - ) { + if (embeddedInSettings) { Text( text = stringResource(R.string.addon_readonly_notice), style = MaterialTheme.typography.bodyMedium, color = NuvioColors.TextSecondary, - modifier = androidx.compose.ui.Modifier.padding(16.dp) + modifier = Modifier.fillMaxWidth() ) + } else { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = Color(0xFF1A3A5C)), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = stringResource(R.string.addon_readonly_notice), + style = MaterialTheme.typography.bodyMedium, + color = NuvioColors.TextSecondary, + modifier = androidx.compose.ui.Modifier.padding(16.dp) + ) + } } } } if (!viewModel.isReadOnly) { item { - Card( - modifier = Modifier + val installContainerModifier = if (embeddedInSettings) { + Modifier .fillMaxWidth() - .animateContentSize(), - colors = CardDefaults.cardColors(containerColor = NuvioColors.BackgroundCard), - shape = RoundedCornerShape(12.dp) - ) { - Column(modifier = Modifier.padding(20.dp)) { - Text( - text = stringResource(R.string.addon_install_title), - style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), - color = NuvioColors.TextPrimary + .animateContentSize() + } else { + Modifier + .fillMaxWidth() + .animateContentSize() + .background( + color = NuvioColors.BackgroundCard, + shape = RoundedCornerShape(12.dp) ) - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically + } + Column( + modifier = installContainerModifier.padding( + if (embeddedInSettings) 0.dp else 20.dp + ) + ) { + Text( + text = stringResource(R.string.addon_install_title), + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + color = NuvioColors.TextPrimary + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Surface( + onClick = { isEditing = true }, + modifier = Modifier + .weight(1f) + .focusRequester(surfaceFocusRequester), + colors = ClickableSurfaceDefaults.colors( + containerColor = NuvioColors.BackgroundElevated, + focusedContainerColor = NuvioColors.BackgroundElevated + ), + border = ClickableSurfaceDefaults.border( + border = Border( + border = BorderStroke(1.dp, NuvioColors.Border), + shape = RoundedCornerShape(12.dp) + ), + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(12.dp) + ) + ), + shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)), + scale = ClickableSurfaceDefaults.scale(focusedScale = 1f) ) { - // Surface always stays in the tree for stable D-pad focus - Surface( - onClick = { isEditing = true }, - modifier = Modifier - .weight(1f) - .focusRequester(surfaceFocusRequester), - colors = ClickableSurfaceDefaults.colors( - containerColor = NuvioColors.BackgroundElevated, - focusedContainerColor = NuvioColors.BackgroundElevated - ), - border = ClickableSurfaceDefaults.border( - border = Border( - border = BorderStroke(1.dp, NuvioColors.Border), - shape = RoundedCornerShape(12.dp) - ), - focusedBorder = Border( - border = BorderStroke(2.dp, NuvioColors.FocusRing), - shape = RoundedCornerShape(12.dp) - ) - ), - shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)), - scale = ClickableSurfaceDefaults.scale(focusedScale = 1f) - ) { - Box(modifier = Modifier.padding(12.dp)) { - BasicTextField( - value = uiState.installUrl, - onValueChange = viewModel::onInstallUrlChange, - modifier = Modifier - .fillMaxWidth() - .focusRequester(textFieldFocusRequester) - .onFocusChanged { - if (!it.isFocused && isEditing) { - isEditing = false - keyboardController?.hide() - } - }, - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions( - onDone = { - viewModel.installAddon() + Box(modifier = Modifier.padding(12.dp)) { + BasicTextField( + value = uiState.installUrl, + onValueChange = viewModel::onInstallUrlChange, + modifier = Modifier + .fillMaxWidth() + .focusRequester(textFieldFocusRequester) + .onFocusChanged { + if (!it.isFocused && isEditing) { isEditing = false keyboardController?.hide() - installButtonFocusRequester.requestFocus() } - ), - textStyle = MaterialTheme.typography.bodyMedium.copy( - color = NuvioColors.TextPrimary - ), - cursorBrush = SolidColor(if (isEditing) NuvioColors.Primary else Color.Transparent), - decorationBox = { innerTextField -> - if (uiState.installUrl.isEmpty()) { - Text( - text = stringResource(R.string.addon_install_placeholder), - style = MaterialTheme.typography.bodyMedium, - color = NuvioColors.TextTertiary - ) - } - innerTextField() + }, + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + viewModel.installAddon() + isEditing = false + keyboardController?.hide() + installButtonFocusRequester.requestFocus() } - ) - } - } - - Button( - onClick = { - viewModel.installAddon() - isEditing = false - keyboardController?.hide() - installButtonFocusRequester.requestFocus() - }, - enabled = !uiState.isInstalling, - modifier = Modifier.focusRequester(installButtonFocusRequester), - colors = ButtonDefaults.colors( - containerColor = NuvioColors.BackgroundCard, - contentColor = NuvioColors.TextPrimary, - focusedContainerColor = NuvioColors.FocusBackground, - focusedContentColor = NuvioColors.Primary - ), - shape = ButtonDefaults.shape(RoundedCornerShape(12.dp)) - ) { - Text(text = if (uiState.isInstalling) stringResource(R.string.addon_installing) else stringResource(R.string.addon_install_btn)) + ), + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = NuvioColors.TextPrimary + ), + cursorBrush = SolidColor(if (isEditing) NuvioColors.Primary else Color.Transparent), + decorationBox = { innerTextField -> + if (uiState.installUrl.isEmpty()) { + Text( + text = stringResource(R.string.addon_install_placeholder), + style = MaterialTheme.typography.bodyMedium, + color = NuvioColors.TextTertiary + ) + } + innerTextField() + } + ) } } - AnimatedVisibility(visible = uiState.error != null) { - Text( - text = uiState.error.orEmpty(), - style = MaterialTheme.typography.bodyMedium, - color = NuvioColors.Error, - modifier = Modifier.padding(top = 10.dp) - ) + Button( + onClick = { + viewModel.installAddon() + isEditing = false + keyboardController?.hide() + installButtonFocusRequester.requestFocus() + }, + enabled = !uiState.isInstalling, + modifier = Modifier.focusRequester(installButtonFocusRequester), + colors = ButtonDefaults.colors( + containerColor = NuvioColors.BackgroundCard, + contentColor = NuvioColors.TextPrimary, + focusedContainerColor = NuvioColors.FocusBackground, + focusedContentColor = NuvioColors.Primary + ), + shape = ButtonDefaults.shape(RoundedCornerShape(12.dp)) + ) { + Text(text = if (uiState.isInstalling) stringResource(R.string.addon_installing) else stringResource(R.string.addon_install_btn)) } } + + AnimatedVisibility(visible = uiState.error != null) { + Text( + text = uiState.error.orEmpty(), + style = MaterialTheme.typography.bodyMedium, + color = NuvioColors.Error, + modifier = Modifier.padding(top = 10.dp) + ) + } } } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackAudioSettings.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackAudioSettings.kt index 08aceb5b..bd6205d7 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackAudioSettings.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackAudioSettings.kt @@ -49,12 +49,14 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import com.nuvio.tv.domain.model.AppTheme import com.nuvio.tv.data.local.AVAILABLE_SUBTITLE_LANGUAGES import com.nuvio.tv.data.local.AudioLanguageOption import com.nuvio.tv.data.local.PlayerSettings import com.nuvio.tv.data.local.TrailerSettings import com.nuvio.tv.ui.components.NuvioDialog import com.nuvio.tv.ui.theme.NuvioColors +import com.nuvio.tv.ui.theme.NuvioTheme internal fun LazyListScope.trailerAndAudioSettingsItems( playerSettings: PlayerSettings, @@ -260,6 +262,7 @@ private fun AudioLanguageSelectionDialog( onDismiss: () -> Unit ) { val focusRequester = remember { FocusRequester() } + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary val specialOptions = listOf( AudioLanguageOption.DEFAULT to stringResource(R.string.audio_lang_default), AudioLanguageOption.DEVICE to stringResource(R.string.audio_lang_device) @@ -301,8 +304,8 @@ private fun AudioLanguageSelectionDialog( .then(if (index == 0) Modifier.focusRequester(focusRequester) else Modifier) .onFocusChanged { isFocused = it.isFocused }, colors = CardDefaults.colors( - containerColor = if (isSelected) NuvioColors.FocusBackground else NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.FocusBackground + containerColor = if (isSelected) selectedAccent.copy(alpha = 0.22f) else NuvioColors.BackgroundCard, + focusedContainerColor = if (isSelected) selectedAccent.copy(alpha = 0.28f) else NuvioColors.FocusBackground ), shape = CardDefaults.shape(shape = RoundedCornerShape(10.dp)), scale = CardDefaults.scale(focusedScale = 1f) @@ -316,7 +319,7 @@ private fun AudioLanguageSelectionDialog( Text( text = name, style = MaterialTheme.typography.bodyLarge, - color = if (isSelected) NuvioColors.Primary else NuvioColors.TextPrimary, + color = NuvioColors.TextPrimary, modifier = Modifier.weight(1f) ) if (isSelected) { @@ -324,7 +327,7 @@ private fun AudioLanguageSelectionDialog( Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(R.string.cd_selected), - tint = NuvioColors.Primary, + tint = selectedAccent, modifier = Modifier.size(20.dp) ) } @@ -343,6 +346,7 @@ private fun DecoderPriorityDialog( onDismiss: () -> Unit ) { val focusRequester = remember { FocusRequester() } + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary val options = listOf( Triple(0, stringResource(R.string.audio_decoder_device_only), stringResource(R.string.audio_decoder_device_only_desc)), Triple(1, stringResource(R.string.audio_decoder_prefer_device), stringResource(R.string.audio_decoder_prefer_device_desc)), @@ -382,8 +386,8 @@ private fun DecoderPriorityDialog( .fillMaxWidth() .then(if (index == 0) Modifier.focusRequester(focusRequester) else Modifier), colors = CardDefaults.colors( - containerColor = if (isSelected) NuvioColors.FocusBackground else NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.FocusBackground + containerColor = if (isSelected) selectedAccent.copy(alpha = 0.22f) else NuvioColors.BackgroundCard, + focusedContainerColor = if (isSelected) selectedAccent.copy(alpha = 0.28f) else NuvioColors.FocusBackground ), shape = CardDefaults.shape(shape = RoundedCornerShape(10.dp)), scale = CardDefaults.scale(focusedScale = 1f) @@ -397,7 +401,7 @@ private fun DecoderPriorityDialog( Column(modifier = Modifier.weight(1f)) { Text( text = title, - color = if (isSelected) NuvioColors.Primary else NuvioColors.TextPrimary, + color = NuvioColors.TextPrimary, style = MaterialTheme.typography.bodyLarge ) Spacer(modifier = Modifier.height(4.dp)) @@ -412,7 +416,7 @@ private fun DecoderPriorityDialog( Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(R.string.cd_selected), - tint = NuvioColors.Primary, + tint = selectedAccent, modifier = Modifier.size(20.dp) ) } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSettingsScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSettingsScreen.kt index e72b4618..f4eb9052 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSettingsScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSettingsScreen.kt @@ -66,6 +66,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.res.stringResource import com.nuvio.tv.R +import com.nuvio.tv.domain.model.AppTheme import androidx.compose.ui.text.input.KeyboardType import android.view.KeyEvent import androidx.compose.ui.input.key.onKeyEvent @@ -85,8 +86,6 @@ import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.Icon import androidx.tv.material3.IconButton import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Switch -import androidx.tv.material3.SwitchDefaults import androidx.tv.material3.Text import com.nuvio.tv.data.local.AVAILABLE_SUBTITLE_LANGUAGES import com.nuvio.tv.data.local.AudioLanguageOption @@ -98,6 +97,7 @@ import com.nuvio.tv.data.local.StreamAutoPlaySource import com.nuvio.tv.data.local.TrailerSettings import com.nuvio.tv.ui.components.NuvioDialog import com.nuvio.tv.ui.theme.NuvioColors +import com.nuvio.tv.ui.theme.NuvioTheme import kotlinx.coroutines.launch import androidx.compose.material.icons.filled.PlayCircle import androidx.compose.material.icons.filled.PauseCircle @@ -404,15 +404,9 @@ internal fun ToggleSettingsItem( Spacer(modifier = Modifier.width(16.dp)) - Switch( + SettingsTogglePill( checked = isChecked, - onCheckedChange = null, // Handled by Card onClick - colors = SwitchDefaults.colors( - checkedThumbColor = NuvioColors.Secondary.copy(alpha = contentAlpha), - checkedTrackColor = NuvioColors.Secondary.copy(alpha = 0.35f * contentAlpha), - uncheckedThumbColor = NuvioColors.TextSecondary.copy(alpha = contentAlpha), - uncheckedTrackColor = NuvioColors.Border - ) + enabled = enabled ) } } @@ -428,6 +422,7 @@ internal fun RenderTypeSettingsItem( enabled: Boolean = true ) { var isFocused by remember { mutableStateOf(false) } + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary val contentAlpha = if (enabled) 1f else 0.4f Card( @@ -443,12 +438,12 @@ internal fun RenderTypeSettingsItem( }, colors = CardDefaults.colors( containerColor = if (isSelected) { - NuvioColors.Primary.copy(alpha = 0.15f * contentAlpha) + selectedAccent.copy(alpha = 0.22f * contentAlpha) } else { NuvioColors.BackgroundCard }, focusedContainerColor = if (isSelected) { - NuvioColors.Primary.copy(alpha = 0.15f * contentAlpha) + selectedAccent.copy(alpha = 0.28f * contentAlpha) } else { NuvioColors.BackgroundCard } @@ -459,7 +454,7 @@ internal fun RenderTypeSettingsItem( shape = RoundedCornerShape(SettingsSecondaryCardRadius) ), border = if (isSelected) Border( - border = BorderStroke(2.dp, NuvioColors.Primary.copy(alpha = contentAlpha)), + border = BorderStroke(2.dp, selectedAccent.copy(alpha = contentAlpha)), shape = RoundedCornerShape(SettingsSecondaryCardRadius) ) else Border.None ), @@ -476,7 +471,7 @@ internal fun RenderTypeSettingsItem( Text( text = title, style = MaterialTheme.typography.bodyLarge, - color = (if (isSelected) NuvioColors.Primary else NuvioColors.TextPrimary).copy(alpha = contentAlpha), + color = NuvioColors.TextPrimary.copy(alpha = contentAlpha), maxLines = 1, overflow = TextOverflow.Ellipsis ) @@ -495,7 +490,7 @@ internal fun RenderTypeSettingsItem( Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(R.string.cd_selected), - tint = NuvioColors.Primary.copy(alpha = contentAlpha), + tint = selectedAccent.copy(alpha = contentAlpha), modifier = Modifier.size(24.dp) ) } @@ -989,6 +984,7 @@ private fun LanguageOptionItem( modifier: Modifier = Modifier ) { var isFocused by remember { mutableStateOf(false) } + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary Card( onClick = onClick, @@ -997,8 +993,8 @@ private fun LanguageOptionItem( .then(modifier) .onFocusChanged { isFocused = it.isFocused }, colors = CardDefaults.colors( - containerColor = if (isSelected) NuvioColors.FocusBackground else NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.FocusBackground + containerColor = if (isSelected) selectedAccent.copy(alpha = 0.22f) else NuvioColors.BackgroundCard, + focusedContainerColor = if (isSelected) selectedAccent.copy(alpha = 0.28f) else NuvioColors.FocusBackground ), shape = CardDefaults.shape(shape = RoundedCornerShape(10.dp)), scale = CardDefaults.scale(focusedScale = 1f) @@ -1012,7 +1008,7 @@ private fun LanguageOptionItem( Text( text = name, style = MaterialTheme.typography.bodyLarge, - color = if (isSelected) NuvioColors.Primary else NuvioColors.TextPrimary, + color = NuvioColors.TextPrimary, modifier = Modifier.weight(1f) ) @@ -1029,7 +1025,7 @@ private fun LanguageOptionItem( Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(R.string.cd_selected), - tint = NuvioColors.Primary, + tint = selectedAccent, modifier = Modifier.size(20.dp) ) } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSubtitleSettings.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSubtitleSettings.kt index 64016831..f9e6c2d7 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSubtitleSettings.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/PlaybackSubtitleSettings.kt @@ -30,6 +30,7 @@ import com.nuvio.tv.R import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text +import com.nuvio.tv.domain.model.AppTheme import com.nuvio.tv.data.local.AVAILABLE_SUBTITLE_LANGUAGES import com.nuvio.tv.data.local.LibassRenderType import com.nuvio.tv.data.local.PlayerSettings @@ -37,6 +38,7 @@ import com.nuvio.tv.data.local.SubtitleOrganizationMode import com.nuvio.tv.data.local.SUBTITLE_LANGUAGE_FORCED import com.nuvio.tv.ui.components.NuvioDialog import com.nuvio.tv.ui.theme.NuvioColors +import com.nuvio.tv.ui.theme.NuvioTheme private val subtitleColors = listOf( Color.White, @@ -431,6 +433,7 @@ private fun SubtitleOrganizationModeDialog( onModeSelected: (SubtitleOrganizationMode) -> Unit, onDismiss: () -> Unit ) { + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary val options = listOf( Triple(SubtitleOrganizationMode.NONE, stringResource(R.string.sub_org_none), stringResource(R.string.sub_org_none_desc)), Triple(SubtitleOrganizationMode.BY_LANGUAGE, stringResource(R.string.sub_org_by_lang), stringResource(R.string.sub_org_by_lang_desc)), @@ -462,8 +465,8 @@ private fun SubtitleOrganizationModeDialog( onClick = { onModeSelected(mode) }, modifier = androidx.compose.ui.Modifier.fillMaxWidth(), colors = androidx.tv.material3.CardDefaults.colors( - containerColor = if (isSelected) NuvioColors.FocusBackground else NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.FocusBackground + containerColor = if (isSelected) selectedAccent.copy(alpha = 0.22f) else NuvioColors.BackgroundCard, + focusedContainerColor = if (isSelected) selectedAccent.copy(alpha = 0.28f) else NuvioColors.FocusBackground ), shape = androidx.tv.material3.CardDefaults.shape( shape = androidx.compose.foundation.shape.RoundedCornerShape(10.dp) @@ -482,7 +485,7 @@ private fun SubtitleOrganizationModeDialog( Text( text = title, style = MaterialTheme.typography.bodyLarge, - color = if (isSelected) NuvioColors.Primary else NuvioColors.TextPrimary + color = NuvioColors.TextPrimary ) Spacer(modifier = androidx.compose.ui.Modifier.height(4.dp)) Text( @@ -495,7 +498,7 @@ private fun SubtitleOrganizationModeDialog( androidx.tv.material3.Icon( imageVector = Icons.Default.Check, contentDescription = stringResource(R.string.cd_selected), - tint = NuvioColors.Primary + tint = selectedAccent ) } } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/ProfileSettingsViewModel.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/ProfileSettingsViewModel.kt index 135b2667..90ccb873 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/ProfileSettingsViewModel.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/ProfileSettingsViewModel.kt @@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -27,6 +28,13 @@ class ProfileSettingsViewModel @Inject constructor( .map { it == 1 } .stateIn(viewModelScope, SharingStarted.Eagerly, true) + val activeProfile: StateFlow = combine( + profileManager.activeProfileId, + profileManager.profiles + ) { activeId, profiles -> + profiles.firstOrNull { it.id == activeId } + }.stateIn(viewModelScope, SharingStarted.Eagerly, profileManager.activeProfile) + val canAddProfile: Boolean get() = profileManager.profiles.value.size < 4 diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsDesignSystem.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsDesignSystem.kt index 5a82008f..d5b787f3 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsDesignSystem.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsDesignSystem.kt @@ -58,8 +58,10 @@ import androidx.tv.material3.Text import coil.compose.rememberAsyncImagePainter import coil.decode.SvgDecoder import coil.request.ImageRequest +import com.nuvio.tv.domain.model.AppTheme import com.nuvio.tv.R import com.nuvio.tv.ui.theme.NuvioColors +import com.nuvio.tv.ui.theme.NuvioTheme internal val SettingsContainerRadius = 28.dp internal val SettingsPillRadius = 999.dp @@ -538,6 +540,7 @@ internal fun SettingsChoiceChip( onFocused: () -> Unit = {} ) { var isFocused by remember { mutableStateOf(false) } + val selectedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary Card( onClick = onClick, @@ -549,12 +552,12 @@ internal fun SettingsChoiceChip( } }, colors = CardDefaults.colors( - containerColor = if (selected) NuvioColors.FocusRing.copy(alpha = 0.2f) else NuvioColors.Background, - focusedContainerColor = if (selected) NuvioColors.FocusRing.copy(alpha = 0.2f) else NuvioColors.Background + containerColor = if (selected) selectedAccent.copy(alpha = 0.22f) else NuvioColors.Background, + focusedContainerColor = if (selected) selectedAccent.copy(alpha = 0.28f) else NuvioColors.Background ), border = CardDefaults.border( border = if (selected) Border( - border = BorderStroke(1.dp, NuvioColors.FocusRing), + border = BorderStroke(1.dp, selectedAccent), shape = RoundedCornerShape(SettingsPillRadius) ) else Border.None, focusedBorder = Border( @@ -568,17 +571,18 @@ internal fun SettingsChoiceChip( Text( text = label, style = MaterialTheme.typography.labelMedium, - color = if (selected || isFocused) NuvioColors.TextPrimary else NuvioColors.TextSecondary, + color = if (selected) selectedAccent else if (isFocused) NuvioColors.TextPrimary else NuvioColors.TextSecondary, modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) ) } } @Composable -private fun SettingsTogglePill( +internal fun SettingsTogglePill( checked: Boolean, enabled: Boolean ) { + val checkedAccent = if (NuvioTheme.currentTheme == AppTheme.WHITE) Color.White else NuvioColors.Secondary val alpha = if (enabled) 1f else 0.35f Box( modifier = Modifier @@ -587,7 +591,7 @@ private fun SettingsTogglePill( .clip(RoundedCornerShape(SettingsPillRadius)) .background( if (checked) { - NuvioColors.Secondary.copy(alpha = 0.35f * alpha) + checkedAccent.copy(alpha = 0.42f * alpha) } else { NuvioColors.Border.copy(alpha = alpha) } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsScreen.kt index cfa40cc1..02fef020 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/SettingsScreen.kt @@ -54,6 +54,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.tv.material3.ExperimentalTvMaterial3Api import com.nuvio.tv.BuildConfig import com.nuvio.tv.R +import com.nuvio.tv.ui.screens.addon.AddonManagerScreen import com.nuvio.tv.ui.screens.plugin.PluginScreenContent import com.nuvio.tv.ui.theme.NuvioColors import kotlinx.coroutines.delay @@ -63,6 +64,7 @@ internal enum class SettingsCategory { PROFILES, APPEARANCE, LAYOUT, + ADDONS, PLUGINS, INTEGRATION, PLAYBACK, @@ -125,6 +127,13 @@ private fun rememberSettingsSectionSpecs() = listOf( subtitle = stringResource(R.string.settings_layout_subtitle), destination = SettingsSectionDestination.Inline ), + SettingsSectionSpec( + category = SettingsCategory.ADDONS, + title = stringResource(R.string.addon_title), + rawIconRes = R.raw.sidebar_plugin, + subtitle = stringResource(R.string.settings_addons_subtitle), + destination = SettingsSectionDestination.Inline + ), SettingsSectionSpec( category = SettingsCategory.PLUGINS, title = stringResource(R.string.settings_plugins), @@ -174,18 +183,29 @@ fun SettingsScreen( showBuiltInHeader: Boolean = true, onNavigateToTrakt: () -> Unit = {}, onNavigateToAuthQrSignIn: () -> Unit = {}, + onNavigateToCatalogOrder: () -> Unit = {}, profileViewModel: ProfileSettingsViewModel = hiltViewModel() ) { val isPrimaryProfileActive by profileViewModel.isPrimaryProfileActive.collectAsStateWithLifecycle() + val activeProfile by profileViewModel.activeProfile.collectAsStateWithLifecycle() + val usesPrimaryProfileAddons = activeProfile?.let { !it.isPrimary && it.usesPrimaryAddons } == true + val usesPrimaryProfilePlugins = activeProfile?.let { !it.isPrimary && it.usesPrimaryPlugins } == true val allSectionSpecs = rememberSettingsSectionSpecs() - val visibleSections = remember(isPrimaryProfileActive, allSectionSpecs) { + val visibleSections = remember( + isPrimaryProfileActive, + usesPrimaryProfileAddons, + usesPrimaryProfilePlugins, + allSectionSpecs + ) { allSectionSpecs.filter { section -> when (section.category) { SettingsCategory.DEBUG -> BuildConfig.IS_DEBUG_BUILD SettingsCategory.PROFILES -> isPrimaryProfileActive SettingsCategory.ACCOUNT -> isPrimaryProfileActive SettingsCategory.TRAKT -> isPrimaryProfileActive + SettingsCategory.ADDONS -> !usesPrimaryProfileAddons + SettingsCategory.PLUGINS -> !usesPrimaryProfilePlugins else -> true } } @@ -373,6 +393,9 @@ fun SettingsScreen( null } ) + SettingsCategory.ADDONS -> AddonsSettingsContent( + onNavigateToCatalogOrder = onNavigateToCatalogOrder + ) SettingsCategory.PLAYBACK -> PlaybackSettingsContent( initialFocusRequester = if (allowDetailAutofocus) { contentFocusRequesters[SettingsCategory.PLAYBACK] @@ -443,6 +466,35 @@ private fun PluginsSettingsContent() { } } +@Composable +private fun AddonsSettingsContent( + onNavigateToCatalogOrder: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + SettingsDetailHeader( + title = stringResource(R.string.addon_title), + subtitle = stringResource(R.string.settings_addons_section_subtitle) + ) + SettingsGroupCard(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + contentAlignment = Alignment.TopStart + ) { + AddonManagerScreen( + showBuiltInHeader = false, + embeddedInSettings = true, + onNavigateToCatalogOrder = onNavigateToCatalogOrder + ) + } + } + } +} + @Composable private fun AccountSettingsInline( onNavigateToAuthQrSignIn: () -> Unit diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 127f97db..bd5641fa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -122,6 +122,7 @@ Manage user profiles. Layout Home structure and poster styles. + Addon installation and catalog sources. Plugins Repositories and providers. Integration @@ -131,6 +132,7 @@ Version and policies. Debug Developer tools and feature flags. + Manage addon installation, updates, and catalog order. Manage repositories, providers, and plugin states. Account and sync status. addons