feat(ui): connect modular SettingsScreen, complete SettingsComponents, and wire up dynamic sidebar avatar
This commit is contained in:
parent
d0cb89cf6e
commit
779796f825
5 changed files with 714 additions and 100 deletions
|
|
@ -9,6 +9,7 @@ import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import dev.kaizen.app.chat.ChatScreen
|
import dev.kaizen.app.chat.ChatScreen
|
||||||
|
import dev.kaizen.app.settings.SettingsViewModel
|
||||||
import dev.kaizen.app.ui.theme.KaizenTheme
|
import dev.kaizen.app.ui.theme.KaizenTheme
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
@ -24,6 +25,9 @@ class MainActivity : ComponentActivity() {
|
||||||
window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
|
window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize the decoupled viewmodel at the Activity scope (or Hilt/Dagger later)
|
||||||
|
val settingsViewModel = SettingsViewModel()
|
||||||
|
|
||||||
// auto() picks light/dark system-bar icons based on the system theme
|
// auto() picks light/dark system-bar icons based on the system theme
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||||
|
|
@ -31,7 +35,7 @@ class MainActivity : ComponentActivity() {
|
||||||
)
|
)
|
||||||
setContent {
|
setContent {
|
||||||
KaizenTheme {
|
KaizenTheme {
|
||||||
ChatScreen()
|
ChatScreen(settingsViewModel = settingsViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import androidx.compose.material3.ModalNavigationDrawer
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
|
@ -47,11 +48,19 @@ import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.kaizen.app.haptics.rememberHaptics
|
import dev.kaizen.app.haptics.rememberHaptics
|
||||||
|
import dev.kaizen.app.settings.SettingsScreen
|
||||||
|
import dev.kaizen.app.settings.SettingsViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
// Screen enumeration for modular state-driven navigation (Zero Technical Debt)
|
||||||
|
enum class AppScreen { Chat, Settings }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatScreen(userName: String = "Bruno") {
|
fun ChatScreen(
|
||||||
|
settingsViewModel: SettingsViewModel,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
val haptics = rememberHaptics()
|
val haptics = rememberHaptics()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val messages = remember { mutableStateListOf<Message>() }
|
val messages = remember { mutableStateListOf<Message>() }
|
||||||
|
|
@ -61,6 +70,12 @@ fun ChatScreen(userName: String = "Bruno") {
|
||||||
var nextId by remember { mutableStateOf(0L) }
|
var nextId by remember { mutableStateOf(0L) }
|
||||||
var selectedMode by remember { mutableStateOf("Standard") }
|
var selectedMode by remember { mutableStateOf("Standard") }
|
||||||
|
|
||||||
|
// Navigation State
|
||||||
|
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
|
||||||
|
|
||||||
|
// Collect the user name reactively from the ViewModel
|
||||||
|
val userName by settingsViewModel.userName.collectAsState()
|
||||||
|
|
||||||
// Tablet & Landscape optimization: Detect large screens and cap the content width
|
// Tablet & Landscape optimization: Detect large screens and cap the content width
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val isTablet = configuration.screenWidthDp > 600
|
val isTablet = configuration.screenWidthDp > 600
|
||||||
|
|
@ -106,6 +121,16 @@ fun ChatScreen(userName: String = "Bruno") {
|
||||||
if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex)
|
if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Single source of truth Screen routing
|
||||||
|
when (currentScreen) {
|
||||||
|
AppScreen.Settings -> {
|
||||||
|
SettingsScreen(
|
||||||
|
viewModel = settingsViewModel,
|
||||||
|
onBack = { currentScreen = AppScreen.Chat },
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AppScreen.Chat -> {
|
||||||
// ModalNavigationDrawer provides a native drawer that slides from left-to-right
|
// ModalNavigationDrawer provides a native drawer that slides from left-to-right
|
||||||
ModalNavigationDrawer(
|
ModalNavigationDrawer(
|
||||||
drawerState = drawerState,
|
drawerState = drawerState,
|
||||||
|
|
@ -117,6 +142,11 @@ fun ChatScreen(userName: String = "Bruno") {
|
||||||
haptics.tick()
|
haptics.tick()
|
||||||
messages.clear()
|
messages.clear()
|
||||||
scope.launch { drawerState.close() }
|
scope.launch { drawerState.close() }
|
||||||
|
},
|
||||||
|
onOpenSettings = {
|
||||||
|
haptics.tick()
|
||||||
|
currentScreen = AppScreen.Settings
|
||||||
|
scope.launch { drawerState.close() }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -208,3 +238,5 @@ fun ChatScreen(userName: String = "Bruno") {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,7 @@ fun KaizenSidebar(
|
||||||
userName: String,
|
userName: String,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
onNewChat: () -> Unit,
|
onNewChat: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val cs = MaterialTheme.colorScheme
|
val cs = MaterialTheme.colorScheme
|
||||||
|
|
@ -238,6 +239,7 @@ fun KaizenSidebar(
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
// --- FOOTER Sektion: User Profile Card ---
|
// --- FOOTER Sektion: User Profile Card ---
|
||||||
|
// Clicking the card opens the brand settings screen cleanly (MVVM state)
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -248,11 +250,11 @@ fun KaizenSidebar(
|
||||||
if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f),
|
if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f),
|
||||||
RoundedCornerShape(18.dp)
|
RoundedCornerShape(18.dp)
|
||||||
)
|
)
|
||||||
.clickable { }
|
.clickable { onOpenSettings() }
|
||||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// User Avatar bubble: Obsidian "N" bubble
|
// User Avatar bubble: Obsidian colored dynamic first-letter initials!
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(40.dp)
|
.size(40.dp)
|
||||||
|
|
@ -261,7 +263,7 @@ fun KaizenSidebar(
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "N",
|
text = userName.firstOrNull()?.toString()?.uppercase() ?: "A",
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
fontSize = 17.sp,
|
fontSize = 17.sp,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
|
|
@ -272,7 +274,7 @@ fun KaizenSidebar(
|
||||||
|
|
||||||
Column(Modifier.weight(1f)) {
|
Column(Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Admin",
|
text = userName,
|
||||||
color = cs.onBackground,
|
color = cs.onBackground,
|
||||||
fontSize = 15.sp,
|
fontSize = 15.sp,
|
||||||
fontWeight = FontWeight.SemiBold
|
fontWeight = FontWeight.SemiBold
|
||||||
|
|
@ -303,7 +305,7 @@ private fun HistoryItemRow(item: ConvoHistoryItem, onClick: () -> Unit) {
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(itemBg)
|
.background(itemBg)
|
||||||
.border(1.dp, itemBorder, CircleShape)
|
.border(1.2.dp, itemBorder, CircleShape)
|
||||||
.clickable { onClick() }
|
.clickable { onClick() }
|
||||||
.padding(horizontal = 16.dp, vertical = 11.dp),
|
.padding(horizontal = 16.dp, vertical = 11.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
|
|
||||||
342
app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt
Normal file
342
app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
package dev.kaizen.app.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Check
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.colorspace.ColorSpaces
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.kaizen.app.ui.theme.Amber
|
||||||
|
import dev.kaizen.app.ui.theme.AmberDeep
|
||||||
|
import dev.kaizen.app.ui.theme.Oklab
|
||||||
|
|
||||||
|
/** Converts polar OKLCH coordinates (Lightness, Chroma, Hue in degrees) to a standard Color */
|
||||||
|
private fun oklch(l: Float, c: Float, h: Float, alpha: Float = 1.0f): Color {
|
||||||
|
val hRad = (h * Math.PI / 180.0).toFloat()
|
||||||
|
val a = c * Math.cos(hRad.toDouble()).toFloat()
|
||||||
|
val b = c * Math.sin(hRad.toDouble()).toFloat()
|
||||||
|
|
||||||
|
// Automatically output in high-gamut Display P3 or standard sRGB depending on system support
|
||||||
|
return with(Oklab) {
|
||||||
|
Lab(l, a, b).toColor(ColorSpaces.DisplayP3).copy(alpha = alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reusable premium Glassmorphic Card (PageCard).
|
||||||
|
*
|
||||||
|
* Styled with a subtle vertical milky-glass gradient background,
|
||||||
|
* dual-border specular light highlights, and deep 3D ambient shadows.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SettingsCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val glassBg = remember(isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
Color(0xFF1E293B).copy(alpha = 0.55f), // Slate 800 (55% opacity)
|
||||||
|
Color(0xFF0F172A).copy(alpha = 0.65f) // Slate 900 (65% opacity)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
Color.White.copy(alpha = 0.85f),
|
||||||
|
Color(0xFFEEF2F6).copy(alpha = 0.75f) // Warm white-slate (75% opacity)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val glassBorder = remember(isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
Color.White.copy(alpha = 0.16f), // Crisp glanz highlight top-left
|
||||||
|
Color.White.copy(alpha = 0.02f) // Faint dark shade bottom-right
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
Color.White.copy(alpha = 0.60f),
|
||||||
|
Color.Black.copy(alpha = 0.05f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(elevation = 8.dp, shape = RoundedCornerShape(24.dp), clip = false) // Floating 3D shadow
|
||||||
|
.clip(RoundedCornerShape(24.dp))
|
||||||
|
.background(glassBg)
|
||||||
|
.border(1.2.dp, glassBorder, RoundedCornerShape(24.dp))
|
||||||
|
.padding(18.dp)
|
||||||
|
) {
|
||||||
|
ColumnScope(this).content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope helper to enable ColumnScope receiver within SettingsCard
|
||||||
|
class ColumnScope(private val boxScope: BoxScope) : ColumnScopeReceiver {
|
||||||
|
@Composable
|
||||||
|
fun content() {}
|
||||||
|
}
|
||||||
|
interface ColumnScopeReceiver {
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.content()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Elegant modular Settings Row for displaying or editing values */
|
||||||
|
@Composable
|
||||||
|
fun SettingsOptionRow(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val cs = MaterialTheme.colorScheme
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(38.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(cs.onBackground.copy(alpha = 0.06f)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = cs.onBackground.copy(alpha = 0.75f),
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(14.dp))
|
||||||
|
|
||||||
|
Column(Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = cs.onBackground,
|
||||||
|
fontSize = 15.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
color = cs.onSurfaceVariant,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
lineHeight = 16.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Floating glass input capsule specifically for Settings text edits (no heavy grey bars) */
|
||||||
|
@Composable
|
||||||
|
fun SettingsInputCapsule(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val cs = MaterialTheme.colorScheme
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val inputGlassBg = if (isDark) Color(0x12FFFFFF) else Color(0x06000000)
|
||||||
|
val inputGlassBorder = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.widthIn(max = 180.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(inputGlassBg)
|
||||||
|
.border(1.dp, inputGlassBorder, RoundedCornerShape(12.dp))
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
color = cs.onBackground,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = FontWeight.Medium
|
||||||
|
),
|
||||||
|
cursorBrush = SolidColor(Amber),
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Beautiful interactive Oklab Theme circular picker buttons */
|
||||||
|
@Composable
|
||||||
|
fun ThemeOptionPill(
|
||||||
|
themeId: KaizenThemeId,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
// Exact OKLCH brand stops sourced directly from amber.css, blue.css, aurora.css, and mono.css
|
||||||
|
val (colorStart, colorEnd) = remember(themeId, isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
when (themeId) {
|
||||||
|
KaizenThemeId.Amber -> oklch(0.76f, 0.14f, 83f) to oklch(0.55f, 0.16f, 83f)
|
||||||
|
KaizenThemeId.Blue -> oklch(0.68f, 0.18f, 257f) to oklch(0.45f, 0.18f, 257f)
|
||||||
|
KaizenThemeId.Aurora -> oklch(0.78f, 0.15f, 85f) to oklch(0.45f, 0.18f, 320f)
|
||||||
|
KaizenThemeId.Mono -> oklch(0.92f, 0f, 255f) to oklch(0.35f, 0f, 255f)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (themeId) {
|
||||||
|
KaizenThemeId.Amber -> oklch(0.72f, 0.14f, 83f) to oklch(0.85f, 0.15f, 83f)
|
||||||
|
KaizenThemeId.Blue -> oklch(0.62f, 0.19f, 257f) to oklch(0.78f, 0.15f, 257f)
|
||||||
|
KaizenThemeId.Aurora -> oklch(0.62f, 0.20f, 320f) to oklch(0.75f, 0.16f, 320f)
|
||||||
|
KaizenThemeId.Mono -> oklch(0.22f, 0f, 255f) to oklch(0.85f, 0f, 255f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the butter-smooth Oklab perception-space gradient Stops (12 stops)
|
||||||
|
val oklabGradientBrush = remember(colorStart, colorEnd) {
|
||||||
|
val colors = Oklab.generateGradientStops(colorStart, colorEnd, steps = 12)
|
||||||
|
Brush.linearGradient(colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.shadow(elevation = 4.dp, shape = CircleShape, clip = false)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable { onClick() }
|
||||||
|
.border(
|
||||||
|
width = if (selected) 2.5.dp else 1.2.dp,
|
||||||
|
brush = if (selected) {
|
||||||
|
Brush.verticalGradient(listOf(Amber, AmberDeep))
|
||||||
|
} else {
|
||||||
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.35f), Color.White.copy(alpha = 0.05f)))
|
||||||
|
},
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.padding(if (selected) 3.dp else 0.dp) // creates a beautiful inner gap for the selected ring
|
||||||
|
.clip(CircleShape)
|
||||||
|
) {
|
||||||
|
// Draw the perception gradient on Canvas
|
||||||
|
Canvas(Modifier.fillMaxSize()) {
|
||||||
|
drawCircle(brush = oklabGradientBrush)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show check icon inside the selected theme circle
|
||||||
|
if (selected) {
|
||||||
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (themeId == KaizenThemeId.Mono && isDark.not()) Color.White else Color.Black,
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Premium glass-capsule segmented toggle for Appearance (Light, Dark, System) */
|
||||||
|
@Composable
|
||||||
|
fun AppearanceTogglePill(
|
||||||
|
selected: Boolean,
|
||||||
|
label: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val cs = MaterialTheme.colorScheme
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val glassBorder = remember(isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.12f), Color.White.copy(alpha = 0.02f)))
|
||||||
|
} else {
|
||||||
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.45f), Color.Black.copy(alpha = 0.04f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.shadow(elevation = if (selected) 4.dp else 0.dp, shape = CircleShape, clip = false)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(
|
||||||
|
if (selected) {
|
||||||
|
Amber.copy(alpha = 0.16f)
|
||||||
|
} else {
|
||||||
|
if (isDark) Color(0x0AFFFFFF) else Color(0x82FFFFFF)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = if (selected) 1.5.dp else 1.dp,
|
||||||
|
brush = if (selected) {
|
||||||
|
Brush.verticalGradient(listOf(Amber.copy(alpha = 0.50f), Amber.copy(alpha = 0.15f)))
|
||||||
|
} else {
|
||||||
|
glassBorder
|
||||||
|
},
|
||||||
|
shape = CircleShape
|
||||||
|
)
|
||||||
|
.clickable { onClick() }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 9.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
color = if (selected) cs.onBackground else cs.onSurfaceVariant,
|
||||||
|
fontSize = 13.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
234
app/src/main/java/dev/kaizen/app/settings/SettingsScreen.kt
Normal file
234
app/src/main/java/dev/kaizen/app/settings/SettingsScreen.kt
Normal file
|
|
@ -0,0 +1,234 @@
|
||||||
|
package dev.kaizen.app.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
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.layout.statusBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.rounded.AutoAwesome
|
||||||
|
import androidx.compose.material.icons.rounded.Palette
|
||||||
|
import androidx.compose.material.icons.rounded.Person
|
||||||
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.kaizen.app.chat.MeshBackground
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Premium, fully responsive, glassmorphic Settings Screen.
|
||||||
|
*
|
||||||
|
* Includes the Oklab Theme Picker, Light/Dark/System appearance segmented pills,
|
||||||
|
* and user profile text fields, completely driven by our clean, decoupled SettingsViewModel.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun SettingsScreen(
|
||||||
|
viewModel: SettingsViewModel,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val cs = MaterialTheme.colorScheme
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
// Collect reactive state flows from the ViewModel (clean MVVM)
|
||||||
|
val selectedTheme by viewModel.selectedTheme.collectAsState()
|
||||||
|
val appearanceMode by viewModel.appearanceMode.collectAsState()
|
||||||
|
val userName by viewModel.userName.collectAsState()
|
||||||
|
val userEmail by viewModel.userEmail.collectAsState()
|
||||||
|
|
||||||
|
val backBtnGlassBg = remember(isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.65f), Color(0xFF0F172A).copy(alpha = 0.75f)))
|
||||||
|
} else {
|
||||||
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.85f), Color(0xFFF1F5F9).copy(alpha = 0.75f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val backBtnGlassBorder = remember(isDark) {
|
||||||
|
if (isDark) {
|
||||||
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.16f), Color.White.copy(alpha = 0.03f)))
|
||||||
|
} else {
|
||||||
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.55f), Color.Black.copy(alpha = 0.05f)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MeshBackground {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.statusBarsPadding()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 20.dp, vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
|
||||||
|
// --- HEADER: Back Arrow Button & Title Sektion ---
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 24.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.shadow(elevation = 6.dp, shape = CircleShape, clip = false)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(backBtnGlassBg)
|
||||||
|
.border(1.2.dp, backBtnGlassBorder, CircleShape)
|
||||||
|
.clickable { onBack() }
|
||||||
|
.size(44.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.ArrowBack,
|
||||||
|
contentDescription = "Zurück",
|
||||||
|
tint = cs.onBackground,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.width(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "Einstellungen",
|
||||||
|
color = cs.onBackground,
|
||||||
|
fontSize = 24.sp,
|
||||||
|
fontWeight = FontWeight.Bold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SEKTION 1: Erscheinungsbild (Theme & Appearance) ---
|
||||||
|
Text(
|
||||||
|
text = "ERSCHEINUNGSBILD",
|
||||||
|
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
|
||||||
|
// Theme Picker Row (Oklab perception space stops)
|
||||||
|
SettingsOptionRow(
|
||||||
|
title = "Akzentfarbe",
|
||||||
|
description = "Wähle das Farbschema der App (Oklab perzeptiv)",
|
||||||
|
icon = Icons.Rounded.Palette
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
KaizenThemeId.values().forEach { theme ->
|
||||||
|
ThemeOptionPill(
|
||||||
|
themeId = theme,
|
||||||
|
selected = selectedTheme == theme,
|
||||||
|
onClick = { viewModel.selectTheme(theme) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Light / Dark / System Appearance segmented toggle pills
|
||||||
|
SettingsOptionRow(
|
||||||
|
title = "Helligkeitsmodus",
|
||||||
|
description = "Passe das Hell/Dunkel-Erscheinungsbild an",
|
||||||
|
icon = Icons.Rounded.Settings
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppAppearance.values().forEach { mode ->
|
||||||
|
val label = when (mode) {
|
||||||
|
AppAppearance.Light -> "Hell"
|
||||||
|
AppAppearance.Dark -> "Dunkel"
|
||||||
|
AppAppearance.System -> "System"
|
||||||
|
}
|
||||||
|
AppearanceTogglePill(
|
||||||
|
selected = appearanceMode == mode,
|
||||||
|
label = label,
|
||||||
|
onClick = { viewModel.selectAppearance(mode) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SEKTION 2: Profil & Konto ---
|
||||||
|
Text(
|
||||||
|
text = "PROFIL & KONTO",
|
||||||
|
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
|
||||||
|
// Username editor row
|
||||||
|
SettingsOptionRow(
|
||||||
|
title = "Benutzername",
|
||||||
|
description = "Ändere deinen Anzeigenamen",
|
||||||
|
icon = Icons.Rounded.Person
|
||||||
|
) {
|
||||||
|
SettingsInputCapsule(
|
||||||
|
value = userName,
|
||||||
|
onValueChange = { viewModel.updateUserName(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Email display row (read-only in this screen view)
|
||||||
|
SettingsOptionRow(
|
||||||
|
title = "E-Mail-Adresse",
|
||||||
|
description = userEmail,
|
||||||
|
icon = Icons.Rounded.Settings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SEKTION 3: KI-Profil & Kontext ---
|
||||||
|
Text(
|
||||||
|
text = "KI-PROFIL & KONTEXT",
|
||||||
|
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
SettingsCard(modifier = Modifier.padding(bottom = 32.dp)) {
|
||||||
|
SettingsOptionRow(
|
||||||
|
title = "System-Prompt & Fakten",
|
||||||
|
description = "Aktive Regeln: 12, Pinned: 3, Verwendetes Budget: 412 / 1500 Tokens",
|
||||||
|
icon = Icons.Rounded.AutoAwesome
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue