mvp shell
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package com.postcard.ui.auth
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.postcard.data.repository.AuthRepository
|
||||
import com.postcard.data.repository.LoginRequest
|
||||
import com.postcard.data.repository.RegisterRequest
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AuthViewModel @Inject constructor(
|
||||
private val authRepository: AuthRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<AuthState>(AuthState.Idle)
|
||||
val uiState: StateFlow<AuthState> = _uiState.asStateFlow()
|
||||
|
||||
fun login(username: String, passwordHash: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = AuthState.Loading
|
||||
val result = authRepository.login(LoginRequest(username, passwordHash))
|
||||
if (result.isSuccess) {
|
||||
_uiState.value = AuthState.Success
|
||||
} else {
|
||||
_uiState.value = AuthState.Error(result.exceptionOrNull()?.message ?: "Login failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun register(username: String, displayName: String, passwordHash: String) {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = AuthState.Loading
|
||||
val result = authRepository.register(RegisterRequest(username, displayName, passwordHash))
|
||||
if (result.isSuccess) {
|
||||
_uiState.value = AuthState.Success
|
||||
} else {
|
||||
_uiState.value = AuthState.Error(result.exceptionOrNull()?.message ?: "Registration failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isLoggedIn(): Boolean = authRepository.isLoggedIn()
|
||||
|
||||
fun logout() {
|
||||
authRepository.logout()
|
||||
_uiState.value = AuthState.Idle
|
||||
}
|
||||
|
||||
fun resetState() {
|
||||
_uiState.value = AuthState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AuthState {
|
||||
object Idle : AuthState()
|
||||
object Loading : AuthState()
|
||||
object Success : AuthState()
|
||||
data class Error(val message: String) : AuthState()
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.postcard.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onNavigateToRegister: () -> Unit,
|
||||
onLoginSuccess: () -> Unit,
|
||||
viewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(uiState) {
|
||||
if (uiState is AuthState.Success) {
|
||||
onLoginSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("Login", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (uiState is AuthState.Error) {
|
||||
Text(
|
||||
text = (uiState as AuthState.Error).message,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.login(username, password) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState !is AuthState.Loading && username.isNotBlank() && password.isNotBlank()
|
||||
) {
|
||||
if (uiState is AuthState.Loading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary)
|
||||
} else {
|
||||
Text("Login")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
TextButton(onClick = {
|
||||
viewModel.resetState()
|
||||
onNavigateToRegister()
|
||||
}) {
|
||||
Text("Don't have an account? Register")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.postcard.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
@Composable
|
||||
fun RegisterScreen(
|
||||
onNavigateToLogin: () -> Unit,
|
||||
onRegisterSuccess: () -> Unit,
|
||||
viewModel: AuthViewModel = hiltViewModel()
|
||||
) {
|
||||
var username by remember { mutableStateOf("") }
|
||||
var displayName by remember { mutableStateOf("") }
|
||||
var password by remember { mutableStateOf("") }
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(uiState) {
|
||||
if (uiState is AuthState.Success) {
|
||||
onRegisterSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text("Register", style = MaterialTheme.typography.headlineMedium)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = displayName,
|
||||
onValueChange = { displayName = it },
|
||||
label = { Text("Display Name") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("Password") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (uiState is AuthState.Error) {
|
||||
Text(
|
||||
text = (uiState as AuthState.Error).message,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.register(username, displayName, password) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState !is AuthState.Loading && username.isNotBlank() && displayName.isNotBlank() && password.isNotBlank()
|
||||
) {
|
||||
if (uiState is AuthState.Loading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), color = MaterialTheme.colorScheme.onPrimary)
|
||||
} else {
|
||||
Text("Register")
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
TextButton(onClick = {
|
||||
viewModel.resetState()
|
||||
onNavigateToLogin()
|
||||
}) {
|
||||
Text("Already have an account? Login")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.postcard.ui.conversations
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ConversationListScreen(
|
||||
onLogout: () -> Unit
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Conversation List")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.postcard.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val Purple80 = Color(0xFFD0BCFF)
|
||||
val PurpleGrey80 = Color(0xFFCCC2DC)
|
||||
val Pink80 = Color(0xFFEFB8C8)
|
||||
|
||||
val Purple40 = Color(0xFF6650a4)
|
||||
val PurpleGrey40 = Color(0xFF625b71)
|
||||
val Pink40 = Color(0xFF7D5260)
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.postcard.ui.theme
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
|
||||
private val DarkColorScheme = darkColorScheme(
|
||||
primary = Purple80,
|
||||
secondary = PurpleGrey80,
|
||||
tertiary = Pink80
|
||||
)
|
||||
|
||||
private val LightColorScheme = lightColorScheme(
|
||||
primary = Purple40,
|
||||
secondary = PurpleGrey40,
|
||||
tertiary = Pink40
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun PostcardTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
darkTheme -> DarkColorScheme
|
||||
else -> LightColorScheme
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colorScheme.primary.toArgb()
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user