Rewrite AddWebdavMountActivity to Compose (#630)

* Migrated AddWebdavMountActivity to Compose

Signed-off-by: Arnau Mora <arnyminerz@proton.me>

* Minor changes, use PasswordTextField

---------

Signed-off-by: Arnau Mora <arnyminerz@proton.me>
Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
Arnau Mora 2024-03-10 18:33:22 +01:00 committed by GitHub
parent 3edcc02a21
commit 66f0075cc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 267 additions and 195 deletions

View file

@ -166,8 +166,9 @@
android:theme="@style/AppTheme.NoActionBar" />
<activity
android:name=".ui.webdav.AddWebdavMountActivity"
android:label="@string/webdav_add_mount_title"
android:parentActivityName=".ui.webdav.WebdavMountsActivity" />
android:parentActivityName=".ui.webdav.WebdavMountsActivity"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<!-- account type "DAVx⁵" -->
<service

View file

@ -5,33 +5,63 @@
package at.bitfire.davdroid.ui.webdav
import android.app.Application
import android.content.Context
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.annotation.StringRes
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.lifecycle.AndroidViewModel
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.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.DavResource
import at.bitfire.dav4jvm.UrlUtils
import at.bitfire.davdroid.App
import at.bitfire.davdroid.R
import at.bitfire.davdroid.databinding.ActivityAddWebdavMountBinding
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.db.WebDavMount
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.ui.UiUtils
import at.bitfire.davdroid.ui.widget.PasswordTextField
import at.bitfire.davdroid.webdav.CredentialsStore
import at.bitfire.davdroid.webdav.DavDocumentsProvider
import com.google.android.material.snackbar.Snackbar
import com.google.accompanist.themeadapter.material.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
@ -45,50 +75,233 @@ import java.util.logging.Level
import javax.inject.Inject
@AndroidEntryPoint
class AddWebdavMountActivity: AppCompatActivity() {
class AddWebdavMountActivity : AppCompatActivity() {
lateinit var binding: ActivityAddWebdavMountBinding
val model by viewModels<Model>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityAddWebdavMountBinding.inflate(layoutInflater)
binding.lifecycleOwner = this
binding.model = model
setContentView(binding.root)
setContent {
val isLoading by model.isLoading.observeAsState(initial = false)
val error by model.error.observeAsState(initial = null)
val displayName by model.displayName.observeAsState(initial = "")
val displayNameError by model.displayNameError.observeAsState(initial = null)
val url by model.url.observeAsState(initial = "")
val urlError by model.urlError.observeAsState(initial = null)
val username by model.userName.observeAsState(initial = "")
val password by model.password.observeAsState(initial = "")
model.error.observe(this) { error ->
if (error != null) {
Snackbar.make(binding.root, error, Snackbar.LENGTH_LONG).show()
model.error.value = null
MdcTheme {
Layout(
isLoading = isLoading,
error = error,
onErrorClearRequested = { model.error.value = null },
displayName = displayName,
onDisplayNameChange = model.displayName::setValue,
displayNameError = displayNameError,
url = url,
onUrlChange = model.url::setValue,
urlError = urlError,
username = username,
onUsernameChange = model.userName::setValue,
password = password,
onPasswordChange = model.password::setValue
)
}
}
binding.addMount.setOnClickListener {
validate()
}
addMenuProvider(object : MenuProvider {
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
menuInflater.inflate(R.menu.activity_add_webdav_mount, menu)
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.help -> {
onShowHelp()
true
}
else -> false
}
}
})
}
fun onShowHelp() {
UiUtils.launchUri(this,
App.homepageUrl(this).buildUpon().appendPath("tested-with").build())
@Composable
fun Layout(
isLoading: Boolean = false,
error: String? = null,
onErrorClearRequested: () -> Unit = {},
displayName: String = "",
onDisplayNameChange: (String) -> Unit = {},
displayNameError: String? = null,
url: String = "",
onUrlChange: (String) -> Unit = {},
urlError: String? = null,
username: String = "",
onUsernameChange: (String) -> Unit = {},
password: String = "",
onPasswordChange: (String) -> Unit = {}
) {
val context = LocalContext.current
val uriHandler = LocalUriHandler.current
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(error) {
if (error != null) {
snackbarHostState.showSnackbar(
message = error
)
onErrorClearRequested()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.webdav_add_mount_title)) },
actions = {
IconButton(
onClick = {
uriHandler.openUri(
App.homepageUrl(context)
.buildUpon()
.appendPath("tested-with")
.build()
.toString()
)
}
) {
Icon(Icons.AutoMirrored.Filled.Help, stringResource(R.string.help))
}
}
)
},
bottomBar = {
Card(
modifier = Modifier.fillMaxWidth(),
elevation = 4.dp
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End
) {
TextButton(
enabled = !isLoading,
onClick = ::validate
) {
Text(
text = stringResource(R.string.webdav_add_mount_add).uppercase()
)
}
}
}
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
if (isLoading)
LinearProgressIndicator(
color = MaterialTheme.colors.primary,
modifier = Modifier.fillMaxWidth()
)
Form(
displayName,
onDisplayNameChange,
displayNameError,
url,
onUrlChange,
urlError,
username,
onUsernameChange,
password,
onPasswordChange
)
}
}
}
@Composable
fun Form(
displayName: String,
onDisplayNameChange: (String) -> Unit,
displayNameError: String?,
url: String,
onUrlChange: (String) -> Unit,
urlError: String?,
username: String,
onUsernameChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
) {
FormField(
displayName,
onDisplayNameChange,
displayNameError,
R.string.webdav_add_mount_display_name
)
FormField(
url,
onUrlChange,
urlError,
R.string.webdav_add_mount_url
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.webdav_add_mount_authentication),
style = MaterialTheme.typography.body1,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp)
)
FormField(
username,
onUsernameChange,
null,
R.string.webdav_add_mount_username
)
PasswordTextField(
password = password,
onPasswordChange = onPasswordChange,
labelText = stringResource(R.string.webdav_add_mount_password)
)
}
}
@Composable
fun FormField(
value: String,
onValueChange: (String) -> Unit,
error: String?,
@StringRes label: Int
) {
OutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
label = { Text(stringResource(label)) },
singleLine = true,
isError = error != null
)
if (error != null) {
Text(
text = error,
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.caption.copy(
color = MaterialTheme.colors.error
)
)
}
}
@Preview
@Composable
fun Layout_Preview() {
MdcTheme {
Layout()
}
}
@ -134,8 +347,7 @@ class AddWebdavMountActivity: AppCompatActivity() {
null
if (ok && url != null) {
binding.progress.visibility = View.VISIBLE
binding.addMount.isEnabled = false
model.isLoading.postValue(true)
val mount = WebDavMount(
name = model.displayName.value ?: return,
@ -145,10 +357,7 @@ class AddWebdavMountActivity: AppCompatActivity() {
if (model.addMount(mount, credentials))
finish()
launch(Dispatchers.Main) {
binding.progress.visibility = View.INVISIBLE
binding.addMount.isEnabled = true
}
model.isLoading.postValue(false)
}
}
}
@ -156,9 +365,9 @@ class AddWebdavMountActivity: AppCompatActivity() {
@HiltViewModel
class Model @Inject constructor(
application: Application,
val context: Application,
val db: AppDatabase
) : AndroidViewModel(application) {
): ViewModel() {
val displayName = MutableLiveData<String>()
val displayNameError = MutableLiveData<String>()
@ -168,8 +377,7 @@ class AddWebdavMountActivity: AppCompatActivity() {
val password = MutableLiveData<String>()
val error = MutableLiveData<String>()
val context: Context get() = getApplication()
val isLoading = MutableLiveData(false)
@WorkerThread

View file

@ -28,9 +28,9 @@ import at.bitfire.davdroid.R
fun PasswordTextField(
password: String,
labelText: String,
enabled: Boolean,
isError: Boolean,
onPasswordChange: (String) -> Unit
onPasswordChange: (String) -> Unit,
enabled: Boolean = true,
isError: Boolean = false
) {
var passwordVisible by remember { mutableStateOf(false) }
OutlinedTextField(

View file

@ -1,126 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<variable
name="model"
type="at.bitfire.davdroid.ui.webdav.AddWebdavMountActivity.Model" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminate="true"
android:visibility="invisible"
style="@style/Widget.AppCompat.ProgressBar.Horizontal" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.webdav.WebdavMountsActivity"
android:orientation="vertical"
android:layout_margin="@dimen/activity_margin">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/display_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/webdav_add_mount_display_name"
app:error="@{model.displayNameError}">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@={model.displayName}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/webdav_add_mount_url"
app:error="@{model.urlError}">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textUri"
android:text="@={model.url}" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_marginBottom="8dp"
android:text="@string/webdav_add_mount_authentication" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/webdav_add_mount_username">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:text="@={model.userName}" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="@string/webdav_add_mount_password"
app:passwordToggleEnabled="true">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:text="@={model.password}" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<androidx.cardview.widget.CardView
style="@style/stepper_nav_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/add_mount"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:text="@string/webdav_add_mount_add" />
</androidx.cardview.widget.CardView>
</LinearLayout>
</layout>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/help"
android:icon="@drawable/ic_help"
android:title="@string/help"
app:showAsAction="always" />
</menu>