Unify settings toggles/chips across playback/profile and fix white-theme selected styling

This commit is contained in:
Nuvio CI 2026-03-01 17:09:21 -05:00
parent b04b609f1f
commit 5b24848820
13 changed files with 335 additions and 215 deletions

View file

@ -2,17 +2,5 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Player" vcs="Git" />
<mapping directory="$PROJECT_DIR$/android-tv-samples" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/ass" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/expat" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/fontconfig" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/freetype" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/freetype/subprojects/dlg" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/fribidi" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/harfbuzz" vcs="Git" />
<mapping directory="$PROJECT_DIR$/libass-android/lib_ass/src/main/cpp/libass-cmake/src/unibreak" vcs="Git" />
</component>
</project>

View file

@ -63,8 +63,7 @@ android {
buildTypes {
debug {
signingConfig = signingConfigs.getByName("release")
isDebuggable = false
isDebuggable = true
isMinifyEnabled = false
buildConfigField("boolean", "IS_DEBUG_BUILD", "true")

View file

@ -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
)
}

View file

@ -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,

View file

@ -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) {

View file

@ -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)
)
}
}
}

View file

@ -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)
)
}

View file

@ -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)
)
}

View file

@ -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
)
}
}

View file

@ -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<UserProfile?> = 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

View file

@ -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)
}

View file

@ -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

View file

@ -122,6 +122,7 @@
<string name="settings_profiles_subtitle">Manage user profiles.</string>
<string name="settings_layout">Layout</string>
<string name="settings_layout_subtitle">Home structure and poster styles.</string>
<string name="settings_addons_subtitle">Addon installation and catalog sources.</string>
<string name="settings_plugins">Plugins</string>
<string name="settings_plugins_subtitle">Repositories and providers.</string>
<string name="settings_integration">Integration</string>
@ -131,6 +132,7 @@
<string name="settings_about_subtitle">Version and policies.</string>
<string name="settings_debug">Debug</string>
<string name="settings_debug_subtitle">Developer tools and feature flags.</string>
<string name="settings_addons_section_subtitle">Manage addon installation, updates, and catalog order.</string>
<string name="settings_plugins_section_subtitle">Manage repositories, providers, and plugin states.</string>
<string name="settings_account_section_subtitle">Account and sync status.</string>
<string name="account_stat_addons">addons</string>