feat: dark mode support — ThemeMode setting, dynamic colors, status bar sync (batch v0.2.4)

- Add ThemeMode enum (DEVICE/LIGHT/DARK) with DataStore persistence
- SettingsScreen: appearance section with radio buttons for Device/Light/Dark
- SettingsViewModel: observe and persist theme mode
- MainActivity: read theme setting, apply dark theme, sync status bar icons
- Replace hardcoded Color.White references with MaterialTheme.colorScheme.surface across auth, onboarding, settings, and theme
- Convert AuthVisuals, SettingsVisuals, CloserPalette brushes to @Composable getters using dynamic scheme colors
- Add values-night/themes.xml for dark theme manifest entry
- Add ThemeModeTest unit test for fromStorageValue parsing
This commit is contained in:
null 2026-06-19 19:00:37 -05:00
parent c878a9be1f
commit 7256a71bdf
15 changed files with 217 additions and 48 deletions

View File

@ -4,19 +4,44 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import app.closer.core.navigation.AppNavigation import app.closer.core.navigation.AppNavigation
import app.closer.domain.repository.AppSettings
import app.closer.domain.repository.SettingsRepository
import app.closer.domain.repository.ThemeMode
import app.closer.ui.theme.CloserTheme import app.closer.ui.theme.CloserTheme
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 settingsRepository: SettingsRepository
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { setContent {
CloserTheme { val settings by settingsRepository.settings.collectAsState(initial = AppSettings())
val systemInDarkTheme = isSystemInDarkTheme()
val useDarkTheme = when (settings.themeMode) {
ThemeMode.DEVICE -> systemInDarkTheme
ThemeMode.LIGHT -> false
ThemeMode.DARK -> true
}
CloserTheme(darkTheme = useDarkTheme) {
SideEffect {
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = !useDarkTheme
isAppearanceLightNavigationBars = !useDarkTheme
}
}
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background color = MaterialTheme.colorScheme.background

View File

@ -5,9 +5,11 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import app.closer.core.notifications.QuietHours import app.closer.core.notifications.QuietHours
import app.closer.domain.repository.AppSettings import app.closer.domain.repository.AppSettings
import app.closer.domain.repository.SettingsRepository import app.closer.domain.repository.SettingsRepository
import app.closer.domain.repository.ThemeMode
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import javax.inject.Inject import javax.inject.Inject
@ -27,6 +29,7 @@ class SettingsDataStore @Inject constructor(
private val QUIET_HOURS_END_HOUR = intPreferencesKey("quiet_hours_end_hour") private val QUIET_HOURS_END_HOUR = intPreferencesKey("quiet_hours_end_hour")
private val QUIET_HOURS_END_MINUTE = intPreferencesKey("quiet_hours_end_minute") private val QUIET_HOURS_END_MINUTE = intPreferencesKey("quiet_hours_end_minute")
private val ONBOARDING_COMPLETE = booleanPreferencesKey("onboarding_complete") private val ONBOARDING_COMPLETE = booleanPreferencesKey("onboarding_complete")
private val THEME_MODE = stringPreferencesKey("theme_mode")
override val settings: Flow<AppSettings> = dataStore.data.map { prefs -> override val settings: Flow<AppSettings> = dataStore.data.map { prefs ->
AppSettings( AppSettings(
@ -41,7 +44,8 @@ class SettingsDataStore @Inject constructor(
endHour = prefs[QUIET_HOURS_END_HOUR] ?: 8, endHour = prefs[QUIET_HOURS_END_HOUR] ?: 8,
endMinute = prefs[QUIET_HOURS_END_MINUTE] ?: 0 endMinute = prefs[QUIET_HOURS_END_MINUTE] ?: 0
), ),
onboardingComplete = prefs[ONBOARDING_COMPLETE] ?: false onboardingComplete = prefs[ONBOARDING_COMPLETE] ?: false,
themeMode = ThemeMode.fromStorageValue(prefs[THEME_MODE])
) )
} }
@ -68,4 +72,7 @@ class SettingsDataStore @Inject constructor(
override suspend fun setOnboardingComplete(complete: Boolean) = override suspend fun setOnboardingComplete(complete: Boolean) =
dataStore.edit { it[ONBOARDING_COMPLETE] = complete }.let {} dataStore.edit { it[ONBOARDING_COMPLETE] = complete }.let {}
override suspend fun setThemeMode(mode: ThemeMode) =
dataStore.edit { it[THEME_MODE] = mode.name }.let {}
} }

View File

@ -3,13 +3,25 @@ package app.closer.domain.repository
import app.closer.core.notifications.QuietHours import app.closer.core.notifications.QuietHours
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
enum class ThemeMode {
DEVICE,
LIGHT,
DARK;
companion object {
fun fromStorageValue(value: String?): ThemeMode =
entries.firstOrNull { it.name == value } ?: DEVICE
}
}
data class AppSettings( data class AppSettings(
val dailyReminderEnabled: Boolean = true, val dailyReminderEnabled: Boolean = true,
val partnerAnsweredEnabled: Boolean = true, val partnerAnsweredEnabled: Boolean = true,
val streakReminderEnabled: Boolean = false, val streakReminderEnabled: Boolean = false,
val quietHoursEnabled: Boolean = false, val quietHoursEnabled: Boolean = false,
val quietHours: QuietHours = QuietHours(), val quietHours: QuietHours = QuietHours(),
val onboardingComplete: Boolean = false val onboardingComplete: Boolean = false,
val themeMode: ThemeMode = ThemeMode.DEVICE
) )
interface SettingsRepository { interface SettingsRepository {
@ -20,4 +32,5 @@ interface SettingsRepository {
suspend fun setQuietHours(enabled: Boolean) suspend fun setQuietHours(enabled: Boolean)
suspend fun setQuietHours(quietHours: QuietHours) suspend fun setQuietHours(quietHours: QuietHours)
suspend fun setOnboardingComplete(complete: Boolean) suspend fun setOnboardingComplete(complete: Boolean)
suspend fun setThemeMode(mode: ThemeMode)
} }

View File

@ -25,25 +25,29 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.BackgroundColor
import app.closer.ui.theme.OnBackgroundColor
import app.closer.ui.theme.OnPrimaryColor
import app.closer.ui.theme.OnSurfaceVariantColor
import app.closer.ui.theme.PrimaryColor
internal val AuthBackgroundBrush: Brush internal val AuthBackgroundBrush: Brush
@Composable
get() = Brush.linearGradient( get() = Brush.linearGradient(
colors = listOf(BackgroundColor, CloserPalette.BackgroundWash, CloserPalette.PinkMist), colors = listOf(
MaterialTheme.colorScheme.background,
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.colorScheme.secondaryContainer
),
start = Offset.Zero, start = Offset.Zero,
end = Offset.Infinite end = Offset.Infinite
) )
internal val AuthInk = OnBackgroundColor internal val AuthInk: Color
internal val AuthMuted = OnSurfaceVariantColor @Composable get() = MaterialTheme.colorScheme.onBackground
internal val AuthPrimary = PrimaryColor internal val AuthMuted: Color
internal val AuthPrimaryDeep = CloserPalette.PurpleDeep @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant
internal val AuthOnPrimary = OnPrimaryColor internal val AuthPrimary: Color
@Composable get() = MaterialTheme.colorScheme.primary
internal val AuthPrimaryDeep: Color
@Composable get() = MaterialTheme.colorScheme.onPrimaryContainer
internal val AuthOnPrimary: Color
@Composable get() = MaterialTheme.colorScheme.onPrimary
@Composable @Composable
internal fun GoogleSignInButton( internal fun GoogleSignInButton(
@ -91,7 +95,7 @@ internal fun authTextFieldColors() = OutlinedTextFieldDefaults.colors(
unfocusedLabelColor = AuthMuted, unfocusedLabelColor = AuthMuted,
cursorColor = AuthPrimaryDeep, cursorColor = AuthPrimaryDeep,
focusedContainerColor = closerCardColor(alpha = 0.92f), focusedContainerColor = closerCardColor(alpha = 0.92f),
unfocusedContainerColor = Color.White.copy(alpha = 0.78f), unfocusedContainerColor = closerCardColor(alpha = 0.78f),
focusedTextColor = AuthInk, focusedTextColor = AuthInk,
unfocusedTextColor = AuthInk, unfocusedTextColor = AuthInk,
focusedTrailingIconColor = AuthPrimaryDeep, focusedTrailingIconColor = AuthPrimaryDeep,

View File

@ -216,7 +216,7 @@ fun LoginScreen(
enabled = !state.isLoading, enabled = !state.isLoading,
modifier = Modifier.fillMaxWidth().height(52.dp), modifier = Modifier.fillMaxWidth().height(52.dp),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.White.copy(alpha = 0.62f), containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.62f),
contentColor = AuthPrimaryDeep contentColor = AuthPrimaryDeep
), ),
border = BorderStroke(1.dp, AuthPrimaryDeep.copy(alpha = 0.28f)) border = BorderStroke(1.dp, AuthPrimaryDeep.copy(alpha = 0.28f))

View File

@ -318,7 +318,7 @@ private fun SexStep(
@Composable @Composable
private fun SexOption(label: String, selected: Boolean, onClick: () -> Unit) { private fun SexOption(label: String, selected: Boolean, onClick: () -> Unit) {
val background = if (selected) AuthPrimary else Color.White.copy(alpha = 0.78f) val background = if (selected) AuthPrimary else MaterialTheme.colorScheme.surface.copy(alpha = 0.78f)
val contentColor = if (selected) AuthOnPrimary else AuthInk val contentColor = if (selected) AuthOnPrimary else AuthInk
val borderColor = if (selected) AuthPrimary else AuthMuted.copy(alpha = 0.24f) val borderColor = if (selected) AuthPrimary else AuthMuted.copy(alpha = 0.24f)
@ -370,7 +370,7 @@ private fun PhotoStep(
modifier = Modifier modifier = Modifier
.size(140.dp) .size(140.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color.White.copy(alpha = 0.78f)) .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.78f))
.border(2.dp, AuthPrimary.copy(alpha = 0.3f), CircleShape), .border(2.dp, AuthPrimary.copy(alpha = 0.3f), CircleShape),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
@ -399,7 +399,7 @@ private fun PhotoStep(
onClick = onGallery, onClick = onGallery,
modifier = Modifier.fillMaxWidth().height(52.dp), modifier = Modifier.fillMaxWidth().height(52.dp),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.White.copy(alpha = 0.78f), containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.78f),
contentColor = AuthInk contentColor = AuthInk
) )
) { ) {
@ -413,7 +413,7 @@ private fun PhotoStep(
onClick = onCamera, onClick = onCamera,
modifier = Modifier.fillMaxWidth().height(52.dp), modifier = Modifier.fillMaxWidth().height(52.dp),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.White.copy(alpha = 0.78f), containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.78f),
contentColor = AuthInk contentColor = AuthInk
) )
) { ) {

View File

@ -300,7 +300,7 @@ private fun AnswerPreviewVisual() {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp), shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = Color.White), colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) { ) {
Column( Column(

View File

@ -180,7 +180,7 @@ fun EditProfileContent(
modifier = Modifier modifier = Modifier
.size(120.dp) .size(120.dp)
.clip(CircleShape) .clip(CircleShape)
.background(Color.White) .background(SettingsCard)
.border(2.dp, SettingsPrimary.copy(alpha = 0.3f), CircleShape) .border(2.dp, SettingsPrimary.copy(alpha = 0.3f), CircleShape)
.clickable { .clickable {
galleryLauncher.launch( galleryLauncher.launch(
@ -233,7 +233,7 @@ fun EditProfileContent(
}, },
modifier = Modifier.weight(1f).height(48.dp), modifier = Modifier.weight(1f).height(48.dp),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.White, containerColor = SettingsCard,
contentColor = SettingsInk contentColor = SettingsInk
) )
) { ) {
@ -249,7 +249,7 @@ fun EditProfileContent(
}, },
modifier = Modifier.weight(1f).height(48.dp), modifier = Modifier.weight(1f).height(48.dp),
colors = ButtonDefaults.outlinedButtonColors( colors = ButtonDefaults.outlinedButtonColors(
containerColor = Color.White, containerColor = SettingsCard,
contentColor = SettingsInk contentColor = SettingsInk
) )
) { ) {
@ -265,7 +265,7 @@ fun EditProfileContent(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.background(Color.White) .background(SettingsCard)
.padding(16.dp) .padding(16.dp)
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@ -370,7 +370,7 @@ private fun SexEditOption(
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val background = if (selected) SettingsPrimary else Color.White val background = if (selected) SettingsPrimary else SettingsCard
val contentColor = if (selected) SettingsOnPrimary else SettingsInk val contentColor = if (selected) SettingsOnPrimary else SettingsInk
val borderColor = if (selected) SettingsPrimary else SettingsMuted.copy(alpha = 0.24f) val borderColor = if (selected) SettingsPrimary else SettingsMuted.copy(alpha = 0.24f)

View File

@ -39,6 +39,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -59,6 +60,7 @@ import android.content.Context
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.core.navigation.ExternalLinks import app.closer.core.navigation.ExternalLinks
import app.closer.domain.repository.ThemeMode
import app.closer.ui.settings.SettingsDanger import app.closer.ui.settings.SettingsDanger
import app.closer.ui.settings.SettingsInk import app.closer.ui.settings.SettingsInk
import app.closer.ui.settings.SettingsMuted import app.closer.ui.settings.SettingsMuted
@ -324,6 +326,44 @@ fun SettingsScreen(
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
Text(
text = "Appearance",
style = MaterialTheme.typography.labelLarge,
color = SettingsMuted,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 4.dp)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = SettingsCard)
) {
Column {
ThemeModeRow(
label = "Device",
description = "Match your device appearance",
selected = state.themeMode == ThemeMode.DEVICE,
onClick = { viewModel.setThemeMode(ThemeMode.DEVICE) }
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
ThemeModeRow(
label = "Light",
description = "Always use light mode",
selected = state.themeMode == ThemeMode.LIGHT,
onClick = { viewModel.setThemeMode(ThemeMode.LIGHT) }
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
ThemeModeRow(
label = "Dark",
description = "Always use dark mode",
selected = state.themeMode == ThemeMode.DARK,
onClick = { viewModel.setThemeMode(ThemeMode.DARK) }
)
}
}
Spacer(Modifier.height(4.dp))
// General section // General section
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@ -429,6 +469,37 @@ fun SettingsScreen(
} }
} }
@Composable
private fun ThemeModeRow(
label: String,
description: String,
selected: Boolean,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
RadioButton(selected = selected, onClick = onClick)
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = label,
style = MaterialTheme.typography.bodyLarge,
color = SettingsInk
)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = SettingsMuted
)
}
}
}
@Composable @Composable
private fun SettingsRow( private fun SettingsRow(
icon: ImageVector, icon: ImageVector,

View File

@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import app.closer.core.navigation.AppRoute import app.closer.core.navigation.AppRoute
import app.closer.domain.repository.AuthRepository import app.closer.domain.repository.AuthRepository
import app.closer.domain.repository.CoupleRepository import app.closer.domain.repository.CoupleRepository
import app.closer.domain.repository.SettingsRepository
import app.closer.domain.repository.ThemeMode
import app.closer.domain.repository.UserRepository import app.closer.domain.repository.UserRepository
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -22,6 +24,7 @@ data class SettingsUiState(
val partnerName: String? = null, val partnerName: String? = null,
val isPaired: Boolean = false, val isPaired: Boolean = false,
val isSigningOut: Boolean = false, val isSigningOut: Boolean = false,
val themeMode: ThemeMode = ThemeMode.DEVICE,
val navigateTo: String? = null val navigateTo: String? = null
) )
@ -29,7 +32,8 @@ data class SettingsUiState(
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val authRepository: AuthRepository, private val authRepository: AuthRepository,
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val coupleRepository: CoupleRepository private val coupleRepository: CoupleRepository,
private val settingsRepository: SettingsRepository
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState()) private val _uiState = MutableStateFlow(SettingsUiState())
@ -37,6 +41,20 @@ class SettingsViewModel @Inject constructor(
init { init {
loadSettings() loadSettings()
observeThemeMode()
}
private fun observeThemeMode() {
viewModelScope.launch {
settingsRepository.settings.collect { settings ->
_uiState.update { it.copy(themeMode = settings.themeMode) }
}
}
}
fun setThemeMode(mode: ThemeMode) {
_uiState.update { it.copy(themeMode = mode) }
viewModelScope.launch { settingsRepository.setThemeMode(mode) }
} }
private fun loadSettings() { private fun loadSettings() {

View File

@ -1,32 +1,39 @@
package app.closer.ui.settings package app.closer.ui.settings
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import app.closer.ui.theme.BackgroundColor
import app.closer.ui.theme.CloserPalette
import app.closer.ui.theme.OnBackgroundColor
import app.closer.ui.theme.OnPrimaryColor
import app.closer.ui.theme.OnSurfaceVariantColor
import app.closer.ui.theme.PrimaryColor
internal val SettingsBackgroundBrush: Brush internal val SettingsBackgroundBrush: Brush
@Composable
get() = Brush.linearGradient( get() = Brush.linearGradient(
colors = listOf(BackgroundColor, CloserPalette.BackgroundWash, CloserPalette.PinkMist), colors = listOf(
MaterialTheme.colorScheme.background,
MaterialTheme.colorScheme.surfaceVariant,
MaterialTheme.colorScheme.secondaryContainer
),
start = Offset.Zero, start = Offset.Zero,
end = Offset.Infinite end = Offset.Infinite
) )
internal val SettingsInk = OnBackgroundColor internal val SettingsInk: Color
internal val SettingsMuted = OnSurfaceVariantColor @Composable get() = MaterialTheme.colorScheme.onBackground
internal val SettingsPrimary = PrimaryColor internal val SettingsMuted: Color
internal val SettingsPrimaryDeep = CloserPalette.PurpleDeep @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant
internal val SettingsOnPrimary = OnPrimaryColor internal val SettingsPrimary: Color
internal val SettingsCard = Color.White @Composable get() = MaterialTheme.colorScheme.primary
internal val SettingsSoft = CloserPalette.PurpleSoft internal val SettingsPrimaryDeep: Color
internal val SettingsDanger = CloserPalette.Danger @Composable get() = MaterialTheme.colorScheme.onPrimaryContainer
internal val SettingsOnPrimary: Color
@Composable get() = MaterialTheme.colorScheme.onPrimary
internal val SettingsCard: Color
@Composable get() = MaterialTheme.colorScheme.surface
internal val SettingsSoft: Color
@Composable get() = MaterialTheme.colorScheme.primaryContainer
internal val SettingsDanger: Color
@Composable get() = MaterialTheme.colorScheme.error
@Composable @Composable
internal fun settingsSwitchColors() = SwitchDefaults.colors( internal fun settingsSwitchColors() = SwitchDefaults.colors(

View File

@ -30,8 +30,8 @@ fun closerBackgroundBrush(): Brush =
Brush.linearGradient( Brush.linearGradient(
colors = listOf( colors = listOf(
MaterialTheme.colorScheme.background, MaterialTheme.colorScheme.background,
CloserPalette.BackgroundWash, MaterialTheme.colorScheme.surfaceVariant,
CloserPalette.PinkMist MaterialTheme.colorScheme.secondaryContainer
), ),
start = Offset.Zero, start = Offset.Zero,
end = Offset.Infinite end = Offset.Infinite
@ -69,7 +69,7 @@ fun closerPlayCardBrush(): Brush =
colors = listOf( colors = listOf(
MaterialTheme.colorScheme.surface, MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.primaryContainer, MaterialTheme.colorScheme.primaryContainer,
CloserPalette.PinkSoft MaterialTheme.colorScheme.secondaryContainer
), ),
start = Offset.Zero, start = Offset.Zero,
end = Offset.Infinite end = Offset.Infinite

View File

@ -4,12 +4,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
import androidx.compose.material3.lightColorScheme import androidx.compose.material3.lightColorScheme
import androidx.compose.material3.darkColorScheme import androidx.compose.material3.darkColorScheme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@Composable @Composable
fun CloserTheme( fun CloserTheme(
darkTheme: Boolean = false, darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val colors = if (darkTheme) darkColors else lightColors val colors = if (darkTheme) darkColors else lightColors

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.Closer" parent="android:Theme.Material.NoActionBar" />
</resources>

View File

@ -0,0 +1,19 @@
package app.closer.domain.repository
import org.junit.Assert.assertEquals
import org.junit.Test
class ThemeModeTest {
@Test
fun `missing or invalid stored values default to device`() {
assertEquals(ThemeMode.DEVICE, ThemeMode.fromStorageValue(null))
assertEquals(ThemeMode.DEVICE, ThemeMode.fromStorageValue("UNKNOWN"))
}
@Test
fun `stored values restore each explicit theme mode`() {
ThemeMode.entries.forEach { mode ->
assertEquals(mode, ThemeMode.fromStorageValue(mode.name))
}
}
}