Add initial UI of AccountCredentialsScreen

This commit is contained in:
Shinokuni 2024-04-29 00:03:28 +02:00
parent 2c105f596a
commit cc7b874ef5
6 changed files with 263 additions and 16 deletions

View File

@ -1,6 +1,7 @@
package com.readrops.app.compose
import com.readrops.app.compose.account.AccountScreenModel
import com.readrops.app.compose.account.credentials.AccountCredentialsScreenModel
import com.readrops.app.compose.account.selection.AccountSelectionViewModel
import com.readrops.app.compose.feeds.FeedScreenModel
import com.readrops.app.compose.item.ItemScreenModel
@ -9,6 +10,7 @@ import com.readrops.app.compose.repositories.GetFoldersWithFeeds
import com.readrops.app.compose.repositories.LocalRSSRepository
import com.readrops.app.compose.timelime.TimelineScreenModel
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import org.koin.dsl.module
val composeAppModule = module {
@ -25,6 +27,8 @@ val composeAppModule = module {
ItemScreenModel(get(), itemId)
}
factory { (accountType: AccountType) -> AccountCredentialsScreenModel(accountType) }
single { GetFoldersWithFeeds(get()) }
// repositories

View File

@ -172,7 +172,7 @@ object AccountTab : Tab {
onDismiss = { screenModel.closeDialog() },
onValidate = { accountType ->
screenModel.closeDialog()
navigator.push(AccountCredentialsScreen(accountType, state.account))
navigator.push(AccountCredentialsScreen(accountType))
}
)
}

View File

@ -1,41 +1,173 @@
package com.readrops.app.compose.account.credentials
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
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.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.readrops.app.compose.home.HomeScreen
import com.readrops.app.compose.R
import com.readrops.app.compose.util.components.AndroidScreen
import com.readrops.db.entities.account.Account
import com.readrops.app.compose.util.theme.ShortSpacer
import com.readrops.app.compose.util.theme.VeryLargeSpacer
import com.readrops.app.compose.util.theme.spacing
import com.readrops.db.entities.account.AccountType
import org.koin.core.parameter.parametersOf
class AccountCredentialsScreen(
private val accountType: AccountType,
private val account: Account? = null,
private val accountType: AccountType
) : AndroidScreen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val screenModel =
getScreenModel<AccountCredentialsScreenModel>(parameters = { parametersOf(accountType) })
Column {
Text(
text = "AccountCredentialsScreen"
)
val state by screenModel.state.collectAsStateWithLifecycle()
Spacer(modifier = Modifier.size(16.dp))
Button(onClick = { navigator.replaceAll(HomeScreen()) }) {
Text(
text = "skip"
Box(
modifier = Modifier.imePadding()
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
.padding(MaterialTheme.spacing.largeSpacing)
.verticalScroll(rememberScrollState())
) {
Image(
painter = painterResource(id = accountType.iconRes),
contentDescription = null,
modifier = Modifier.size(48.dp)
)
ShortSpacer()
Text(
text = stringResource(id = accountType.typeName),
style = MaterialTheme.typography.headlineMedium
)
VeryLargeSpacer()
OutlinedTextField(
value = state.name,
onValueChange = { screenModel.onEvent(Event.NameEvent(it)) },
label = { Text(text = stringResource(id = R.string.account_name)) },
singleLine = true,
isError = state.isNameError,
supportingText = { Text(text = state.nameError?.errorText().orEmpty()) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
modifier = Modifier.fillMaxWidth()
)
ShortSpacer()
OutlinedTextField(
value = state.url,
onValueChange = { screenModel.onEvent(Event.URLEvent(it)) },
label = { Text(text = stringResource(id = R.string.account_url)) },
singleLine = true,
isError = state.isUrlError,
supportingText = { Text(text = state.urlError?.errorText().orEmpty()) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
modifier = Modifier.fillMaxWidth()
)
ShortSpacer()
OutlinedTextField(
value = state.login,
onValueChange = { screenModel.onEvent(Event.LoginEvent(it)) },
label = { Text(text = stringResource(id = R.string.login)) },
singleLine = true,
isError = state.isLoginError,
supportingText = { Text(text = state.passwordError?.errorText().orEmpty()) },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
modifier = Modifier.fillMaxWidth()
)
ShortSpacer()
OutlinedTextField(
value = state.password,
onValueChange = { screenModel.onEvent(Event.PasswordEvent(it)) },
label = { Text(text = stringResource(id = R.string.password)) },
trailingIcon = {
IconButton(
onClick = { screenModel.setPasswordVisibility(!state.isPasswordVisible) }
) {
Icon(
painter = painterResource(
id = if (state.isPasswordVisible)
R.drawable.ic_visible_off
else R.drawable.ic_visible
),
contentDescription = null
)
}
},
singleLine = true,
visualTransformation = if (state.isPasswordVisible)
VisualTransformation.None
else
PasswordVisualTransformation(),
isError = state.isPasswordError,
supportingText = { Text(text = state.passwordError?.errorText().orEmpty()) },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
modifier = Modifier.fillMaxWidth()
)
ShortSpacer()
Button(
onClick = { screenModel.login() },
modifier = Modifier.fillMaxWidth()
) {
if (state.isLoginStarted) {
CircularProgressIndicator(
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp,
modifier = Modifier.size(16.dp)
)
} else {
Text(text = stringResource(id = R.string.validate))
}
}
}
}
}
}
}

View File

@ -0,0 +1,101 @@
package com.readrops.app.compose.account.credentials
import android.util.Patterns
import cafe.adriel.voyager.core.model.StateScreenModel
import com.readrops.app.compose.util.components.TextFieldError
import com.readrops.db.entities.account.Account
import com.readrops.db.entities.account.AccountType
import kotlinx.coroutines.flow.update
class AccountCredentialsScreenModel(
private val accountType: AccountType
) : StateScreenModel<AccountCredentialsState>(AccountCredentialsState(name = accountType.name)) {
fun onEvent(event: Event): Unit = with(mutableState) {
when (event) {
is Event.LoginEvent -> update { it.copy(login = event.value, loginError = null) }
is Event.NameEvent -> update { it.copy(name = event.value, nameError = null) }
is Event.PasswordEvent -> update { it.copy(password = event.value, passwordError = null) }
is Event.URLEvent -> update { it.copy(url = event.value, urlError = null) }
}
}
fun setPasswordVisibility(isVisible: Boolean) {
mutableState.update { it.copy(isPasswordVisible = isVisible) }
}
fun login() {
if (validateFields()) {
mutableState.update { it.copy(isLoginStarted = true) }
with(state.value) {
val account = Account(
url = url,
accountName = name,
login = login,
password = password,
accountType = accountType
)
}
}
}
private fun validateFields(): Boolean = with(mutableState.value) {
var validate = true
if (url.isEmpty()) {
mutableState.update { it.copy(urlError = TextFieldError.EmptyField) }
validate = false
}
if (name.isEmpty()) {
mutableState.update { it.copy(nameError = TextFieldError.EmptyField) }
validate = false
}
if (login.isEmpty()) {
mutableState.update { it.copy(loginError = TextFieldError.EmptyField) }
validate = false
}
if (password.isEmpty()) {
mutableState.update { it.copy(passwordError = TextFieldError.EmptyField) }
validate = false
}
if (url.isNotEmpty() && !Patterns.WEB_URL.matcher(url).matches()) {
mutableState.update { it.copy(urlError = TextFieldError.BadUrl) }
validate = false
}
return validate
}
}
data class AccountCredentialsState(
val url: String = "https://",
val urlError: TextFieldError? = null,
val name: String = "",
val nameError: TextFieldError? = null,
val login: String = "",
val loginError: TextFieldError? = null,
val password: String = "",
val passwordError: TextFieldError? = null,
val isPasswordVisible: Boolean = false,
val isLoginStarted: Boolean = false
) {
val isUrlError = urlError != null
val isNameError = nameError != null
val isLoginError = loginError != null
val isPasswordError = passwordError != null
}
sealed class Event(val value: String) {
class URLEvent(value: String) : Event(value)
class NameEvent(value: String) : Event(value)
class LoginEvent(value: String) : Event(value)
class PasswordEvent(value: String) : Event(value)
}

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12,7c2.76,0 5,2.24 5,5 0,0.65 -0.13,1.26 -0.36,1.83l2.92,2.92c1.51,-1.26 2.7,-2.89 3.43,-4.75 -1.73,-4.39 -6,-7.5 -11,-7.5 -1.4,0 -2.74,0.25 -3.98,0.7l2.16,2.16C10.74,7.13 11.35,7 12,7zM2,4.27l2.28,2.28 0.46,0.46C3.08,8.3 1.78,10.02 1,12c1.73,4.39 6,7.5 11,7.5 1.55,0 3.03,-0.3 4.38,-0.84l0.42,0.42L19.73,22 21,20.73 3.27,3 2,4.27zM7.53,9.8l1.55,1.55c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.66 1.34,3 3,3 0.22,0 0.44,-0.03 0.65,-0.08l1.55,1.55c-0.67,0.33 -1.41,0.53 -2.2,0.53 -2.76,0 -5,-2.24 -5,-5 0,-0.79 0.2,-1.53 0.53,-2.2zM11.84,9.02l3.15,3.15 0.02,-0.16c0,-1.66 -1.34,-3 -3,-3l-0.17,0.01z"/>
</vector>