diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 590ad89d..2d58fa4c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -8,9 +8,7 @@ plugins { android { namespace = "com.nuvio.tv" - compileSdk { - version = release(36) - } + compileSdk = 36 defaultConfig { applicationId = "com.nuvio.tv" diff --git a/app/src/main/java/com/nuvio/tv/MainActivity.kt b/app/src/main/java/com/nuvio/tv/MainActivity.kt index 0c7b75e2..bd5fa482 100644 --- a/app/src/main/java/com/nuvio/tv/MainActivity.kt +++ b/app/src/main/java/com/nuvio/tv/MainActivity.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.unit.dp @@ -44,18 +45,27 @@ import androidx.compose.material.icons.filled.Bookmark import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import coil.compose.AsyncImage +import com.nuvio.tv.data.local.ThemeDataStore +import com.nuvio.tv.domain.model.AppTheme import com.nuvio.tv.ui.navigation.NuvioNavHost import com.nuvio.tv.ui.navigation.Screen import com.nuvio.tv.ui.theme.NuvioTheme import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject @AndroidEntryPoint class MainActivity : ComponentActivity() { + + @Inject + lateinit var themeDataStore: ThemeDataStore + @OptIn(ExperimentalTvMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - NuvioTheme { + val currentTheme by themeDataStore.selectedTheme.collectAsState(initial = AppTheme.CRIMSON) + + NuvioTheme(appTheme = currentTheme) { Surface( modifier = Modifier.fillMaxSize(), shape = RectangleShape diff --git a/app/src/main/java/com/nuvio/tv/data/local/ThemeDataStore.kt b/app/src/main/java/com/nuvio/tv/data/local/ThemeDataStore.kt new file mode 100644 index 00000000..5d1231bc --- /dev/null +++ b/app/src/main/java/com/nuvio/tv/data/local/ThemeDataStore.kt @@ -0,0 +1,40 @@ +package com.nuvio.tv.data.local + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import com.nuvio.tv.domain.model.AppTheme +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Inject +import javax.inject.Singleton + +private val Context.themeDataStore: DataStore by preferencesDataStore(name = "theme_settings") + +@Singleton +class ThemeDataStore @Inject constructor( + @ApplicationContext private val context: Context +) { + private val dataStore = context.themeDataStore + + private val themeKey = stringPreferencesKey("selected_theme") + + val selectedTheme: Flow = dataStore.data.map { prefs -> + val themeName = prefs[themeKey] ?: AppTheme.CRIMSON.name + try { + AppTheme.valueOf(themeName) + } catch (e: IllegalArgumentException) { + AppTheme.CRIMSON + } + } + + suspend fun setTheme(theme: AppTheme) { + dataStore.edit { prefs -> + prefs[themeKey] = theme.name + } + } +} diff --git a/app/src/main/java/com/nuvio/tv/domain/model/AppTheme.kt b/app/src/main/java/com/nuvio/tv/domain/model/AppTheme.kt new file mode 100644 index 00000000..b7dbea15 --- /dev/null +++ b/app/src/main/java/com/nuvio/tv/domain/model/AppTheme.kt @@ -0,0 +1,10 @@ +package com.nuvio.tv.domain.model + +enum class AppTheme(val displayName: String) { + CRIMSON("Crimson"), + OCEAN("Ocean"), + VIOLET("Violet"), + EMERALD("Emerald"), + AMBER("Amber"), + ROSE("Rose") +} diff --git a/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt b/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt index fde3bfd7..ce4d8a1a 100644 --- a/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt +++ b/app/src/main/java/com/nuvio/tv/ui/components/ContentCard.kt @@ -28,7 +28,6 @@ import androidx.tv.material3.Border import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.Glow import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import coil.compose.AsyncImage @@ -79,18 +78,12 @@ fun ContentCard( ), border = CardDefaults.border( focusedBorder = Border( - border = BorderStroke(3.dp, NuvioColors.FocusRing), + border = BorderStroke(2.dp, NuvioColors.FocusRing), shape = RoundedCornerShape(8.dp) ) ), scale = CardDefaults.scale( - focusedScale = 1.08f - ), - glow = CardDefaults.glow( - focusedGlow = Glow( - elevation = 8.dp, - elevationColor = NuvioColors.FocusRing.copy(alpha = 0.3f) - ) + focusedScale = 1.05f ) ) { Box( diff --git a/app/src/main/java/com/nuvio/tv/ui/components/ContinueWatchingSection.kt b/app/src/main/java/com/nuvio/tv/ui/components/ContinueWatchingSection.kt index 95a08a4a..e0fac566 100644 --- a/app/src/main/java/com/nuvio/tv/ui/components/ContinueWatchingSection.kt +++ b/app/src/main/java/com/nuvio/tv/ui/components/ContinueWatchingSection.kt @@ -101,7 +101,7 @@ private fun ContinueWatchingCard( ), colors = CardDefaults.colors( containerColor = NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.BackgroundCard + focusedContainerColor = NuvioColors.FocusBackground ), border = CardDefaults.border( focusedBorder = Border( diff --git a/app/src/main/java/com/nuvio/tv/ui/components/SidebarNavigation.kt b/app/src/main/java/com/nuvio/tv/ui/components/SidebarNavigation.kt index 87846506..671082c1 100644 --- a/app/src/main/java/com/nuvio/tv/ui/components/SidebarNavigation.kt +++ b/app/src/main/java/com/nuvio/tv/ui/components/SidebarNavigation.kt @@ -111,7 +111,7 @@ private fun SidebarNavItem( label = "navItemBackground" ) val borderColor by animateColorAsState( - targetValue = if (isFocused) NuvioColors.BorderFocused else Color.Transparent, + targetValue = if (isFocused) NuvioColors.FocusRing else Color.Transparent, label = "navItemBorder" ) @@ -121,7 +121,7 @@ private fun SidebarNavItem( .height(56.dp) .clip(shape) .background(backgroundColor) - .border(width = 1.dp, color = borderColor, shape = shape) + .border(width = 2.dp, color = borderColor, shape = shape) .then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier) .onFocusChanged { state -> isFocused = state.isFocused 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 62d5517f..a1558d34 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 @@ -14,6 +14,7 @@ import com.nuvio.tv.ui.screens.player.PlayerScreen import com.nuvio.tv.ui.screens.plugin.PluginScreen import com.nuvio.tv.ui.screens.search.SearchScreen import com.nuvio.tv.ui.screens.settings.SettingsScreen +import com.nuvio.tv.ui.screens.settings.ThemeSettingsScreen import com.nuvio.tv.ui.screens.settings.TmdbSettingsScreen import com.nuvio.tv.ui.screens.stream.StreamScreen @@ -240,7 +241,8 @@ fun NuvioNavHost( composable(Screen.Settings.route) { SettingsScreen( onNavigateToPlugins = { navController.navigate(Screen.Plugins.route) }, - onNavigateToTmdb = { navController.navigate(Screen.TmdbSettings.route) } + onNavigateToTmdb = { navController.navigate(Screen.TmdbSettings.route) }, + onNavigateToTheme = { navController.navigate(Screen.ThemeSettings.route) } ) } @@ -250,6 +252,12 @@ fun NuvioNavHost( ) } + composable(Screen.ThemeSettings.route) { + ThemeSettingsScreen( + onBackPress = { navController.popBackStack() } + ) + } + composable(Screen.AddonManager.route) { AddonManagerScreen() } diff --git a/app/src/main/java/com/nuvio/tv/ui/navigation/Screen.kt b/app/src/main/java/com/nuvio/tv/ui/navigation/Screen.kt index 56b91299..31d231dd 100644 --- a/app/src/main/java/com/nuvio/tv/ui/navigation/Screen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/navigation/Screen.kt @@ -83,6 +83,7 @@ sealed class Screen(val route: String) { data object Library : Screen("library") data object Settings : Screen("settings") data object TmdbSettings : Screen("tmdb_settings") + data object ThemeSettings : Screen("theme_settings") data object AddonManager : Screen("addon_manager") data object Plugins : Screen("plugins") } diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/detail/EpisodesSection.kt b/app/src/main/java/com/nuvio/tv/ui/screens/detail/EpisodesSection.kt index f69b2ed5..c28e2143 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/detail/EpisodesSection.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/detail/EpisodesSection.kt @@ -83,7 +83,7 @@ fun SeasonTabs( ), colors = CardDefaults.colors( containerColor = if (isSelected) NuvioColors.SurfaceVariant else NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.Primary + focusedContainerColor = NuvioColors.Secondary ), border = CardDefaults.border( focusedBorder = Border( @@ -160,7 +160,7 @@ private fun EpisodeCard( ), colors = CardDefaults.colors( containerColor = NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.BackgroundCard + focusedContainerColor = NuvioColors.FocusBackground ), border = CardDefaults.border( focusedBorder = Border( diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/detail/HeroSection.kt b/app/src/main/java/com/nuvio/tv/ui/screens/detail/HeroSection.kt index e37d0519..05378bd9 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/detail/HeroSection.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/detail/HeroSection.kt @@ -1,5 +1,6 @@ package com.nuvio.tv.ui.screens.detail +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -23,6 +24,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.tv.material3.Border import androidx.tv.material3.Button import androidx.tv.material3.ButtonDefaults import androidx.tv.material3.ExperimentalTvMaterial3Api @@ -164,13 +166,19 @@ private fun PlayButton( .onFocusChanged { isFocused = it.isFocused }, colors = ButtonDefaults.colors( containerColor = androidx.compose.ui.graphics.Color.White, - focusedContainerColor = androidx.compose.ui.graphics.Color(0xFFD0D0D0), + focusedContainerColor = androidx.compose.ui.graphics.Color.White, contentColor = androidx.compose.ui.graphics.Color.Black, focusedContentColor = androidx.compose.ui.graphics.Color.Black ), shape = ButtonDefaults.shape( shape = RoundedCornerShape(32.dp) ), + border = ButtonDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(32.dp) + ) + ), contentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp) ) { Row( @@ -206,10 +214,16 @@ private fun ActionIconButton( .onFocusChanged { isFocused = it.isFocused }, colors = IconButtonDefaults.colors( containerColor = NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.Primary, + focusedContainerColor = NuvioColors.Secondary, contentColor = NuvioColors.TextPrimary, focusedContentColor = NuvioColors.OnPrimary ), + border = IconButtonDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = CircleShape + ) + ), shape = IconButtonDefaults.shape( shape = CircleShape ) diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/library/LibraryScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/library/LibraryScreen.kt index d9e63f90..4235b8a5 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/library/LibraryScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/library/LibraryScreen.kt @@ -122,7 +122,7 @@ private fun LibraryTabs( shape = CardDefaults.shape(shape = RoundedCornerShape(20.dp)), colors = CardDefaults.colors( containerColor = if (isSelected) NuvioColors.SurfaceVariant else NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.Primary + focusedContainerColor = NuvioColors.Secondary ), border = CardDefaults.border( focusedBorder = Border( diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/plugin/PluginScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/plugin/PluginScreen.kt index ece113ad..5f455ce2 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/plugin/PluginScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/plugin/PluginScreen.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement @@ -54,6 +55,7 @@ import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.tv.foundation.lazy.list.TvLazyColumn import androidx.tv.foundation.lazy.list.items +import androidx.tv.material3.Border import androidx.tv.material3.Button import androidx.tv.material3.ButtonDefaults import androidx.tv.material3.ClickableSurfaceDefaults @@ -265,8 +267,16 @@ private fun PluginHeader( Button( onClick = onAddRepository, colors = ButtonDefaults.colors( - containerColor = NuvioColors.Primary, - contentColor = Color.White + containerColor = NuvioColors.Secondary, + focusedContainerColor = NuvioColors.SecondaryVariant, + contentColor = Color.White, + focusedContentColor = Color.White + ), + border = ButtonDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(50) + ) ) ) { Icon( @@ -296,9 +306,15 @@ private fun RepositoryCard( .fillMaxWidth() .onFocusChanged { isFocused = it.isFocused }, colors = ClickableSurfaceDefaults.colors( - containerColor = if (isFocused) NuvioColors.FocusBackground else NuvioColors.BackgroundCard, + containerColor = NuvioColors.BackgroundCard, focusedContainerColor = NuvioColors.FocusBackground ), + border = ClickableSurfaceDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(12.dp) + ) + ), shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp)) ) { Row( 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 1d761f6a..2e141991 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 @@ -2,6 +2,7 @@ package com.nuvio.tv.ui.screens.settings +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -19,6 +20,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Tune import androidx.compose.runtime.Composable @@ -32,6 +34,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.material3.Border import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults import androidx.tv.material3.ExperimentalTvMaterial3Api @@ -43,7 +46,8 @@ import com.nuvio.tv.ui.theme.NuvioColors @Composable fun SettingsScreen( onNavigateToPlugins: () -> Unit = {}, - onNavigateToTmdb: () -> Unit = {} + onNavigateToTmdb: () -> Unit = {}, + onNavigateToTheme: () -> Unit = {} ) { Column( modifier = Modifier @@ -73,6 +77,15 @@ fun SettingsScreen( contentPadding = PaddingValues(bottom = 32.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { + item { + SettingsItem( + icon = Icons.Default.Palette, + title = "Appearance", + subtitle = "Choose your color theme", + onClick = onNavigateToTheme + ) + } + item { SettingsItem( icon = Icons.Default.Build, @@ -90,7 +103,7 @@ fun SettingsScreen( onClick = onNavigateToTmdb ) } - + item { SettingsItem( icon = Icons.Default.Settings, @@ -127,7 +140,14 @@ private fun SettingsItem( .fillMaxWidth() .onFocusChanged { isFocused = it.isFocused }, colors = CardDefaults.colors( - containerColor = if (isFocused) NuvioColors.FocusBackground else NuvioColors.BackgroundCard + containerColor = NuvioColors.BackgroundCard, + focusedContainerColor = NuvioColors.FocusBackground + ), + border = CardDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(12.dp) + ) ), shape = CardDefaults.shape(RoundedCornerShape(12.dp)) ) { diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/ThemeSettingsScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/ThemeSettingsScreen.kt new file mode 100644 index 00000000..5967e039 --- /dev/null +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/ThemeSettingsScreen.kt @@ -0,0 +1,172 @@ +@file:OptIn(ExperimentalTvMaterial3Api::class) + +package com.nuvio.tv.ui.screens.settings + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.tv.foundation.lazy.grid.TvGridCells +import androidx.tv.foundation.lazy.grid.TvLazyVerticalGrid +import androidx.tv.foundation.lazy.grid.items +import androidx.tv.material3.Border +import androidx.tv.material3.Card +import androidx.tv.material3.CardDefaults +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.ui.theme.NuvioColors +import com.nuvio.tv.ui.theme.ThemeColors + +@Composable +fun ThemeSettingsScreen( + viewModel: ThemeSettingsViewModel = hiltViewModel(), + onBackPress: () -> Unit +) { + val uiState by viewModel.uiState.collectAsState() + + BackHandler { onBackPress() } + + Column( + modifier = Modifier + .fillMaxSize() + .background(NuvioColors.Background) + .padding(horizontal = 48.dp, vertical = 24.dp) + ) { + Text( + text = "Appearance", + style = MaterialTheme.typography.headlineLarge, + color = NuvioColors.TextPrimary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Choose your color theme", + style = MaterialTheme.typography.bodyMedium, + color = NuvioColors.TextSecondary + ) + + Spacer(modifier = Modifier.height(32.dp)) + + TvLazyVerticalGrid( + columns = TvGridCells.Fixed(3), + contentPadding = PaddingValues(bottom = 32.dp), + horizontalArrangement = Arrangement.spacedBy(24.dp), + verticalArrangement = Arrangement.spacedBy(24.dp) + ) { + items(uiState.availableThemes) { theme -> + ThemeCard( + theme = theme, + isSelected = theme == uiState.selectedTheme, + onClick = { viewModel.onEvent(ThemeSettingsEvent.SelectTheme(theme)) } + ) + } + } + } +} + +@Composable +private fun ThemeCard( + theme: AppTheme, + isSelected: Boolean, + onClick: () -> Unit +) { + var isFocused by remember { mutableStateOf(false) } + val palette = ThemeColors.getColorPalette(theme) + + Card( + onClick = onClick, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { isFocused = it.isFocused }, + colors = CardDefaults.colors( + containerColor = NuvioColors.BackgroundCard, + focusedContainerColor = palette.focusBackground + ), + border = CardDefaults.border( + border = if (isSelected) Border( + border = BorderStroke(2.dp, palette.focusRing), + shape = RoundedCornerShape(16.dp) + ) else Border.None, + focusedBorder = Border( + border = BorderStroke(2.dp, palette.focusRing), + shape = RoundedCornerShape(16.dp) + ) + ), + shape = CardDefaults.shape(RoundedCornerShape(16.dp)), + scale = CardDefaults.scale(focusedScale = 1.05f) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Color preview circle + Box( + modifier = Modifier + .size(64.dp) + .clip(CircleShape) + .background(palette.secondary), + contentAlignment = Alignment.Center + ) { + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + tint = Color.White, + modifier = Modifier.size(32.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = theme.displayName, + style = MaterialTheme.typography.titleMedium, + color = if (isFocused || isSelected) NuvioColors.TextPrimary else NuvioColors.TextSecondary + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Color bar showing the theme colors + Box( + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + .clip(RoundedCornerShape(2.dp)) + .background(palette.focusRing) + ) + } + } +} diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/ThemeSettingsViewModel.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/ThemeSettingsViewModel.kt new file mode 100644 index 00000000..f6963933 --- /dev/null +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/ThemeSettingsViewModel.kt @@ -0,0 +1,52 @@ +package com.nuvio.tv.ui.screens.settings + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.nuvio.tv.data.local.ThemeDataStore +import com.nuvio.tv.domain.model.AppTheme +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class ThemeSettingsUiState( + val selectedTheme: AppTheme = AppTheme.CRIMSON, + val availableThemes: List = AppTheme.entries +) + +sealed class ThemeSettingsEvent { + data class SelectTheme(val theme: AppTheme) : ThemeSettingsEvent() +} + +@HiltViewModel +class ThemeSettingsViewModel @Inject constructor( + private val themeDataStore: ThemeDataStore +) : ViewModel() { + + private val _uiState = MutableStateFlow(ThemeSettingsUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + viewModelScope.launch { + themeDataStore.selectedTheme.collectLatest { theme -> + _uiState.update { it.copy(selectedTheme = theme) } + } + } + } + + fun onEvent(event: ThemeSettingsEvent) { + when (event) { + is ThemeSettingsEvent.SelectTheme -> selectTheme(event.theme) + } + } + + private fun selectTheme(theme: AppTheme) { + viewModelScope.launch { + themeDataStore.setTheme(theme) + } + } +} diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/settings/TmdbSettingsScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/settings/TmdbSettingsScreen.kt index 2ee47fac..8a9a4b29 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/settings/TmdbSettingsScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/settings/TmdbSettingsScreen.kt @@ -3,6 +3,7 @@ package com.nuvio.tv.ui.screens.settings import androidx.activity.compose.BackHandler +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -28,6 +29,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.tv.foundation.lazy.list.TvLazyColumn +import androidx.tv.material3.Border import androidx.tv.material3.Card import androidx.tv.material3.CardDefaults import androidx.tv.material3.ExperimentalTvMaterial3Api @@ -171,7 +173,14 @@ private fun ToggleCard( .fillMaxWidth() .onFocusChanged { isFocused = it.isFocused }, colors = CardDefaults.colors( - containerColor = if (isFocused) NuvioColors.FocusBackground else NuvioColors.BackgroundCard + containerColor = NuvioColors.BackgroundCard, + focusedContainerColor = NuvioColors.FocusBackground + ), + border = CardDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(12.dp) + ) ), shape = CardDefaults.shape(RoundedCornerShape(12.dp)) ) { diff --git a/app/src/main/java/com/nuvio/tv/ui/screens/stream/StreamScreen.kt b/app/src/main/java/com/nuvio/tv/ui/screens/stream/StreamScreen.kt index 8b343fa2..c7b81d3e 100644 --- a/app/src/main/java/com/nuvio/tv/ui/screens/stream/StreamScreen.kt +++ b/app/src/main/java/com/nuvio/tv/ui/screens/stream/StreamScreen.kt @@ -497,7 +497,13 @@ private fun ErrorState( modifier = Modifier.onFocusChanged { isFocused = it.isFocused }, colors = CardDefaults.colors( containerColor = NuvioColors.BackgroundCard, - focusedContainerColor = NuvioColors.Primary + focusedContainerColor = NuvioColors.Secondary + ), + border = CardDefaults.border( + focusedBorder = Border( + border = BorderStroke(2.dp, NuvioColors.FocusRing), + shape = RoundedCornerShape(8.dp) + ) ), shape = CardDefaults.shape(shape = RoundedCornerShape(8.dp)) ) { @@ -583,7 +589,7 @@ private fun StreamCard( ) ), shape = CardDefaults.shape(shape = RoundedCornerShape(12.dp)), - scale = CardDefaults.scale(focusedScale = 1.02f) + scale = CardDefaults.scale(focusedScale = 1.05f) ) { Row( modifier = Modifier diff --git a/app/src/main/java/com/nuvio/tv/ui/theme/Color.kt b/app/src/main/java/com/nuvio/tv/ui/theme/Color.kt index dd857309..11fadd8a 100644 --- a/app/src/main/java/com/nuvio/tv/ui/theme/Color.kt +++ b/app/src/main/java/com/nuvio/tv/ui/theme/Color.kt @@ -1,42 +1,122 @@ package com.nuvio.tv.ui.theme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color +import com.nuvio.tv.domain.model.AppTheme -object NuvioColors { - // Primary Background - Deep charcoal (NO gradients) - val Background = Color(0xFF0D0D0D) - val BackgroundElevated = Color(0xFF1A1A1A) - val BackgroundCard = Color(0xFF242424) +/** + * Dynamic color scheme that changes based on selected theme. + * Background colors have subtle theme tinting. + * Accent colors (secondary, focus) change per theme. + */ +class NuvioColorScheme(palette: ThemeColorPalette) { + // Primary Background - Theme dependent with subtle tinting + val Background = palette.background + val BackgroundElevated = palette.backgroundElevated + val BackgroundCard = palette.backgroundCard - // Surface colors + // Surface colors (constant) val Surface = Color(0xFF1E1E1E) val SurfaceVariant = Color(0xFF2D2D2D) - // Primary accent - Neutral Grey + // Primary accent - Neutral Grey (constant) val Primary = Color(0xFF9E9E9E) val PrimaryVariant = Color(0xFF6F6F6F) val OnPrimary = Color(0xFFFFFFFF) - // Secondary accent - Teal - val Secondary = Color(0xFF00BFA6) - val SecondaryVariant = Color(0xFF008F7A) + // Secondary accent - Theme dependent + val Secondary = palette.secondary + val SecondaryVariant = palette.secondaryVariant - // Text colors + // Text colors (constant) val TextPrimary = Color(0xFFFFFFFF) val TextSecondary = Color(0xFFB3B3B3) val TextTertiary = Color(0xFF808080) val TextDisabled = Color(0xFF4D4D4D) - // Focus states - Critical for TV navigation - val FocusRing = Color(0xFF00E5CC) // Bright teal for high visibility - val FocusBackground = Color(0xFF1A3D38) // Dark teal background + // Focus states - Theme dependent + val FocusRing = palette.focusRing + val FocusBackground = palette.focusBackground - // Status colors + // Status colors (constant) val Rating = Color(0xFFFFD700) val Error = Color(0xFFCF6679) val Success = Color(0xFF4CAF50) // Borders val Border = Color(0xFF333333) - val BorderFocused = Color(0xFF00E5CC) // Bright teal for high visibility + val BorderFocused = palette.focusRing +} + +/** + * Legacy NuvioColors object for backwards compatibility. + * Components should migrate to using NuvioTheme.colors instead. + * This object provides the current theme's colors via composition local. + */ +object NuvioColors { + // Dynamic background colors - Theme dependent with subtle tinting + val Background: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.Background + + val BackgroundElevated: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.BackgroundElevated + + val BackgroundCard: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.BackgroundCard + + // Surface colors (constant) + val Surface = Color(0xFF1E1E1E) + val SurfaceVariant = Color(0xFF2D2D2D) + + // Primary accent - Neutral Grey (constant) + val Primary = Color(0xFF9E9E9E) + val PrimaryVariant = Color(0xFF6F6F6F) + val OnPrimary = Color(0xFFFFFFFF) + + // Text colors (constant) + val TextPrimary = Color(0xFFFFFFFF) + val TextSecondary = Color(0xFFB3B3B3) + val TextTertiary = Color(0xFF808080) + val TextDisabled = Color(0xFF4D4D4D) + + // Status colors (constant) + val Rating = Color(0xFFFFD700) + val Error = Color(0xFFCF6679) + val Success = Color(0xFF4CAF50) + + // Borders (non-focus constant) + val Border = Color(0xFF333333) + + // Dynamic accent colors - Theme dependent + val Secondary: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.Secondary + + val SecondaryVariant: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.SecondaryVariant + + val FocusRing: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.FocusRing + + val FocusBackground: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.FocusBackground + + val BorderFocused: Color + @Composable + @ReadOnlyComposable + get() = NuvioTheme.colors.BorderFocused } diff --git a/app/src/main/java/com/nuvio/tv/ui/theme/Theme.kt b/app/src/main/java/com/nuvio/tv/ui/theme/Theme.kt index 2741f44d..91960026 100644 --- a/app/src/main/java/com/nuvio/tv/ui/theme/Theme.kt +++ b/app/src/main/java/com/nuvio/tv/ui/theme/Theme.kt @@ -2,11 +2,13 @@ package com.nuvio.tv.ui.theme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.tv.material3.ExperimentalTvMaterial3Api import androidx.tv.material3.MaterialTheme import androidx.tv.material3.darkColorScheme +import com.nuvio.tv.domain.model.AppTheme data class NuvioExtendedColors( val backgroundElevated: Color, @@ -19,48 +21,62 @@ data class NuvioExtendedColors( ) val LocalNuvioColors = staticCompositionLocalOf { + NuvioColorScheme(ThemeColors.Crimson) +} + +val LocalNuvioExtendedColors = staticCompositionLocalOf { NuvioExtendedColors( - backgroundElevated = NuvioColors.BackgroundElevated, - backgroundCard = NuvioColors.BackgroundCard, - textSecondary = NuvioColors.TextSecondary, - textTertiary = NuvioColors.TextTertiary, - focusRing = NuvioColors.FocusRing, - focusBackground = NuvioColors.FocusBackground, - rating = NuvioColors.Rating + backgroundElevated = Color(0xFF1A1A1A), + backgroundCard = Color(0xFF242424), + textSecondary = Color(0xFFB3B3B3), + textTertiary = Color(0xFF808080), + focusRing = ThemeColors.Crimson.focusRing, + focusBackground = ThemeColors.Crimson.focusBackground, + rating = Color(0xFFFFD700) ) } +val LocalAppTheme = staticCompositionLocalOf { AppTheme.CRIMSON } + @OptIn(ExperimentalTvMaterial3Api::class) @Composable fun NuvioTheme( + appTheme: AppTheme = AppTheme.CRIMSON, content: @Composable () -> Unit ) { - val colorScheme = darkColorScheme( - primary = NuvioColors.Primary, - onPrimary = NuvioColors.OnPrimary, - secondary = NuvioColors.Secondary, - background = NuvioColors.Background, - surface = NuvioColors.Surface, - surfaceVariant = NuvioColors.SurfaceVariant, - onBackground = NuvioColors.TextPrimary, - onSurface = NuvioColors.TextPrimary, - onSurfaceVariant = NuvioColors.TextSecondary, - error = NuvioColors.Error + val palette = ThemeColors.getColorPalette(appTheme) + val colorScheme = NuvioColorScheme(palette) + + val materialColorScheme = darkColorScheme( + primary = colorScheme.Primary, + onPrimary = colorScheme.OnPrimary, + secondary = colorScheme.Secondary, + background = colorScheme.Background, + surface = colorScheme.Surface, + surfaceVariant = colorScheme.SurfaceVariant, + onBackground = colorScheme.TextPrimary, + onSurface = colorScheme.TextPrimary, + onSurfaceVariant = colorScheme.TextSecondary, + error = colorScheme.Error ) val extendedColors = NuvioExtendedColors( - backgroundElevated = NuvioColors.BackgroundElevated, - backgroundCard = NuvioColors.BackgroundCard, - textSecondary = NuvioColors.TextSecondary, - textTertiary = NuvioColors.TextTertiary, - focusRing = NuvioColors.FocusRing, - focusBackground = NuvioColors.FocusBackground, - rating = NuvioColors.Rating + backgroundElevated = colorScheme.BackgroundElevated, + backgroundCard = colorScheme.BackgroundCard, + textSecondary = colorScheme.TextSecondary, + textTertiary = colorScheme.TextTertiary, + focusRing = colorScheme.FocusRing, + focusBackground = colorScheme.FocusBackground, + rating = colorScheme.Rating ) - CompositionLocalProvider(LocalNuvioColors provides extendedColors) { + CompositionLocalProvider( + LocalNuvioColors provides colorScheme, + LocalNuvioExtendedColors provides extendedColors, + LocalAppTheme provides appTheme + ) { MaterialTheme( - colorScheme = colorScheme, + colorScheme = materialColorScheme, typography = NuvioTypography, content = content ) @@ -68,7 +84,18 @@ fun NuvioTheme( } object NuvioTheme { + val colors: NuvioColorScheme + @Composable + @ReadOnlyComposable + get() = LocalNuvioColors.current + val extendedColors: NuvioExtendedColors @Composable - get() = LocalNuvioColors.current + @ReadOnlyComposable + get() = LocalNuvioExtendedColors.current + + val currentTheme: AppTheme + @Composable + @ReadOnlyComposable + get() = LocalAppTheme.current } diff --git a/app/src/main/java/com/nuvio/tv/ui/theme/ThemeColors.kt b/app/src/main/java/com/nuvio/tv/ui/theme/ThemeColors.kt new file mode 100644 index 00000000..568f65c1 --- /dev/null +++ b/app/src/main/java/com/nuvio/tv/ui/theme/ThemeColors.kt @@ -0,0 +1,93 @@ +package com.nuvio.tv.ui.theme + +import androidx.compose.ui.graphics.Color +import com.nuvio.tv.domain.model.AppTheme + +/** + * Color palette for each theme. + * Includes both accent colors and background tints for full theme customization. + */ +data class ThemeColorPalette( + val secondary: Color, + val secondaryVariant: Color, + val focusRing: Color, + val focusBackground: Color, + // Background colors with subtle theme tinting + val background: Color = Color(0xFF0D0D0D), + val backgroundElevated: Color = Color(0xFF1A1A1A), + val backgroundCard: Color = Color(0xFF242424) +) + +object ThemeColors { + + val Crimson = ThemeColorPalette( + secondary = Color(0xFFE53935), + secondaryVariant = Color(0xFFC62828), + focusRing = Color(0xFFFF5252), + focusBackground = Color(0xFF3D1A1A), + background = Color(0xFF0D0D0D), + backgroundElevated = Color(0xFF1A1A1A), + backgroundCard = Color(0xFF241A1A) // Warm red tint + ) + + val Ocean = ThemeColorPalette( + secondary = Color(0xFF1E88E5), + secondaryVariant = Color(0xFF1565C0), + focusRing = Color(0xFF42A5F5), + focusBackground = Color(0xFF1A2D3D), + background = Color(0xFF0D0D0F), // Cool blue tint + backgroundElevated = Color(0xFF1A1A1E), + backgroundCard = Color(0xFF1A1F24) + ) + + val Violet = ThemeColorPalette( + secondary = Color(0xFF8E24AA), + secondaryVariant = Color(0xFF6A1B9A), + focusRing = Color(0xFFAB47BC), + focusBackground = Color(0xFF2D1A3D), + background = Color(0xFF0D0D0F), // Purple tint + backgroundElevated = Color(0xFF1A1A1E), + backgroundCard = Color(0xFF1F1A24) + ) + + val Emerald = ThemeColorPalette( + secondary = Color(0xFF43A047), + secondaryVariant = Color(0xFF2E7D32), + focusRing = Color(0xFF66BB6A), + focusBackground = Color(0xFF1A3D1E), + background = Color(0xFF0D0D0D), + backgroundElevated = Color(0xFF1A1A1A), + backgroundCard = Color(0xFF1A241A) // Green tint + ) + + val Amber = ThemeColorPalette( + secondary = Color(0xFFFB8C00), + secondaryVariant = Color(0xFFEF6C00), + focusRing = Color(0xFFFFA726), + focusBackground = Color(0xFF3D2D1A), + background = Color(0xFF0F0D0D), // Warm amber tint + backgroundElevated = Color(0xFF1E1A1A), + backgroundCard = Color(0xFF24201A) + ) + + val Rose = ThemeColorPalette( + secondary = Color(0xFFD81B60), + secondaryVariant = Color(0xFFC2185B), + focusRing = Color(0xFFEC407A), + focusBackground = Color(0xFF3D1A2D), + background = Color(0xFF0D0D0D), + backgroundElevated = Color(0xFF1A1A1A), + backgroundCard = Color(0xFF241A1F) // Pink tint + ) + + fun getColorPalette(theme: AppTheme): ThemeColorPalette { + return when (theme) { + AppTheme.CRIMSON -> Crimson + AppTheme.OCEAN -> Ocean + AppTheme.VIOLET -> Violet + AppTheme.EMERALD -> Emerald + AppTheme.AMBER -> Amber + AppTheme.ROSE -> Rose + } + } +}