mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-19 16:31:44 +00:00
Implement theme selection system with 6 color themes
Add settings screen for users to choose between Crimson, Ocean, Violet, Emerald, Amber, and Rose themes. Each theme customizes accent colors (focus states, buttons) and includes subtle background tinting for a cohesive visual experience. Theme selection persists via DataStore.
This commit is contained in:
parent
e505570770
commit
c82d94c71e
21 changed files with 624 additions and 75 deletions
|
|
@ -8,9 +8,7 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.nuvio.tv"
|
namespace = "com.nuvio.tv"
|
||||||
compileSdk {
|
compileSdk = 36
|
||||||
version = release(36)
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "com.nuvio.tv"
|
applicationId = "com.nuvio.tv"
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.unit.dp
|
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.Search
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import coil.compose.AsyncImage
|
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.NuvioNavHost
|
||||||
import com.nuvio.tv.ui.navigation.Screen
|
import com.nuvio.tv.ui.navigation.Screen
|
||||||
import com.nuvio.tv.ui.theme.NuvioTheme
|
import com.nuvio.tv.ui.theme.NuvioTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var themeDataStore: ThemeDataStore
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
NuvioTheme {
|
val currentTheme by themeDataStore.selectedTheme.collectAsState(initial = AppTheme.CRIMSON)
|
||||||
|
|
||||||
|
NuvioTheme(appTheme = currentTheme) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
shape = RectangleShape
|
shape = RectangleShape
|
||||||
|
|
|
||||||
40
app/src/main/java/com/nuvio/tv/data/local/ThemeDataStore.kt
Normal file
40
app/src/main/java/com/nuvio/tv/data/local/ThemeDataStore.kt
Normal file
|
|
@ -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<Preferences> 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<AppTheme> = 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
app/src/main/java/com/nuvio/tv/domain/model/AppTheme.kt
Normal file
10
app/src/main/java/com/nuvio/tv/domain/model/AppTheme.kt
Normal file
|
|
@ -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")
|
||||||
|
}
|
||||||
|
|
@ -28,7 +28,6 @@ import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.Card
|
import androidx.tv.material3.Card
|
||||||
import androidx.tv.material3.CardDefaults
|
import androidx.tv.material3.CardDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
import androidx.tv.material3.Glow
|
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
|
@ -79,18 +78,12 @@ fun ContentCard(
|
||||||
),
|
),
|
||||||
border = CardDefaults.border(
|
border = CardDefaults.border(
|
||||||
focusedBorder = Border(
|
focusedBorder = Border(
|
||||||
border = BorderStroke(3.dp, NuvioColors.FocusRing),
|
border = BorderStroke(2.dp, NuvioColors.FocusRing),
|
||||||
shape = RoundedCornerShape(8.dp)
|
shape = RoundedCornerShape(8.dp)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
scale = CardDefaults.scale(
|
scale = CardDefaults.scale(
|
||||||
focusedScale = 1.08f
|
focusedScale = 1.05f
|
||||||
),
|
|
||||||
glow = CardDefaults.glow(
|
|
||||||
focusedGlow = Glow(
|
|
||||||
elevation = 8.dp,
|
|
||||||
elevationColor = NuvioColors.FocusRing.copy(alpha = 0.3f)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ private fun ContinueWatchingCard(
|
||||||
),
|
),
|
||||||
colors = CardDefaults.colors(
|
colors = CardDefaults.colors(
|
||||||
containerColor = NuvioColors.BackgroundCard,
|
containerColor = NuvioColors.BackgroundCard,
|
||||||
focusedContainerColor = NuvioColors.BackgroundCard
|
focusedContainerColor = NuvioColors.FocusBackground
|
||||||
),
|
),
|
||||||
border = CardDefaults.border(
|
border = CardDefaults.border(
|
||||||
focusedBorder = Border(
|
focusedBorder = Border(
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ private fun SidebarNavItem(
|
||||||
label = "navItemBackground"
|
label = "navItemBackground"
|
||||||
)
|
)
|
||||||
val borderColor by animateColorAsState(
|
val borderColor by animateColorAsState(
|
||||||
targetValue = if (isFocused) NuvioColors.BorderFocused else Color.Transparent,
|
targetValue = if (isFocused) NuvioColors.FocusRing else Color.Transparent,
|
||||||
label = "navItemBorder"
|
label = "navItemBorder"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -121,7 +121,7 @@ private fun SidebarNavItem(
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(backgroundColor)
|
.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)
|
.then(if (focusRequester != null) Modifier.focusRequester(focusRequester) else Modifier)
|
||||||
.onFocusChanged { state ->
|
.onFocusChanged { state ->
|
||||||
isFocused = state.isFocused
|
isFocused = state.isFocused
|
||||||
|
|
|
||||||
|
|
@ -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.plugin.PluginScreen
|
||||||
import com.nuvio.tv.ui.screens.search.SearchScreen
|
import com.nuvio.tv.ui.screens.search.SearchScreen
|
||||||
import com.nuvio.tv.ui.screens.settings.SettingsScreen
|
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.settings.TmdbSettingsScreen
|
||||||
import com.nuvio.tv.ui.screens.stream.StreamScreen
|
import com.nuvio.tv.ui.screens.stream.StreamScreen
|
||||||
|
|
||||||
|
|
@ -240,7 +241,8 @@ fun NuvioNavHost(
|
||||||
composable(Screen.Settings.route) {
|
composable(Screen.Settings.route) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
onNavigateToPlugins = { navController.navigate(Screen.Plugins.route) },
|
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) {
|
composable(Screen.AddonManager.route) {
|
||||||
AddonManagerScreen()
|
AddonManagerScreen()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ sealed class Screen(val route: String) {
|
||||||
data object Library : Screen("library")
|
data object Library : Screen("library")
|
||||||
data object Settings : Screen("settings")
|
data object Settings : Screen("settings")
|
||||||
data object TmdbSettings : Screen("tmdb_settings")
|
data object TmdbSettings : Screen("tmdb_settings")
|
||||||
|
data object ThemeSettings : Screen("theme_settings")
|
||||||
data object AddonManager : Screen("addon_manager")
|
data object AddonManager : Screen("addon_manager")
|
||||||
data object Plugins : Screen("plugins")
|
data object Plugins : Screen("plugins")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ fun SeasonTabs(
|
||||||
),
|
),
|
||||||
colors = CardDefaults.colors(
|
colors = CardDefaults.colors(
|
||||||
containerColor = if (isSelected) NuvioColors.SurfaceVariant else NuvioColors.BackgroundCard,
|
containerColor = if (isSelected) NuvioColors.SurfaceVariant else NuvioColors.BackgroundCard,
|
||||||
focusedContainerColor = NuvioColors.Primary
|
focusedContainerColor = NuvioColors.Secondary
|
||||||
),
|
),
|
||||||
border = CardDefaults.border(
|
border = CardDefaults.border(
|
||||||
focusedBorder = Border(
|
focusedBorder = Border(
|
||||||
|
|
@ -160,7 +160,7 @@ private fun EpisodeCard(
|
||||||
),
|
),
|
||||||
colors = CardDefaults.colors(
|
colors = CardDefaults.colors(
|
||||||
containerColor = NuvioColors.BackgroundCard,
|
containerColor = NuvioColors.BackgroundCard,
|
||||||
focusedContainerColor = NuvioColors.BackgroundCard
|
focusedContainerColor = NuvioColors.FocusBackground
|
||||||
),
|
),
|
||||||
border = CardDefaults.border(
|
border = CardDefaults.border(
|
||||||
focusedBorder = Border(
|
focusedBorder = Border(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.nuvio.tv.ui.screens.detail
|
package com.nuvio.tv.ui.screens.detail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
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.layout.ContentScale
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ButtonDefaults
|
import androidx.tv.material3.ButtonDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
|
@ -164,13 +166,19 @@ private fun PlayButton(
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
.onFocusChanged { isFocused = it.isFocused },
|
||||||
colors = ButtonDefaults.colors(
|
colors = ButtonDefaults.colors(
|
||||||
containerColor = androidx.compose.ui.graphics.Color.White,
|
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,
|
contentColor = androidx.compose.ui.graphics.Color.Black,
|
||||||
focusedContentColor = androidx.compose.ui.graphics.Color.Black
|
focusedContentColor = androidx.compose.ui.graphics.Color.Black
|
||||||
),
|
),
|
||||||
shape = ButtonDefaults.shape(
|
shape = ButtonDefaults.shape(
|
||||||
shape = RoundedCornerShape(32.dp)
|
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)
|
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
@ -206,10 +214,16 @@ private fun ActionIconButton(
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
.onFocusChanged { isFocused = it.isFocused },
|
||||||
colors = IconButtonDefaults.colors(
|
colors = IconButtonDefaults.colors(
|
||||||
containerColor = NuvioColors.BackgroundCard,
|
containerColor = NuvioColors.BackgroundCard,
|
||||||
focusedContainerColor = NuvioColors.Primary,
|
focusedContainerColor = NuvioColors.Secondary,
|
||||||
contentColor = NuvioColors.TextPrimary,
|
contentColor = NuvioColors.TextPrimary,
|
||||||
focusedContentColor = NuvioColors.OnPrimary
|
focusedContentColor = NuvioColors.OnPrimary
|
||||||
),
|
),
|
||||||
|
border = IconButtonDefaults.border(
|
||||||
|
focusedBorder = Border(
|
||||||
|
border = BorderStroke(2.dp, NuvioColors.FocusRing),
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
),
|
||||||
shape = IconButtonDefaults.shape(
|
shape = IconButtonDefaults.shape(
|
||||||
shape = CircleShape
|
shape = CircleShape
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ private fun LibraryTabs(
|
||||||
shape = CardDefaults.shape(shape = RoundedCornerShape(20.dp)),
|
shape = CardDefaults.shape(shape = RoundedCornerShape(20.dp)),
|
||||||
colors = CardDefaults.colors(
|
colors = CardDefaults.colors(
|
||||||
containerColor = if (isSelected) NuvioColors.SurfaceVariant else NuvioColors.BackgroundCard,
|
containerColor = if (isSelected) NuvioColors.SurfaceVariant else NuvioColors.BackgroundCard,
|
||||||
focusedContainerColor = NuvioColors.Primary
|
focusedContainerColor = NuvioColors.Secondary
|
||||||
),
|
),
|
||||||
border = CardDefaults.border(
|
border = CardDefaults.border(
|
||||||
focusedBorder = Border(
|
focusedBorder = Border(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -54,6 +55,7 @@ import androidx.compose.ui.unit.sp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
import androidx.tv.foundation.lazy.list.items
|
import androidx.tv.foundation.lazy.list.items
|
||||||
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.Button
|
import androidx.tv.material3.Button
|
||||||
import androidx.tv.material3.ButtonDefaults
|
import androidx.tv.material3.ButtonDefaults
|
||||||
import androidx.tv.material3.ClickableSurfaceDefaults
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
|
|
@ -265,8 +267,16 @@ private fun PluginHeader(
|
||||||
Button(
|
Button(
|
||||||
onClick = onAddRepository,
|
onClick = onAddRepository,
|
||||||
colors = ButtonDefaults.colors(
|
colors = ButtonDefaults.colors(
|
||||||
containerColor = NuvioColors.Primary,
|
containerColor = NuvioColors.Secondary,
|
||||||
contentColor = Color.White
|
focusedContainerColor = NuvioColors.SecondaryVariant,
|
||||||
|
contentColor = Color.White,
|
||||||
|
focusedContentColor = Color.White
|
||||||
|
),
|
||||||
|
border = ButtonDefaults.border(
|
||||||
|
focusedBorder = Border(
|
||||||
|
border = BorderStroke(2.dp, NuvioColors.FocusRing),
|
||||||
|
shape = RoundedCornerShape(50)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -296,9 +306,15 @@ private fun RepositoryCard(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
.onFocusChanged { isFocused = it.isFocused },
|
||||||
colors = ClickableSurfaceDefaults.colors(
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
containerColor = if (isFocused) NuvioColors.FocusBackground else NuvioColors.BackgroundCard,
|
containerColor = NuvioColors.BackgroundCard,
|
||||||
focusedContainerColor = NuvioColors.FocusBackground
|
focusedContainerColor = NuvioColors.FocusBackground
|
||||||
),
|
),
|
||||||
|
border = ClickableSurfaceDefaults.border(
|
||||||
|
focusedBorder = Border(
|
||||||
|
border = BorderStroke(2.dp, NuvioColors.FocusRing),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
),
|
||||||
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp))
|
shape = ClickableSurfaceDefaults.shape(RoundedCornerShape(12.dp))
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
package com.nuvio.tv.ui.screens.settings
|
package com.nuvio.tv.ui.screens.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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.ArrowForward
|
||||||
import androidx.compose.material.icons.filled.Build
|
import androidx.compose.material.icons.filled.Build
|
||||||
import androidx.compose.material.icons.filled.Info
|
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.Settings
|
||||||
import androidx.compose.material.icons.filled.Tune
|
import androidx.compose.material.icons.filled.Tune
|
||||||
import androidx.compose.runtime.Composable
|
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.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.Card
|
import androidx.tv.material3.Card
|
||||||
import androidx.tv.material3.CardDefaults
|
import androidx.tv.material3.CardDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
|
@ -43,7 +46,8 @@ import com.nuvio.tv.ui.theme.NuvioColors
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen(
|
fun SettingsScreen(
|
||||||
onNavigateToPlugins: () -> Unit = {},
|
onNavigateToPlugins: () -> Unit = {},
|
||||||
onNavigateToTmdb: () -> Unit = {}
|
onNavigateToTmdb: () -> Unit = {},
|
||||||
|
onNavigateToTheme: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -73,6 +77,15 @@ fun SettingsScreen(
|
||||||
contentPadding = PaddingValues(bottom = 32.dp),
|
contentPadding = PaddingValues(bottom = 32.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
item {
|
||||||
|
SettingsItem(
|
||||||
|
icon = Icons.Default.Palette,
|
||||||
|
title = "Appearance",
|
||||||
|
subtitle = "Choose your color theme",
|
||||||
|
onClick = onNavigateToTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
SettingsItem(
|
SettingsItem(
|
||||||
icon = Icons.Default.Build,
|
icon = Icons.Default.Build,
|
||||||
|
|
@ -127,7 +140,14 @@ private fun SettingsItem(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
.onFocusChanged { isFocused = it.isFocused },
|
||||||
colors = CardDefaults.colors(
|
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))
|
shape = CardDefaults.shape(RoundedCornerShape(12.dp))
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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> = 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<ThemeSettingsUiState> = _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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
package com.nuvio.tv.ui.screens.settings
|
package com.nuvio.tv.ui.screens.settings
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
|
@ -28,6 +29,7 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
import androidx.tv.foundation.lazy.list.TvLazyColumn
|
||||||
|
import androidx.tv.material3.Border
|
||||||
import androidx.tv.material3.Card
|
import androidx.tv.material3.Card
|
||||||
import androidx.tv.material3.CardDefaults
|
import androidx.tv.material3.CardDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
|
@ -171,7 +173,14 @@ private fun ToggleCard(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onFocusChanged { isFocused = it.isFocused },
|
.onFocusChanged { isFocused = it.isFocused },
|
||||||
colors = CardDefaults.colors(
|
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))
|
shape = CardDefaults.shape(RoundedCornerShape(12.dp))
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -497,7 +497,13 @@ private fun ErrorState(
|
||||||
modifier = Modifier.onFocusChanged { isFocused = it.isFocused },
|
modifier = Modifier.onFocusChanged { isFocused = it.isFocused },
|
||||||
colors = CardDefaults.colors(
|
colors = CardDefaults.colors(
|
||||||
containerColor = NuvioColors.BackgroundCard,
|
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))
|
shape = CardDefaults.shape(shape = RoundedCornerShape(8.dp))
|
||||||
) {
|
) {
|
||||||
|
|
@ -583,7 +589,7 @@ private fun StreamCard(
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
shape = CardDefaults.shape(shape = RoundedCornerShape(12.dp)),
|
shape = CardDefaults.shape(shape = RoundedCornerShape(12.dp)),
|
||||||
scale = CardDefaults.scale(focusedScale = 1.02f)
|
scale = CardDefaults.scale(focusedScale = 1.05f)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,122 @@
|
||||||
package com.nuvio.tv.ui.theme
|
package com.nuvio.tv.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import com.nuvio.tv.domain.model.AppTheme
|
||||||
|
|
||||||
object NuvioColors {
|
/**
|
||||||
// Primary Background - Deep charcoal (NO gradients)
|
* Dynamic color scheme that changes based on selected theme.
|
||||||
val Background = Color(0xFF0D0D0D)
|
* Background colors have subtle theme tinting.
|
||||||
val BackgroundElevated = Color(0xFF1A1A1A)
|
* Accent colors (secondary, focus) change per theme.
|
||||||
val BackgroundCard = Color(0xFF242424)
|
*/
|
||||||
|
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 Surface = Color(0xFF1E1E1E)
|
||||||
val SurfaceVariant = Color(0xFF2D2D2D)
|
val SurfaceVariant = Color(0xFF2D2D2D)
|
||||||
|
|
||||||
// Primary accent - Neutral Grey
|
// Primary accent - Neutral Grey (constant)
|
||||||
val Primary = Color(0xFF9E9E9E)
|
val Primary = Color(0xFF9E9E9E)
|
||||||
val PrimaryVariant = Color(0xFF6F6F6F)
|
val PrimaryVariant = Color(0xFF6F6F6F)
|
||||||
val OnPrimary = Color(0xFFFFFFFF)
|
val OnPrimary = Color(0xFFFFFFFF)
|
||||||
|
|
||||||
// Secondary accent - Teal
|
// Secondary accent - Theme dependent
|
||||||
val Secondary = Color(0xFF00BFA6)
|
val Secondary = palette.secondary
|
||||||
val SecondaryVariant = Color(0xFF008F7A)
|
val SecondaryVariant = palette.secondaryVariant
|
||||||
|
|
||||||
// Text colors
|
// Text colors (constant)
|
||||||
val TextPrimary = Color(0xFFFFFFFF)
|
val TextPrimary = Color(0xFFFFFFFF)
|
||||||
val TextSecondary = Color(0xFFB3B3B3)
|
val TextSecondary = Color(0xFFB3B3B3)
|
||||||
val TextTertiary = Color(0xFF808080)
|
val TextTertiary = Color(0xFF808080)
|
||||||
val TextDisabled = Color(0xFF4D4D4D)
|
val TextDisabled = Color(0xFF4D4D4D)
|
||||||
|
|
||||||
// Focus states - Critical for TV navigation
|
// Focus states - Theme dependent
|
||||||
val FocusRing = Color(0xFF00E5CC) // Bright teal for high visibility
|
val FocusRing = palette.focusRing
|
||||||
val FocusBackground = Color(0xFF1A3D38) // Dark teal background
|
val FocusBackground = palette.focusBackground
|
||||||
|
|
||||||
// Status colors
|
// Status colors (constant)
|
||||||
val Rating = Color(0xFFFFD700)
|
val Rating = Color(0xFFFFD700)
|
||||||
val Error = Color(0xFFCF6679)
|
val Error = Color(0xFFCF6679)
|
||||||
val Success = Color(0xFF4CAF50)
|
val Success = Color(0xFF4CAF50)
|
||||||
|
|
||||||
// Borders
|
// Borders
|
||||||
val Border = Color(0xFF333333)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ package com.nuvio.tv.ui.theme
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.staticCompositionLocalOf
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.darkColorScheme
|
import androidx.tv.material3.darkColorScheme
|
||||||
|
import com.nuvio.tv.domain.model.AppTheme
|
||||||
|
|
||||||
data class NuvioExtendedColors(
|
data class NuvioExtendedColors(
|
||||||
val backgroundElevated: Color,
|
val backgroundElevated: Color,
|
||||||
|
|
@ -19,48 +21,62 @@ data class NuvioExtendedColors(
|
||||||
)
|
)
|
||||||
|
|
||||||
val LocalNuvioColors = staticCompositionLocalOf {
|
val LocalNuvioColors = staticCompositionLocalOf {
|
||||||
|
NuvioColorScheme(ThemeColors.Crimson)
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalNuvioExtendedColors = staticCompositionLocalOf {
|
||||||
NuvioExtendedColors(
|
NuvioExtendedColors(
|
||||||
backgroundElevated = NuvioColors.BackgroundElevated,
|
backgroundElevated = Color(0xFF1A1A1A),
|
||||||
backgroundCard = NuvioColors.BackgroundCard,
|
backgroundCard = Color(0xFF242424),
|
||||||
textSecondary = NuvioColors.TextSecondary,
|
textSecondary = Color(0xFFB3B3B3),
|
||||||
textTertiary = NuvioColors.TextTertiary,
|
textTertiary = Color(0xFF808080),
|
||||||
focusRing = NuvioColors.FocusRing,
|
focusRing = ThemeColors.Crimson.focusRing,
|
||||||
focusBackground = NuvioColors.FocusBackground,
|
focusBackground = ThemeColors.Crimson.focusBackground,
|
||||||
rating = NuvioColors.Rating
|
rating = Color(0xFFFFD700)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalAppTheme = staticCompositionLocalOf { AppTheme.CRIMSON }
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NuvioTheme(
|
fun NuvioTheme(
|
||||||
|
appTheme: AppTheme = AppTheme.CRIMSON,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val colorScheme = darkColorScheme(
|
val palette = ThemeColors.getColorPalette(appTheme)
|
||||||
primary = NuvioColors.Primary,
|
val colorScheme = NuvioColorScheme(palette)
|
||||||
onPrimary = NuvioColors.OnPrimary,
|
|
||||||
secondary = NuvioColors.Secondary,
|
val materialColorScheme = darkColorScheme(
|
||||||
background = NuvioColors.Background,
|
primary = colorScheme.Primary,
|
||||||
surface = NuvioColors.Surface,
|
onPrimary = colorScheme.OnPrimary,
|
||||||
surfaceVariant = NuvioColors.SurfaceVariant,
|
secondary = colorScheme.Secondary,
|
||||||
onBackground = NuvioColors.TextPrimary,
|
background = colorScheme.Background,
|
||||||
onSurface = NuvioColors.TextPrimary,
|
surface = colorScheme.Surface,
|
||||||
onSurfaceVariant = NuvioColors.TextSecondary,
|
surfaceVariant = colorScheme.SurfaceVariant,
|
||||||
error = NuvioColors.Error
|
onBackground = colorScheme.TextPrimary,
|
||||||
|
onSurface = colorScheme.TextPrimary,
|
||||||
|
onSurfaceVariant = colorScheme.TextSecondary,
|
||||||
|
error = colorScheme.Error
|
||||||
)
|
)
|
||||||
|
|
||||||
val extendedColors = NuvioExtendedColors(
|
val extendedColors = NuvioExtendedColors(
|
||||||
backgroundElevated = NuvioColors.BackgroundElevated,
|
backgroundElevated = colorScheme.BackgroundElevated,
|
||||||
backgroundCard = NuvioColors.BackgroundCard,
|
backgroundCard = colorScheme.BackgroundCard,
|
||||||
textSecondary = NuvioColors.TextSecondary,
|
textSecondary = colorScheme.TextSecondary,
|
||||||
textTertiary = NuvioColors.TextTertiary,
|
textTertiary = colorScheme.TextTertiary,
|
||||||
focusRing = NuvioColors.FocusRing,
|
focusRing = colorScheme.FocusRing,
|
||||||
focusBackground = NuvioColors.FocusBackground,
|
focusBackground = colorScheme.FocusBackground,
|
||||||
rating = NuvioColors.Rating
|
rating = colorScheme.Rating
|
||||||
)
|
)
|
||||||
|
|
||||||
CompositionLocalProvider(LocalNuvioColors provides extendedColors) {
|
CompositionLocalProvider(
|
||||||
|
LocalNuvioColors provides colorScheme,
|
||||||
|
LocalNuvioExtendedColors provides extendedColors,
|
||||||
|
LocalAppTheme provides appTheme
|
||||||
|
) {
|
||||||
MaterialTheme(
|
MaterialTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = materialColorScheme,
|
||||||
typography = NuvioTypography,
|
typography = NuvioTypography,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
|
@ -68,7 +84,18 @@ fun NuvioTheme(
|
||||||
}
|
}
|
||||||
|
|
||||||
object NuvioTheme {
|
object NuvioTheme {
|
||||||
|
val colors: NuvioColorScheme
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = LocalNuvioColors.current
|
||||||
|
|
||||||
val extendedColors: NuvioExtendedColors
|
val extendedColors: NuvioExtendedColors
|
||||||
@Composable
|
@Composable
|
||||||
get() = LocalNuvioColors.current
|
@ReadOnlyComposable
|
||||||
|
get() = LocalNuvioExtendedColors.current
|
||||||
|
|
||||||
|
val currentTheme: AppTheme
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
get() = LocalAppTheme.current
|
||||||
}
|
}
|
||||||
|
|
|
||||||
93
app/src/main/java/com/nuvio/tv/ui/theme/ThemeColors.kt
Normal file
93
app/src/main/java/com/nuvio/tv/ui/theme/ThemeColors.kt
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue