feat: Inter Variable font, theme picker wired end-to-end, settings i18n
Inter Variable (v4.1, 859 KB): full type scale with 9 weight axes (W100–W900), negative tracking at display sizes, positive at labels. Theme picker: SettingsViewModel now uses KaizenThemeId from the design system (AMBER/BLAU/MONO/AURORA). MainActivity wires selectedTheme + appearanceMode to KaizenTheme composable — switching themes in Settings instantly propagates to the entire app. No server sync needed — web stores theme in localStorage, app stores locally. Backend /api/v1/me has no uiTheme field in the DB. Settings strings migrated to string resources (de + en). SettingsViewModelTest updated for new enum values + System default.
This commit is contained in:
parent
f83d4b4e17
commit
515a71efb5
8 changed files with 181 additions and 184 deletions
|
|
@ -71,7 +71,7 @@ Full spec: `DESIGN.md`. Branch: `feature/design-system-v2`.
|
|||
2. **Squircle shapes** — iOS-style superellipse with G2 continuity (`ui/shape/Squircle.kt`). Token system `KaizenShapes.{xs,sm,md,lg,xl,pill,circle}`. Replaces all `RoundedCornerShape`.
|
||||
3. **GlassSurface** — frosted-glass composable (gradient tint + specular border + inner highlight + multi-layer shadow). Android has no backdrop-filter; glass effect works via semi-transparent tint over the blob layer. Opacity tiers per component in `GlassTiers`.
|
||||
4. **Multi-layer shadows** — `ShadowStack` with 2–4 overlapping layers (`KaizenShadows.level0..4`). Dark mode multiplies alphas ×1.8. Replaces all `Modifier.shadow()`.
|
||||
5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Server sync planned via `GET /api/v1/me` → `uiTheme`.
|
||||
5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Theme selection wired end-to-end: `SettingsViewModel.selectedTheme` → `MainActivity` → `KaizenTheme(themeId=...)`. No server sync (web stores in localStorage, app stores locally). Appearance (Light/Dark/System) also wired end-to-end.
|
||||
6. **Sensor-reactive motion** — `rememberTiltState()` provides smoothed tilt `Offset(-1..1)`. Uses `TYPE_GAME_ROTATION_VECTOR` (fused, no drift). Auto-disables on battery saver, reduced motion, background, extreme tilt.
|
||||
|
||||
**Motion tokens** (`ui/motion/Motion.kt`): `EaseSpring` (overshoot), `EaseSmooth` (expo-out), `EaseEmphasized` (M3), `SpringSnappy/Default/Gentle`. All durations centralised in `Durations` object.
|
||||
|
|
@ -80,6 +80,10 @@ Full spec: `DESIGN.md`. Branch: `feature/design-system-v2`.
|
|||
|
||||
**KaizenOrb** (`chat/Background.kt`) uses `rememberTiltState()` from `ui/sensor/` instead of inline sensor code. Breath animation uses `Durations.ORB_BREATH` / `ORB_BREATH_STREAMING` from `ui/motion/`.
|
||||
|
||||
**Typography** — Inter Variable (`res/font/inter_variable.ttf`, 859 KB, SIL OFL). Full type scale from DESIGN.md §8: `displayLarge` (32sp/W600) through `labelSmall` (11sp/W500). All 9 weight axes registered (W100–W900). Negative tracking at display sizes, positive at label sizes. `@OptIn(ExperimentalTextApi::class)` for `FontVariation.Settings`.
|
||||
|
||||
**Oklab** (`ui/theme/Oklab.kt`) — Double-precision matrix math for accurate sRGB↔Oklab round-trips. Used by theme picker's Oklab gradient swatches and `generateGradientStops()`. Forward LMS matrix corrected to Björn Ottosson reference.
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
- **de** (default), **en** — same as web (`messages/de.json`, `messages/en.json`).
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import android.os.Bundle
|
|||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dev.kaizen.app.auth.LoginScreen
|
||||
import dev.kaizen.app.chat.ChatScreen
|
||||
|
|
@ -14,6 +15,7 @@ import dev.kaizen.app.chat.ChatViewModel
|
|||
import dev.kaizen.app.net.SecureStore
|
||||
import dev.kaizen.app.net.SessionViewModel
|
||||
import dev.kaizen.app.settings.SettingsViewModel
|
||||
import dev.kaizen.app.settings.AppAppearance
|
||||
import dev.kaizen.app.ui.theme.KaizenTheme
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
|
@ -39,7 +41,14 @@ class MainActivity : ComponentActivity() {
|
|||
navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
|
||||
)
|
||||
setContent {
|
||||
KaizenTheme {
|
||||
val themeId = settingsViewModel.selectedTheme.collectAsState()
|
||||
val appearance = settingsViewModel.appearanceMode.collectAsState()
|
||||
val darkOverride = when (appearance.value) {
|
||||
AppAppearance.Light -> false
|
||||
AppAppearance.Dark -> true
|
||||
AppAppearance.System -> androidx.compose.foundation.isSystemInDarkTheme()
|
||||
}
|
||||
KaizenTheme(themeId = themeId.value, darkTheme = darkOverride) {
|
||||
// Snapshot-state routing: logging in/out flips session.config and
|
||||
// recomposes this branch — no NavHost needed for two top-level states.
|
||||
if (sessionViewModel.isLoggedIn) {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ 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
|
||||
import dev.kaizen.app.ui.theme.themes.KaizenThemeId
|
||||
|
||||
/** 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 {
|
||||
|
|
@ -201,17 +202,17 @@ fun ThemeOptionPill(
|
|||
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)
|
||||
KaizenThemeId.AMBER -> oklch(0.76f, 0.14f, 83f) to oklch(0.55f, 0.16f, 83f)
|
||||
KaizenThemeId.BLAU -> 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)
|
||||
KaizenThemeId.AMBER -> oklch(0.72f, 0.14f, 83f) to oklch(0.85f, 0.15f, 83f)
|
||||
KaizenThemeId.BLAU -> 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -251,7 +252,7 @@ fun ThemeOptionPill(
|
|||
Icon(
|
||||
imageVector = Icons.Rounded.Check,
|
||||
contentDescription = null,
|
||||
tint = if (themeId == KaizenThemeId.Mono && isDark.not()) Color.White else Color.Black,
|
||||
tint = if (themeId == KaizenThemeId.MONO && isDark.not()) Color.White else Color.Black,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ 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.res.stringResource
|
||||
import dev.kaizen.app.R
|
||||
import dev.kaizen.app.ui.theme.themes.KaizenThemeId
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
|
@ -94,119 +97,48 @@ fun SettingsScreen(
|
|||
|
||||
Spacer(Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Einstellungen",
|
||||
color = cs.onBackground,
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(stringResource(R.string.settings_title), 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)
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.settings_section_appearance), 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) }
|
||||
)
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_accent_title), description = stringResource(R.string.settings_accent_desc), icon = Icons.Rounded.Palette) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
KaizenThemeId.entries.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 ->
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_appearance_title), description = stringResource(R.string.settings_appearance_desc), icon = Icons.Rounded.Settings) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
AppAppearance.entries.forEach { mode ->
|
||||
val label = when (mode) {
|
||||
AppAppearance.Light -> "Hell"
|
||||
AppAppearance.Dark -> "Dunkel"
|
||||
AppAppearance.System -> "System"
|
||||
AppAppearance.Light -> stringResource(R.string.settings_appearance_light)
|
||||
AppAppearance.Dark -> stringResource(R.string.settings_appearance_dark)
|
||||
AppAppearance.System -> stringResource(R.string.settings_appearance_system)
|
||||
}
|
||||
AppearanceTogglePill(
|
||||
selected = appearanceMode == mode,
|
||||
label = label,
|
||||
onClick = { viewModel.selectAppearance(mode) }
|
||||
)
|
||||
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)
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.settings_section_profile), 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) }
|
||||
)
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_name_title), description = stringResource(R.string.settings_name_desc), 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
|
||||
)
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_email_title), 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)
|
||||
)
|
||||
|
||||
Text(stringResource(R.string.settings_section_ai), 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
|
||||
)
|
||||
SettingsOptionRow(title = stringResource(R.string.settings_ai_title), description = "", icon = Icons.Rounded.AutoAwesome)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,67 +1,40 @@
|
|||
package dev.kaizen.app.settings
|
||||
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dev.kaizen.app.ui.theme.themes.KaizenThemeId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
||||
// Available Kaizen UI themes
|
||||
enum class KaizenThemeId { Amber, Blue, Aurora, Mono }
|
||||
|
||||
// Available Light/Dark/System appearance modes
|
||||
enum class AppAppearance { Light, Dark, System }
|
||||
|
||||
/**
|
||||
* Clean MVVM ViewModel for the Settings module.
|
||||
*
|
||||
* Completely decoupled from Android UI classes, making it 100% unit-testable
|
||||
* on the JVM. Manages the state of the Oklab Theme Picker, selected model,
|
||||
* and user profile options.
|
||||
*/
|
||||
class SettingsViewModel : ViewModel() {
|
||||
|
||||
// 1. Reactive State Flow for Theme
|
||||
private val _selectedTheme = MutableStateFlow(KaizenThemeId.Amber)
|
||||
private val _selectedTheme = MutableStateFlow(KaizenThemeId.AMBER)
|
||||
val selectedTheme: StateFlow<KaizenThemeId> = _selectedTheme.asStateFlow()
|
||||
|
||||
// 2. Reactive State Flow for Appearance
|
||||
private val _appearanceMode = MutableStateFlow(AppAppearance.Dark)
|
||||
private val _appearanceMode = MutableStateFlow(AppAppearance.System)
|
||||
val appearanceMode: StateFlow<AppAppearance> = _appearanceMode.asStateFlow()
|
||||
|
||||
// 3. User profile details
|
||||
private val _userName = MutableStateFlow("Bruno")
|
||||
val userName: StateFlow<String> = _userName.asStateFlow()
|
||||
|
||||
private val _userEmail = MutableStateFlow("admin@kryptomrx.de")
|
||||
private val _userEmail = MutableStateFlow("")
|
||||
val userEmail: StateFlow<String> = _userEmail.asStateFlow()
|
||||
|
||||
// 4. Default Chat Model
|
||||
private val _defaultModel = MutableStateFlow("gemini-2.5-flash")
|
||||
val defaultModel: StateFlow<String> = _defaultModel.asStateFlow()
|
||||
|
||||
/** Selects a new Kaizen UI theme and triggers an Oklab transition stops recalculation */
|
||||
fun selectTheme(themeId: KaizenThemeId) {
|
||||
_selectedTheme.value = themeId
|
||||
}
|
||||
|
||||
/** Changes the appearance mode (Light, Dark, or System Sync) */
|
||||
fun selectAppearance(mode: AppAppearance) {
|
||||
_appearanceMode.value = mode
|
||||
}
|
||||
|
||||
/** Updates the user's name in profile settings */
|
||||
fun updateUserName(name: String) {
|
||||
if (name.isNotBlank()) {
|
||||
_userName.value = name.trim()
|
||||
}
|
||||
if (name.isNotBlank()) _userName.value = name.trim()
|
||||
}
|
||||
|
||||
/** Updates the standard chat model */
|
||||
fun updateDefaultModel(modelId: String) {
|
||||
if (modelId.isNotBlank()) {
|
||||
_defaultModel.value = modelId.trim()
|
||||
}
|
||||
fun updateUserEmail(email: String) {
|
||||
_userEmail.value = email
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,123 @@ package dev.kaizen.app.ui.theme
|
|||
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontVariation
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
import dev.kaizen.app.R
|
||||
|
||||
@OptIn(androidx.compose.ui.text.ExperimentalTextApi::class)
|
||||
val InterVariable = FontFamily(
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W100,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(100)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W200,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(200)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W300,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(300)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W400,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(400)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W500,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(500)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W600,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(600)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W700,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(700)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W800,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(800)),
|
||||
),
|
||||
Font(
|
||||
R.font.inter_variable,
|
||||
weight = FontWeight.W900,
|
||||
variationSettings = FontVariation.Settings(FontVariation.weight(900)),
|
||||
),
|
||||
)
|
||||
|
||||
// Set of Material typography styles to start with
|
||||
val Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 38.sp,
|
||||
letterSpacing = (-0.32).sp,
|
||||
),
|
||||
displayMedium = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 24.sp,
|
||||
lineHeight = 30.sp,
|
||||
letterSpacing = (-0.12).sp,
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W600,
|
||||
fontSize = 20.sp,
|
||||
lineHeight = 26.sp,
|
||||
),
|
||||
titleMedium = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W500,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 22.sp,
|
||||
letterSpacing = 0.05.sp,
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 16.sp,
|
||||
lineHeight = 24.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
/* Other default text styles to override
|
||||
titleLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 22.sp,
|
||||
lineHeight = 28.sp,
|
||||
letterSpacing = 0.sp
|
||||
letterSpacing = 0.08.sp,
|
||||
),
|
||||
bodyMedium = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W400,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.11.sp,
|
||||
),
|
||||
labelLarge = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W500,
|
||||
fontSize = 14.sp,
|
||||
lineHeight = 20.sp,
|
||||
letterSpacing = 0.14.sp,
|
||||
),
|
||||
labelMedium = TextStyle(
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W500,
|
||||
fontSize = 12.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.24.sp,
|
||||
),
|
||||
labelSmall = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontFamily = InterVariable,
|
||||
fontWeight = FontWeight.W500,
|
||||
fontSize = 11.sp,
|
||||
lineHeight = 16.sp,
|
||||
letterSpacing = 0.5.sp
|
||||
)
|
||||
*/
|
||||
)
|
||||
letterSpacing = 0.33.sp,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
BIN
app/src/main/res/font/inter_variable.ttf
Normal file
BIN
app/src/main/res/font/inter_variable.ttf
Normal file
Binary file not shown.
|
|
@ -1,67 +1,55 @@
|
|||
package dev.kaizen.app
|
||||
|
||||
import dev.kaizen.app.settings.AppAppearance
|
||||
import dev.kaizen.app.settings.KaizenThemeId
|
||||
import dev.kaizen.app.settings.SettingsViewModel
|
||||
import dev.kaizen.app.ui.theme.themes.KaizenThemeId
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* High-fidelity JVM Unit Test for the SettingsViewModel.
|
||||
*
|
||||
* Verifies that state transitions, user inputs, and theme selections are
|
||||
* correctly and reactively managed inside the view model under clean MVVM.
|
||||
*/
|
||||
class SettingsViewModelTest {
|
||||
|
||||
@Test
|
||||
fun testInitialState() {
|
||||
val viewModel = SettingsViewModel()
|
||||
|
||||
// Assert correct default states
|
||||
assertEquals(KaizenThemeId.Amber, viewModel.selectedTheme.value)
|
||||
assertEquals(AppAppearance.Dark, viewModel.appearanceMode.value)
|
||||
assertEquals(KaizenThemeId.AMBER, viewModel.selectedTheme.value)
|
||||
assertEquals(AppAppearance.System, viewModel.appearanceMode.value)
|
||||
assertEquals("Bruno", viewModel.userName.value)
|
||||
assertEquals("gemini-2.5-flash", viewModel.defaultModel.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSelectTheme() {
|
||||
val viewModel = SettingsViewModel()
|
||||
|
||||
// Transition theme to Aurora
|
||||
viewModel.selectTheme(KaizenThemeId.Aurora)
|
||||
|
||||
assertEquals(KaizenThemeId.Aurora, viewModel.selectedTheme.value)
|
||||
viewModel.selectTheme(KaizenThemeId.AURORA)
|
||||
assertEquals(KaizenThemeId.AURORA, viewModel.selectedTheme.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSelectAllThemes() {
|
||||
val viewModel = SettingsViewModel()
|
||||
KaizenThemeId.entries.forEach { id ->
|
||||
viewModel.selectTheme(id)
|
||||
assertEquals(id, viewModel.selectedTheme.value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSelectAppearance() {
|
||||
val viewModel = SettingsViewModel()
|
||||
|
||||
// Switch appearance to Light Mode
|
||||
viewModel.selectAppearance(AppAppearance.Light)
|
||||
|
||||
assertEquals(AppAppearance.Light, viewModel.appearanceMode.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateUserName() {
|
||||
val viewModel = SettingsViewModel()
|
||||
|
||||
// Update user's name
|
||||
viewModel.updateUserName(" Giuseppe ") // Should be trimmed
|
||||
|
||||
viewModel.updateUserName(" Giuseppe ")
|
||||
assertEquals("Giuseppe", viewModel.userName.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUpdateUserNameBlankIgnored() {
|
||||
val viewModel = SettingsViewModel()
|
||||
|
||||
// Attempting to set an empty/blank name should be ignored
|
||||
viewModel.updateUserName(" ")
|
||||
|
||||
assertEquals("Bruno", viewModel.userName.value) // Remains unchanged
|
||||
assertEquals("Bruno", viewModel.userName.value)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue