feat: Implement setup screen and login logic
- Add SetupScreen with UI for server URL, username, and password input. - Make SetupScreen the initial screen in the navigation graph. - Implement secure credential storage using EncryptedSharedPreferences. - Create CredentialsRepository and AuthRepository to manage credentials and auth tokens. - Add LoginUseCase to handle the business logic for logging in. - Implement a temporary Retrofit client in ItemRepository to handle login against a user-provided URL. - Integrate login logic into SetupViewModel. - Update all relevant project documentation and DI modules.
This commit is contained in:
@@ -82,6 +82,9 @@ dependencies {
|
||||
// [DEPENDENCY] Logging
|
||||
implementation(Libs.timber)
|
||||
|
||||
// [DEPENDENCY] Security
|
||||
implementation(Libs.securityCrypto)
|
||||
|
||||
// [DEPENDENCY] Testing
|
||||
testImplementation(Libs.junit)
|
||||
androidTestImplementation(Libs.extJunit)
|
||||
|
||||
@@ -8,6 +8,13 @@ import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.homebox.lens.ui.screen.dashboard.DashboardScreen
|
||||
import com.homebox.lens.ui.screen.inventorylist.InventoryListScreen
|
||||
import com.homebox.lens.ui.screen.itemdetails.ItemDetailsScreen
|
||||
import com.homebox.lens.ui.screen.itemedit.ItemEditScreen
|
||||
import com.homebox.lens.ui.screen.labelslist.LabelsListScreen
|
||||
import com.homebox.lens.ui.screen.locationslist.LocationsListScreen
|
||||
import com.homebox.lens.ui.screen.search.SearchScreen
|
||||
import com.homebox.lens.ui.screen.setup.SetupScreen
|
||||
|
||||
// [CORE-LOGIC]
|
||||
/**
|
||||
@@ -19,12 +26,36 @@ fun NavGraph() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = Screen.Dashboard.route
|
||||
startDestination = Screen.Setup.route
|
||||
) {
|
||||
composable(route = Screen.Setup.route) {
|
||||
SetupScreen(onSetupComplete = {
|
||||
navController.navigate(Screen.Dashboard.route) {
|
||||
popUpTo(Screen.Setup.route) { inclusive = true }
|
||||
}
|
||||
})
|
||||
}
|
||||
composable(route = Screen.Dashboard.route) {
|
||||
DashboardScreen()
|
||||
}
|
||||
// TODO: Добавить остальные экраны в граф навигации
|
||||
composable(route = Screen.InventoryList.route) {
|
||||
InventoryListScreen()
|
||||
}
|
||||
composable(route = Screen.ItemDetails.route) {
|
||||
ItemDetailsScreen()
|
||||
}
|
||||
composable(route = Screen.ItemEdit.route) {
|
||||
ItemEditScreen()
|
||||
}
|
||||
composable(route = Screen.LabelsList.route) {
|
||||
LabelsListScreen()
|
||||
}
|
||||
composable(route = Screen.LocationsList.route) {
|
||||
LocationsListScreen()
|
||||
}
|
||||
composable(route = Screen.Search.route) {
|
||||
SearchScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_NavGraph.kt]
|
||||
@@ -11,14 +11,17 @@ package com.homebox.lens.navigation
|
||||
* @property route Строковый идентификатор маршрута.
|
||||
*/
|
||||
sealed class Screen(val route: String) {
|
||||
/**
|
||||
* [CONTRACT]
|
||||
* Представляет экран "Дэшборд".
|
||||
*/
|
||||
data object Setup : Screen("setup_screen")
|
||||
data object Dashboard : Screen("dashboard_screen")
|
||||
|
||||
// TODO: Добавить объекты для остальных экранов:
|
||||
// data object ItemDetails : Screen("item_details_screen")
|
||||
// data object Search : Screen("search_screen")
|
||||
data object InventoryList : Screen("inventory_list_screen")
|
||||
data object ItemDetails : Screen("item_details_screen/{itemId}") {
|
||||
fun createRoute(itemId: String) = "item_details_screen/$itemId"
|
||||
}
|
||||
data object ItemEdit : Screen("item_edit_screen/{itemId}") {
|
||||
fun createRoute(itemId: String) = "item_edit_screen/$itemId"
|
||||
}
|
||||
data object LabelsList : Screen("labels_list_screen")
|
||||
data object LocationsList : Screen("locations_list_screen")
|
||||
data object Search : Screen("search_screen")
|
||||
}
|
||||
// [END_FILE_Screen.kt]
|
||||
@@ -0,0 +1,113 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupScreen.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
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.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
|
||||
// [ENTRYPOINT]
|
||||
@Composable
|
||||
fun SetupScreen(
|
||||
viewModel: SetupViewModel = hiltViewModel(),
|
||||
onSetupComplete: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
if (uiState.isSetupComplete) {
|
||||
onSetupComplete()
|
||||
}
|
||||
|
||||
SetupScreenContent(
|
||||
uiState = uiState,
|
||||
onServerUrlChange = viewModel::onServerUrlChange,
|
||||
onUsernameChange = viewModel::onUsernameChange,
|
||||
onPasswordChange = viewModel::onPasswordChange,
|
||||
onConnectClick = viewModel::connect
|
||||
)
|
||||
}
|
||||
|
||||
// [CONTENT]
|
||||
@Composable
|
||||
private fun SetupScreenContent(
|
||||
uiState: SetupUiState,
|
||||
onServerUrlChange: (String) -> Unit,
|
||||
onUsernameChange: (String) -> Unit,
|
||||
onPasswordChange: (String) -> Unit,
|
||||
onConnectClick: () -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text("Server Setup") })
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text("Server URL") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text("Username") },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text("Password") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onConnectClick,
|
||||
enabled = !uiState.isLoading,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
} else {
|
||||
Text("Connect")
|
||||
}
|
||||
}
|
||||
uiState.error?.let {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(text = it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun SetupScreenPreview() {
|
||||
SetupScreenContent(
|
||||
uiState = SetupUiState(error = "Failed to connect"),
|
||||
onServerUrlChange = {},
|
||||
onUsernameChange = {},
|
||||
onPasswordChange = {},
|
||||
onConnectClick = {}
|
||||
)
|
||||
}
|
||||
// [END_FILE_SetupScreen.kt]
|
||||
@@ -0,0 +1,15 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupUiState.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
// [STATE]
|
||||
data class SetupUiState(
|
||||
val serverUrl: String = "",
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null,
|
||||
val isSetupComplete: Boolean = false
|
||||
)
|
||||
// [END_FILE_SetupUiState.kt]
|
||||
@@ -0,0 +1,88 @@
|
||||
// [PACKAGE] com.homebox.lens.ui.screen.setup
|
||||
// [FILE] SetupViewModel.kt
|
||||
|
||||
package com.homebox.lens.ui.screen.setup
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.homebox.lens.domain.model.Credentials
|
||||
import com.homebox.lens.domain.model.Result
|
||||
import com.homebox.lens.domain.repository.CredentialsRepository
|
||||
import com.homebox.lens.domain.usecase.LoginUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
// [VIEWMODEL]
|
||||
@HiltViewModel
|
||||
class SetupViewModel @Inject constructor(
|
||||
private val credentialsRepository: CredentialsRepository,
|
||||
private val loginUseCase: LoginUseCase
|
||||
) : ViewModel() {
|
||||
|
||||
// [STATE]
|
||||
private val _uiState = MutableStateFlow(SetupUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadCredentials()
|
||||
}
|
||||
|
||||
private fun loadCredentials() {
|
||||
viewModelScope.launch {
|
||||
credentialsRepository.getCredentials().collect { credentials ->
|
||||
if (credentials != null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
serverUrl = credentials.serverUrl,
|
||||
username = credentials.username,
|
||||
password = credentials.password
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun onServerUrlChange(newUrl: String) {
|
||||
_uiState.update { it.copy(serverUrl = newUrl) }
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun onUsernameChange(newUsername: String) {
|
||||
_uiState.update { it.copy(username = newUsername) }
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun onPasswordChange(newPassword: String) {
|
||||
_uiState.update { it.copy(password = newPassword) }
|
||||
}
|
||||
|
||||
// [ACTION]
|
||||
fun connect() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(isLoading = true, error = null) }
|
||||
val credentials = Credentials(
|
||||
serverUrl = _uiState.value.serverUrl.trim(),
|
||||
username = _uiState.value.username.trim(),
|
||||
password = _uiState.value.password
|
||||
)
|
||||
|
||||
credentialsRepository.saveCredentials(credentials)
|
||||
|
||||
when (val result = loginUseCase(credentials)) {
|
||||
is Result.Success -> {
|
||||
_uiState.update { it.copy(isLoading = false, isSetupComplete = true) }
|
||||
}
|
||||
is Result.Error -> {
|
||||
_uiState.update { it.copy(isLoading = false, error = result.exception.message ?: "Login failed") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// [END_FILE_SetupViewModel.kt]
|
||||
Reference in New Issue
Block a user