android: align settings/profile with app theme and add real settings controls
This commit is contained in:
@@ -747,3 +747,12 @@
|
||||
- shows saved accounts,
|
||||
- allows switch/remove,
|
||||
- triggers auth recheck + chats reload on switch.
|
||||
|
||||
### Step 111 - Real Settings + persistent theme + add-account UX
|
||||
- Implemented persistent app theme storage via DataStore (`ThemeRepository`) and applied theme mode on app start in `MainActivity`.
|
||||
- Reworked `SettingsScreen` to contain only working settings and actions:
|
||||
- multi-account list (`Switch`/`Remove`) + `Add account` dialog with email/password sign-in,
|
||||
- appearance (`Light`/`Dark`/`System`) wired to persisted theme,
|
||||
- notifications (`global` + `preview`) wired to `NotificationSettingsRepository`,
|
||||
- privacy update, blocked users management, sessions revoke/revoke-all, 2FA controls.
|
||||
- Updated `ProfileScreen` to follow current app theme colors instead of forced dark palette.
|
||||
|
||||
@@ -12,13 +12,20 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import ru.daemonlord.messenger.core.notifications.NotificationIntentExtras
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
import ru.daemonlord.messenger.domain.settings.repository.ThemeRepository
|
||||
import ru.daemonlord.messenger.ui.navigation.MessengerNavHost
|
||||
import ru.daemonlord.messenger.ui.theme.MessengerTheme
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MainActivity : AppCompatActivity() {
|
||||
@Inject
|
||||
lateinit var themeRepository: ThemeRepository
|
||||
private var pendingInviteToken by mutableStateOf<String?>(null)
|
||||
private var pendingVerifyEmailToken by mutableStateOf<String?>(null)
|
||||
private var pendingResetPasswordToken by mutableStateOf<String?>(null)
|
||||
@@ -26,6 +33,14 @@ class MainActivity : AppCompatActivity() {
|
||||
private var pendingNotificationMessageId by mutableStateOf<Long?>(null)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
val savedThemeMode = runBlocking { themeRepository.getThemeMode() }
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (savedThemeMode) {
|
||||
AppThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
AppThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
AppThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
super.onCreate(savedInstanceState)
|
||||
pendingInviteToken = intent.extractInviteToken()
|
||||
pendingVerifyEmailToken = intent.extractVerifyEmailToken()
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package ru.daemonlord.messenger.data.settings.repository
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
import ru.daemonlord.messenger.domain.settings.repository.ThemeRepository
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DataStoreThemeRepository @Inject constructor(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
) : ThemeRepository {
|
||||
|
||||
override fun observeThemeMode(): Flow<AppThemeMode> {
|
||||
return dataStore.data.map { prefs ->
|
||||
prefs[THEME_MODE_KEY]
|
||||
?.let { raw -> runCatching { AppThemeMode.valueOf(raw) }.getOrNull() }
|
||||
?: AppThemeMode.SYSTEM
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getThemeMode(): AppThemeMode {
|
||||
return observeThemeMode().first()
|
||||
}
|
||||
|
||||
override suspend fun setThemeMode(mode: AppThemeMode) {
|
||||
dataStore.edit { prefs ->
|
||||
prefs[THEME_MODE_KEY] = mode.name
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
val THEME_MODE_KEY = stringPreferencesKey("app_theme_mode")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import ru.daemonlord.messenger.data.media.repository.NetworkMediaRepository
|
||||
import ru.daemonlord.messenger.data.message.repository.NetworkMessageRepository
|
||||
import ru.daemonlord.messenger.data.notifications.repository.DataStoreNotificationSettingsRepository
|
||||
import ru.daemonlord.messenger.data.search.repository.NetworkSearchRepository
|
||||
import ru.daemonlord.messenger.data.settings.repository.DataStoreThemeRepository
|
||||
import ru.daemonlord.messenger.data.user.repository.NetworkAccountRepository
|
||||
import ru.daemonlord.messenger.domain.account.repository.AccountRepository
|
||||
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
|
||||
@@ -22,6 +23,7 @@ import ru.daemonlord.messenger.domain.media.repository.MediaRepository
|
||||
import ru.daemonlord.messenger.domain.message.repository.MessageRepository
|
||||
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
|
||||
import ru.daemonlord.messenger.domain.search.repository.SearchRepository
|
||||
import ru.daemonlord.messenger.domain.settings.repository.ThemeRepository
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -81,4 +83,10 @@ abstract class RepositoryModule {
|
||||
abstract fun bindSearchRepository(
|
||||
repository: NetworkSearchRepository,
|
||||
): SearchRepository
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
abstract fun bindThemeRepository(
|
||||
repository: DataStoreThemeRepository,
|
||||
): ThemeRepository
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package ru.daemonlord.messenger.domain.settings.model
|
||||
|
||||
enum class AppThemeMode {
|
||||
LIGHT,
|
||||
DARK,
|
||||
SYSTEM,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package ru.daemonlord.messenger.domain.settings.repository
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
|
||||
interface ThemeRepository {
|
||||
fun observeThemeMode(): Flow<AppThemeMode>
|
||||
suspend fun getThemeMode(): AppThemeMode
|
||||
suspend fun setThemeMode(mode: AppThemeMode)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package ru.daemonlord.messenger.ui.account
|
||||
import ru.daemonlord.messenger.domain.account.model.UserSearchItem
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthSession
|
||||
import ru.daemonlord.messenger.domain.auth.model.AuthUser
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
|
||||
data class AccountUiState(
|
||||
val isLoading: Boolean = false,
|
||||
@@ -16,6 +17,10 @@ data class AccountUiState(
|
||||
val recoveryCodesRemaining: Int? = null,
|
||||
val activeUserId: Long? = null,
|
||||
val storedAccounts: List<StoredAccountUi> = emptyList(),
|
||||
val appThemeMode: AppThemeMode = AppThemeMode.SYSTEM,
|
||||
val notificationsEnabled: Boolean = true,
|
||||
val notificationsPreviewEnabled: Boolean = true,
|
||||
val isAddingAccount: Boolean = false,
|
||||
val message: String? = null,
|
||||
val errorMessage: String? = null,
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@ package ru.daemonlord.messenger.ui.account
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -9,15 +10,22 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||
import ru.daemonlord.messenger.domain.auth.usecase.LoginUseCase
|
||||
import ru.daemonlord.messenger.domain.account.repository.AccountRepository
|
||||
import ru.daemonlord.messenger.domain.common.AppError
|
||||
import ru.daemonlord.messenger.domain.common.AppResult
|
||||
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
import ru.daemonlord.messenger.domain.settings.repository.ThemeRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AccountViewModel @Inject constructor(
|
||||
private val accountRepository: AccountRepository,
|
||||
private val tokenRepository: TokenRepository,
|
||||
private val loginUseCase: LoginUseCase,
|
||||
private val notificationSettingsRepository: NotificationSettingsRepository,
|
||||
private val themeRepository: ThemeRepository,
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(AccountUiState())
|
||||
val uiState: StateFlow<AccountUiState> = _uiState.asStateFlow()
|
||||
@@ -34,6 +42,8 @@ class AccountViewModel @Inject constructor(
|
||||
val blocked = accountRepository.listBlockedUsers()
|
||||
val activeUserId = tokenRepository.getActiveUserId()
|
||||
val storedAccounts = tokenRepository.getAccounts()
|
||||
val notificationSettings = notificationSettingsRepository.getSettings()
|
||||
val appThemeMode = themeRepository.getThemeMode()
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
isLoading = false,
|
||||
@@ -53,6 +63,9 @@ class AccountViewModel @Inject constructor(
|
||||
isActive = activeUserId == account.userId,
|
||||
)
|
||||
},
|
||||
notificationsEnabled = notificationSettings.globalEnabled,
|
||||
notificationsPreviewEnabled = notificationSettings.previewEnabled,
|
||||
appThemeMode = appThemeMode,
|
||||
errorMessage = listOf(me, sessions, blocked)
|
||||
.filterIsInstance<AppResult.Error>()
|
||||
.firstOrNull()
|
||||
@@ -63,6 +76,67 @@ class AccountViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun setThemeMode(mode: AppThemeMode) {
|
||||
viewModelScope.launch {
|
||||
themeRepository.setThemeMode(mode)
|
||||
AppCompatDelegate.setDefaultNightMode(
|
||||
when (mode) {
|
||||
AppThemeMode.LIGHT -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
AppThemeMode.DARK -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
AppThemeMode.SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
)
|
||||
_uiState.update { it.copy(appThemeMode = mode) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setGlobalNotificationsEnabled(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
notificationSettingsRepository.setGlobalEnabled(enabled)
|
||||
_uiState.update { it.copy(notificationsEnabled = enabled) }
|
||||
}
|
||||
}
|
||||
|
||||
fun setNotificationPreviewEnabled(enabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
notificationSettingsRepository.setPreviewEnabled(enabled)
|
||||
_uiState.update { it.copy(notificationsPreviewEnabled = enabled) }
|
||||
}
|
||||
}
|
||||
|
||||
fun addAccount(email: String, password: String, onDone: (Boolean) -> Unit = {}) {
|
||||
val normalizedEmail = email.trim()
|
||||
if (normalizedEmail.isBlank() || password.isBlank()) {
|
||||
_uiState.update { it.copy(errorMessage = "Email and password are required.") }
|
||||
onDone(false)
|
||||
return
|
||||
}
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isAddingAccount = true, errorMessage = null, message = null) }
|
||||
when (val result = loginUseCase(normalizedEmail, password)) {
|
||||
is AppResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isAddingAccount = false,
|
||||
message = "Account added.",
|
||||
)
|
||||
}
|
||||
refresh()
|
||||
onDone(true)
|
||||
}
|
||||
is AppResult.Error -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isAddingAccount = false,
|
||||
errorMessage = result.reason.toUiMessage(),
|
||||
)
|
||||
}
|
||||
onDone(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun switchStoredAccount(userId: Long, onSwitched: (Boolean) -> Unit = {}) {
|
||||
viewModelScope.launch {
|
||||
val switched = tokenRepository.switchAccount(userId)
|
||||
|
||||
@@ -43,7 +43,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -65,10 +64,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
private val ProfileBg = Color(0xFF0D0F12)
|
||||
private val ProfileCard = Color(0xFF171A1F)
|
||||
private val ProfileMuted = Color(0xFFA2A7B3)
|
||||
|
||||
@Composable
|
||||
fun ProfileRoute(
|
||||
onBackToChats: () -> Unit,
|
||||
@@ -97,7 +92,7 @@ fun ProfileScreen(
|
||||
var username by remember(profile?.username) { mutableStateOf(profile?.username.orEmpty()) }
|
||||
var bio by remember(profile?.bio) { mutableStateOf(profile?.bio.orEmpty()) }
|
||||
var avatarUrl by remember(profile?.avatarUrl) { mutableStateOf(profile?.avatarUrl.orEmpty()) }
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
var selectedTab by remember { mutableStateOf(0) }
|
||||
var editMode by remember { mutableStateOf(false) }
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -136,7 +131,7 @@ fun ProfileScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(ProfileBg)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
@@ -153,7 +148,11 @@ fun ProfileScreen(
|
||||
.height(305.dp)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color(0xFF9F4649), Color(0xFFB85C61), Color(0xFF9D4A4C)),
|
||||
colors = listOf(
|
||||
MaterialTheme.colorScheme.primaryContainer,
|
||||
MaterialTheme.colorScheme.secondaryContainer,
|
||||
MaterialTheme.colorScheme.tertiaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
) {
|
||||
@@ -172,20 +171,20 @@ fun ProfileScreen(
|
||||
modifier = Modifier
|
||||
.size(108.dp)
|
||||
.clip(CircleShape)
|
||||
.border(1.dp, Color(0x88FFFFFF), CircleShape),
|
||||
.border(1.dp, MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.5f), CircleShape),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(108.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0x55FFFFFF)),
|
||||
.background(MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = name.firstOrNull()?.uppercase() ?: "?",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
color = Color.White,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -193,12 +192,12 @@ fun ProfileScreen(
|
||||
Text(
|
||||
text = if (name.isBlank()) "User" else name,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = Color.White,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
Text("online", color = Color(0xE6FFFFFF), style = MaterialTheme.typography.bodyLarge)
|
||||
Text("online", color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f), style = MaterialTheme.typography.bodyLarge)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -234,7 +233,7 @@ fun ProfileScreen(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -254,7 +253,7 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(18.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -271,7 +270,7 @@ fun ProfileScreen(
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -282,10 +281,10 @@ fun ProfileScreen(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text("No posts yet", color = Color.White, style = MaterialTheme.typography.titleLarge)
|
||||
Text("No posts yet", style = MaterialTheme.typography.titleLarge)
|
||||
Text(
|
||||
"Publish something in your profile.",
|
||||
color = ProfileMuted,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Button(onClick = {}, modifier = Modifier.padding(top = 6.dp)) {
|
||||
@@ -296,7 +295,7 @@ fun ProfileScreen(
|
||||
|
||||
if (editMode) {
|
||||
Surface(
|
||||
color = ProfileCard,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -306,7 +305,7 @@ fun ProfileScreen(
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text("Edit profile", color = Color.White, style = MaterialTheme.typography.titleMedium)
|
||||
Text("Edit profile", style = MaterialTheme.typography.titleMedium)
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
@@ -390,7 +389,7 @@ private fun HeroActionButton(
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Surface(
|
||||
color = Color(0x40FFFFFF),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.8f),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
modifier = modifier,
|
||||
) {
|
||||
@@ -407,8 +406,8 @@ private fun HeroActionButton(
|
||||
@Composable
|
||||
private fun ProfileInfoRow(label: String, value: String) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(text = value, style = MaterialTheme.typography.titleLarge, color = Color.White)
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium, color = ProfileMuted)
|
||||
Text(text = value, style = MaterialTheme.typography.titleLarge)
|
||||
Text(text = label, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
package ru.daemonlord.messenger.ui.settings
|
||||
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
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.safeDrawing
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -22,37 +19,37 @@ import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.HelpOutline
|
||||
import androidx.compose.material.icons.filled.BatterySaver
|
||||
import androidx.compose.material.icons.filled.ChatBubbleOutline
|
||||
import androidx.compose.material.icons.filled.DataUsage
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.DeleteOutline
|
||||
import androidx.compose.material.icons.filled.Devices
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Notifications
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Shield
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Security
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -61,12 +58,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import ru.daemonlord.messenger.domain.settings.model.AppThemeMode
|
||||
import ru.daemonlord.messenger.ui.account.AccountViewModel
|
||||
|
||||
private val SettingsBackground = Color(0xFF0D0F12)
|
||||
private val SettingsCard = Color(0xFF1A1D23)
|
||||
private val SettingsMuted = Color(0xFF9EA3B0)
|
||||
|
||||
@Composable
|
||||
fun SettingsRoute(
|
||||
onBackToChats: () -> Unit,
|
||||
@@ -99,7 +93,19 @@ fun SettingsScreen(
|
||||
val profile = state.profile
|
||||
val scrollState = rememberScrollState()
|
||||
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
|
||||
var nightMode by remember { mutableIntStateOf(AppCompatDelegate.getDefaultNightMode()) }
|
||||
|
||||
var showAddAccountDialog by remember { mutableStateOf(false) }
|
||||
var addEmail by remember { mutableStateOf("") }
|
||||
var addPassword by remember { mutableStateOf("") }
|
||||
|
||||
var privacyPm by remember(profile?.privacyPrivateMessages) { mutableStateOf(profile?.privacyPrivateMessages ?: "everyone") }
|
||||
var privacyLastSeen by remember(profile?.privacyLastSeen) { mutableStateOf(profile?.privacyLastSeen ?: "everyone") }
|
||||
var privacyAvatar by remember(profile?.privacyAvatar) { mutableStateOf(profile?.privacyAvatar ?: "everyone") }
|
||||
var privacyGroupInvites by remember(profile?.privacyGroupInvites) { mutableStateOf(profile?.privacyGroupInvites ?: "everyone") }
|
||||
|
||||
var blockUserIdInput by remember { mutableStateOf("") }
|
||||
var twoFactorCode by remember { mutableStateOf("") }
|
||||
var recoveryRegenerateCode by remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
@@ -118,10 +124,58 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddAccountDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (!state.isAddingAccount) showAddAccountDialog = false },
|
||||
title = { Text("Add account") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(
|
||||
value = addEmail,
|
||||
onValueChange = { addEmail = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text("Email") },
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = addPassword,
|
||||
onValueChange = { addPassword = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
label = { Text("Password") },
|
||||
)
|
||||
if (state.isAddingAccount) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.addAccount(addEmail, addPassword) { success ->
|
||||
if (success) {
|
||||
showAddAccountDialog = false
|
||||
addEmail = ""
|
||||
addPassword = ""
|
||||
onSwitchAccount()
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = !state.isAddingAccount,
|
||||
) { Text("Sign in") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { if (!state.isAddingAccount) showAddAccountDialog = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(SettingsBackground)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.windowInsetsPadding(WindowInsets.safeDrawing),
|
||||
contentAlignment = Alignment.TopCenter,
|
||||
) {
|
||||
@@ -139,14 +193,14 @@ fun SettingsScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text("Settings", style = MaterialTheme.typography.headlineSmall, color = Color.White)
|
||||
Text("Settings", style = MaterialTheme.typography.headlineSmall)
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(22.dp), strokeWidth = 2.dp)
|
||||
}
|
||||
}
|
||||
|
||||
Surface(
|
||||
color = SettingsCard,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -170,13 +224,12 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.size(58.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF6650A4)),
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = profile?.name?.firstOrNull()?.uppercase() ?: "?",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
}
|
||||
@@ -185,7 +238,6 @@ fun SettingsScreen(
|
||||
Text(
|
||||
text = profile?.name ?: "Loading...",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@@ -195,25 +247,35 @@ fun SettingsScreen(
|
||||
profile?.username?.takeIf { it.isNotBlank() }?.let { "@$it" },
|
||||
).joinToString(" • "),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = SettingsMuted,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onOpenProfile) {
|
||||
Icon(Icons.Filled.Person, contentDescription = "Profile", tint = Color.White)
|
||||
Icon(Icons.Filled.Person, contentDescription = "Profile")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Accounts", style = MaterialTheme.typography.titleSmall, color = Color.White)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text("Accounts", style = MaterialTheme.typography.titleSmall)
|
||||
OutlinedButton(onClick = { showAddAccountDialog = true }) {
|
||||
Icon(Icons.Filled.Add, contentDescription = null)
|
||||
Text("Add account", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
}
|
||||
state.storedAccounts.forEach { account ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(Color(0x141F232B))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
@@ -229,12 +291,11 @@ fun SettingsScreen(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF2A2F38)),
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = account.title.firstOrNull()?.uppercase() ?: "?",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
}
|
||||
@@ -242,19 +303,18 @@ fun SettingsScreen(
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = account.title,
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Text(
|
||||
text = account.subtitle,
|
||||
color = SettingsMuted,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
if (account.isActive) {
|
||||
Text("Active", color = Color(0xFF9EDB9D), style = MaterialTheme.typography.labelSmall)
|
||||
Text("Active", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelSmall)
|
||||
} else {
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
@@ -267,57 +327,205 @@ fun SettingsScreen(
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.removeStoredAccount(account.userId) }) {
|
||||
Text("Remove")
|
||||
Icon(Icons.Filled.DeleteOutline, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
SettingsRow(Icons.Filled.Person, "Account", "Name, username, bio") { onOpenProfile() }
|
||||
SettingsRow(Icons.Filled.ChatBubbleOutline, "Chat settings", "Wallpaper, media, interactions") {}
|
||||
SettingsRow(Icons.Filled.Lock, "Privacy", "Last seen, messages, avatar") {}
|
||||
SettingsRow(Icons.Filled.Notifications, "Notifications", "Sounds and counters") {}
|
||||
SettingsRow(Icons.Filled.DataUsage, "Data and storage", "Media auto-download") {}
|
||||
SettingsRow(Icons.Filled.Devices, "Devices", "${state.sessions.size} active sessions") {}
|
||||
SettingsRow(Icons.Filled.BatterySaver, "Power saving", "Animation and media limits") {}
|
||||
SettingsRow(Icons.Filled.Language, "Language", "English") {}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Appearance", style = MaterialTheme.typography.titleSmall, color = Color.White)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text("Appearance", style = MaterialTheme.typography.titleSmall)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ThemeButton(
|
||||
text = "Light",
|
||||
selected = nightMode == AppCompatDelegate.MODE_NIGHT_NO,
|
||||
) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_NO
|
||||
}
|
||||
selected = state.appThemeMode == AppThemeMode.LIGHT,
|
||||
) { viewModel.setThemeMode(AppThemeMode.LIGHT) }
|
||||
ThemeButton(
|
||||
text = "Dark",
|
||||
selected = nightMode == AppCompatDelegate.MODE_NIGHT_YES,
|
||||
) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_YES
|
||||
}
|
||||
selected = state.appThemeMode == AppThemeMode.DARK,
|
||||
) { viewModel.setThemeMode(AppThemeMode.DARK) }
|
||||
ThemeButton(
|
||||
text = "System",
|
||||
selected = nightMode == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM,
|
||||
selected = state.appThemeMode == AppThemeMode.SYSTEM,
|
||||
) { viewModel.setThemeMode(AppThemeMode.SYSTEM) }
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Notifications", style = MaterialTheme.typography.titleSmall)
|
||||
SettingsToggleRow(
|
||||
icon = Icons.Filled.Notifications,
|
||||
title = "Enable notifications",
|
||||
checked = state.notificationsEnabled,
|
||||
onCheckedChange = viewModel::setGlobalNotificationsEnabled,
|
||||
)
|
||||
SettingsToggleRow(
|
||||
icon = Icons.Filled.Visibility,
|
||||
title = "Show message preview",
|
||||
checked = state.notificationsPreviewEnabled,
|
||||
onCheckedChange = viewModel::setNotificationPreviewEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
Text("Privacy", style = MaterialTheme.typography.titleSmall)
|
||||
OutlinedTextField(
|
||||
value = privacyPm,
|
||||
onValueChange = { privacyPm = it },
|
||||
label = { Text("Private messages") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = privacyLastSeen,
|
||||
onValueChange = { privacyLastSeen = it },
|
||||
label = { Text("Last seen") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = privacyAvatar,
|
||||
onValueChange = { privacyAvatar = it },
|
||||
label = { Text("Avatar") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = privacyGroupInvites,
|
||||
onValueChange = { privacyGroupInvites = it },
|
||||
label = { Text("Group invites") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
Button(
|
||||
onClick = {
|
||||
viewModel.updatePrivacy(
|
||||
privateMessages = privacyPm,
|
||||
lastSeen = privacyLastSeen,
|
||||
avatar = privacyAvatar,
|
||||
groupInvites = privacyGroupInvites,
|
||||
)
|
||||
},
|
||||
enabled = !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Filled.Lock, contentDescription = null)
|
||||
Text("Save privacy", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = blockUserIdInput,
|
||||
onValueChange = { blockUserIdInput = it.filter(Char::isDigit) },
|
||||
label = { Text("User ID to block") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
blockUserIdInput.toLongOrNull()?.let(viewModel::blockUser)
|
||||
blockUserIdInput = ""
|
||||
},
|
||||
enabled = blockUserIdInput.isNotBlank() && !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Block user")
|
||||
}
|
||||
state.blockedUsers.forEach { blocked ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
nightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(blocked.name, style = MaterialTheme.typography.bodyMedium)
|
||||
if (!blocked.username.isNullOrBlank()) {
|
||||
Text("@${blocked.username}", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.unblockUser(blocked.id) }) {
|
||||
Text("Unblock")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
SettingsRow(Icons.Filled.Shield, "Revoke all sessions", "Sign out from all other devices") {
|
||||
viewModel.revokeAllSessions()
|
||||
Text("Sessions & Security", style = MaterialTheme.typography.titleSmall)
|
||||
state.sessions.forEach { session ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Icon(Icons.Filled.Devices, contentDescription = null)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(session.userAgent ?: "Unknown device", style = MaterialTheme.typography.bodyMedium)
|
||||
Text(session.ipAddress ?: "Unknown IP", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.revokeSession(session.jti) },
|
||||
enabled = !state.isSaving && session.current != true,
|
||||
) {
|
||||
Text("Revoke")
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingsRow(Icons.Filled.Star, "Premium", "Placeholder section") {}
|
||||
SettingsRow(Icons.AutoMirrored.Filled.HelpOutline, "Help", "FAQ and support") {}
|
||||
OutlinedButton(
|
||||
onClick = viewModel::revokeAllSessions,
|
||||
enabled = !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Filled.Security, contentDescription = null)
|
||||
Text("Revoke all sessions", modifier = Modifier.padding(start = 6.dp))
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedButton(onClick = viewModel::setupTwoFactor, enabled = !state.isSaving) {
|
||||
Text("Setup 2FA")
|
||||
}
|
||||
OutlinedButton(onClick = viewModel::refreshRecoveryStatus, enabled = !state.isSaving) {
|
||||
Text("Recovery status")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = twoFactorCode,
|
||||
onValueChange = { twoFactorCode = it },
|
||||
label = { Text("2FA code") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(onClick = { viewModel.enableTwoFactor(twoFactorCode) }, enabled = twoFactorCode.isNotBlank() && !state.isSaving) {
|
||||
Text("Enable")
|
||||
}
|
||||
OutlinedButton(onClick = { viewModel.disableTwoFactor(twoFactorCode) }, enabled = twoFactorCode.isNotBlank() && !state.isSaving) {
|
||||
Text("Disable")
|
||||
}
|
||||
}
|
||||
OutlinedTextField(
|
||||
value = recoveryRegenerateCode,
|
||||
onValueChange = { recoveryRegenerateCode = it },
|
||||
label = { Text("Code for recovery regeneration") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.regenerateRecoveryCodes(recoveryRegenerateCode) },
|
||||
enabled = recoveryRegenerateCode.isNotBlank() && !state.isSaving,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text("Regenerate recovery codes")
|
||||
}
|
||||
if (state.recoveryCodesRemaining != null) {
|
||||
Text("Recovery codes left: ${state.recoveryCodesRemaining}", style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
state.recoveryCodes.forEach { code ->
|
||||
Text(code, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsSectionCard {
|
||||
SettingsActionRow(Icons.Filled.Email, "Profile", "Open profile") { onOpenProfile() }
|
||||
}
|
||||
|
||||
if (!state.message.isNullOrBlank()) {
|
||||
@@ -327,16 +535,10 @@ fun SettingsScreen(
|
||||
Text(state.errorMessage.orEmpty(), color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onBackToChats,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
OutlinedButton(onClick = onBackToChats, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Back to chats")
|
||||
}
|
||||
Button(
|
||||
onClick = onLogout,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Button(onClick = onLogout, modifier = Modifier.fillMaxWidth()) {
|
||||
Text("Logout")
|
||||
}
|
||||
}
|
||||
@@ -346,7 +548,7 @@ fun SettingsScreen(
|
||||
@Composable
|
||||
private fun SettingsSectionCard(content: @Composable ColumnScope.() -> Unit) {
|
||||
Surface(
|
||||
color = SettingsCard,
|
||||
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||
shape = RoundedCornerShape(22.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
@@ -354,14 +556,14 @@ private fun SettingsSectionCard(content: @Composable ColumnScope.() -> Unit) {
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsRow(
|
||||
private fun SettingsActionRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
@@ -371,23 +573,15 @@ private fun SettingsRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(Color(0x141F232B))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(34.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF2A2F38)),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = Color(0xFFB38BFF))
|
||||
}
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(title, color = Color.White, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(subtitle, color = SettingsMuted, style = MaterialTheme.typography.bodySmall)
|
||||
Text(title, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(subtitle, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
OutlinedButton(onClick = onClick) {
|
||||
Text("Open")
|
||||
@@ -395,6 +589,28 @@ private fun SettingsRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsToggleRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceContainerHighest)
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = MaterialTheme.colorScheme.primary)
|
||||
Text(title, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.weight(1f))
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThemeButton(
|
||||
text: String,
|
||||
|
||||
Reference in New Issue
Block a user