feat(settings): add appearance screen with theme picker, refactor settings nav

This commit is contained in:
null 2026-06-20 18:45:44 -05:00
parent 1f777e827d
commit bdd2bf27c0
4 changed files with 168 additions and 71 deletions

View File

@ -66,6 +66,7 @@ import app.closer.ui.questions.QuestionComposerScreen
import app.closer.ui.questions.QuestionPackLibraryScreen
import app.closer.ui.questions.QuestionThreadScreen
import app.closer.ui.settings.AccountScreen
import app.closer.ui.settings.AppearanceScreen
import app.closer.ui.settings.DeleteAccountScreen
import app.closer.ui.settings.NotificationSettingsScreen
import app.closer.ui.settings.PrivacyScreen
@ -424,6 +425,9 @@ fun AppNavigation(
composable(route = AppRoute.ACCOUNT) {
AccountScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.APPEARANCE) {
AppearanceScreen(onNavigate = navigateRoute)
}
composable(route = AppRoute.NOTIFICATIONS) {
NotificationSettingsScreen(onNavigate = navigateRoute)
}

View File

@ -30,6 +30,7 @@ object AppRoute {
const val SETTINGS = "settings"
const val ACCOUNT = "account"
const val NOTIFICATIONS = "notifications"
const val APPEARANCE = "appearance"
const val PRIVACY = "privacy"
const val SUBSCRIPTION = "subscription"
const val RELATIONSHIP_SETTINGS = "relationship_settings"

View File

@ -0,0 +1,156 @@
package app.closer.ui.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
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.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import app.closer.domain.repository.ThemeMode
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppearanceScreen(
onNavigate: (String) -> Unit = {},
viewModel: SettingsViewModel = hiltViewModel()
) {
val state by viewModel.uiState.collectAsState()
Scaffold(
containerColor = Color.Transparent,
modifier = Modifier.background(SettingsBackgroundBrush),
topBar = {
TopAppBar(
title = { Text("Appearance", color = SettingsInk) },
navigationIcon = {
IconButton(onClick = { onNavigate("back") }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
tint = SettingsInk
)
}
},
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent)
)
}
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.safeDrawingPadding()
.navigationBarsPadding()
.verticalScroll(rememberScrollState())
.padding(padding)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Theme",
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 {
ThemeOptionRow(
label = "Device default",
description = "Match your device's light or dark setting",
selected = state.themeMode == ThemeMode.DEVICE,
onClick = { viewModel.setThemeMode(ThemeMode.DEVICE) }
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
ThemeOptionRow(
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)
ThemeOptionRow(
label = "Dark",
description = "Always use dark mode",
selected = state.themeMode == ThemeMode.DARK,
onClick = { viewModel.setThemeMode(ThemeMode.DARK) }
)
}
}
Spacer(Modifier.height(8.dp))
Text(
text = "Changes apply instantly across the whole app.",
style = MaterialTheme.typography.bodySmall,
color = SettingsMuted,
modifier = Modifier.padding(horizontal = 4.dp)
)
}
}
}
@Composable
private fun ThemeOptionRow(
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
)
}
}
}

View File

@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.FavoriteBorder
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.Palette
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Warning
@ -39,7 +40,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@ -60,7 +60,6 @@ import android.content.Context
import androidx.compose.ui.platform.LocalContext
import app.closer.core.navigation.AppRoute
import app.closer.core.navigation.ExternalLinks
import app.closer.domain.repository.ThemeMode
import app.closer.ui.settings.SettingsDanger
import app.closer.ui.settings.SettingsInk
import app.closer.ui.settings.SettingsMuted
@ -326,44 +325,6 @@ fun SettingsScreen(
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
Card(
modifier = Modifier.fillMaxWidth(),
@ -377,6 +338,12 @@ fun SettingsScreen(
onClick = { onNavigate(AppRoute.ANSWER_HISTORY) }
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
SettingsRow(
icon = Icons.Filled.Palette,
label = "Appearance",
onClick = { onNavigate(AppRoute.APPEARANCE) }
)
Divider(modifier = Modifier.padding(horizontal = 16.dp), thickness = 0.5.dp)
SettingsRow(
icon = Icons.Filled.Notifications,
label = "Notifications",
@ -469,37 +436,6 @@ 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
private fun SettingsRow(
icon: ImageVector,