From 39af42a58ff918cac3e864151a36c1554bccd242 Mon Sep 17 00:00:00 2001 From: Ash Date: Thu, 1 Feb 2024 14:58:01 +0800 Subject: [PATCH] refactor(ui): improve add account dialog --- .../ui/component/base/RYOutlineTextField.kt | 22 +++++++++-- .../settings/accounts/AccountDetailsPage.kt | 13 +++++++ .../settings/accounts/AccountViewModel.kt | 23 ++++++++++- .../addition/AddFeverAccountDialog.kt | 38 ++++++++++++++---- .../addition/AddFreshRSSAccountDialog.kt | 39 +++++++++++++++---- .../addition/AddGoogleReaderAccountDialog.kt | 39 +++++++++++++++---- .../addition/AddLocalAccountDialog.kt | 38 ++++++++++++++---- 7 files changed, 178 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt index d92ac5eb6..06bff56d2 100644 --- a/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYOutlineTextField.kt @@ -7,8 +7,19 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.ContentPaste import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material3.ExperimentalMaterial3Api +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.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -23,6 +34,7 @@ import me.ash.reader.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun RYOutlineTextField( + requestFocus: Boolean = true, readOnly: Boolean = false, value: String, label: String = "", @@ -40,8 +52,10 @@ fun RYOutlineTextField( var showPassword by remember { mutableStateOf(false) } LaunchedEffect(Unit) { - delay(100) // ??? - focusRequester.requestFocus() + if (requestFocus) { + delay(100) // ??? + focusRequester.requestFocus() + } } OutlinedTextField( diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt index 9ce999488..6204b81e9 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountDetailsPage.kt @@ -17,6 +17,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.PersonOff import androidx.compose.material.icons.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Close import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -55,6 +56,7 @@ import me.ash.reader.ui.ext.getCurrentVersion import me.ash.reader.ui.ext.showToast import me.ash.reader.ui.ext.showToastLong import me.ash.reader.ui.ext.toString +import me.ash.reader.ui.page.common.RouteName import me.ash.reader.ui.page.settings.SettingItem import me.ash.reader.ui.page.settings.accounts.connection.AccountConnection import me.ash.reader.ui.theme.palette.onLight @@ -113,6 +115,17 @@ fun AccountDetailsPage( navController.popBackStack() } }, + actions = { + FeedbackIconButton( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(R.string.close), + tint = MaterialTheme.colorScheme.onSurface + ) { + navController.navigate(RouteName.FEEDS) { + launchSingleTop = true + } + } + }, content = { LazyColumn { item { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt index 7838ef29d..3f7e18eb4 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/AccountViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -38,6 +39,7 @@ class AccountViewModel @Inject constructor( private val _accountUiState = MutableStateFlow(AccountUiState()) val accountUiState: StateFlow = _accountUiState.asStateFlow() val accounts = accountService.getAccounts() + var addAccountJob: Job? = null fun initData(accountId: Int) { viewModelScope.launch(ioDispatcher) { @@ -98,7 +100,8 @@ class AccountViewModel @Inject constructor( } fun addAccount(account: Account, callback: (account: Account?, exception: Exception?) -> Unit) { - viewModelScope.launch(ioDispatcher) { + setLoading(true) + addAccountJob = viewModelScope.launch(ioDispatcher) { val addAccount = accountService.addAccount(account) try { if (rssService.get(addAccount.type.id).validCredentials(account)) { @@ -113,6 +116,8 @@ class AccountViewModel @Inject constructor( withContext(mainDispatcher) { callback(null, e) } + } finally { + setLoading(false) } } } @@ -135,6 +140,21 @@ class AccountViewModel @Inject constructor( } } } + + private fun setLoading(isLoading: Boolean) { + viewModelScope.launch { + _accountUiState.update { + it.copy( + isLoading = isLoading + ) + } + } + } + + fun cancelAdd() { + addAccountJob?.cancel() + setLoading(false) + } } data class AccountUiState( @@ -142,6 +162,7 @@ data class AccountUiState( val deleteDialogVisible: Boolean = false, val clearDialogVisible: Boolean = false, val exportOPMLMode: ExportOPMLMode = ExportOPMLMode.ATTACH_INFO, + val isLoading: Boolean = false, ) sealed class ExportOPMLMode { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt index c90afb625..0324a0085 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFeverAccountDialog.kt @@ -1,10 +1,16 @@ package me.ash.reader.ui.page.settings.accounts.addition -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +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.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -44,6 +50,7 @@ fun AddFeverAccountDialog( val context = LocalContext.current val focusManager = LocalFocusManager.current val uiState = viewModel.additionUiState.collectAsStateValue() + val accountUiState = accountViewModel.accountUiState.collectAsStateValue() var feverServerUrl by rememberSaveable { mutableStateOf("") } var feverUsername by rememberSaveable { mutableStateOf("") } @@ -55,14 +62,22 @@ fun AddFeverAccountDialog( properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddFeverAccountDialog() }, icon = { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(id = R.drawable.ic_fever), - contentDescription = stringResource(R.string.fever), - ) + if (accountUiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.onSurface, + ) + } else { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_fever), + contentDescription = stringResource(R.string.fever), + ) + } }, title = { Text( @@ -77,6 +92,7 @@ fun AddFeverAccountDialog( ) { Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + readOnly = accountUiState.isLoading, value = feverServerUrl, onValueChange = { feverServerUrl = it }, label = stringResource(R.string.server_url), @@ -85,6 +101,8 @@ fun AddFeverAccountDialog( ) Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + requestFocus = false, + readOnly = accountUiState.isLoading, value = feverUsername, onValueChange = { feverUsername = it }, label = stringResource(R.string.username), @@ -93,6 +111,8 @@ fun AddFeverAccountDialog( ) Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + requestFocus = false, + readOnly = accountUiState.isLoading, value = feverPassword, onValueChange = { feverPassword = it }, isPassword = true, @@ -105,7 +125,10 @@ fun AddFeverAccountDialog( }, confirmButton = { TextButton( - enabled = feverServerUrl.isNotBlank() && feverUsername.isNotEmpty() && feverPassword.isNotEmpty(), + enabled = !accountUiState.isLoading + && feverServerUrl.isNotBlank() + && feverUsername.isNotEmpty() + && feverPassword.isNotEmpty(), onClick = { focusManager.clearFocus() accountViewModel.addAccount(Account( @@ -136,6 +159,7 @@ fun AddFeverAccountDialog( TextButton( onClick = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddFeverAccountDialog() } ) { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt index 1cd459b5a..f830ae7f6 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddFreshRSSAccountDialog.kt @@ -1,10 +1,16 @@ package me.ash.reader.ui.page.settings.accounts.addition -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +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.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -44,6 +50,7 @@ fun AddFreshRSSAccountDialog( val context = LocalContext.current val focusManager = LocalFocusManager.current val uiState = viewModel.additionUiState.collectAsStateValue() + val accountUiState = accountViewModel.accountUiState.collectAsStateValue() var freshRSSServerUrl by rememberSaveable { mutableStateOf("") } var freshRSSUsername by rememberSaveable { mutableStateOf("") } @@ -55,14 +62,23 @@ fun AddFreshRSSAccountDialog( properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddFreshRSSAccountDialog() }, icon = { - Icon( - modifier = Modifier.size(24.dp), - painter = painterResource(id = R.drawable.ic_freshrss), - contentDescription = stringResource(R.string.fresh_rss), - ) + if (accountUiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp), + color = MaterialTheme.colorScheme.onSurface, + ) + } else { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(id = R.drawable.ic_freshrss), + contentDescription = stringResource(R.string.fresh_rss), + ) + } }, title = { Text( @@ -77,6 +93,7 @@ fun AddFreshRSSAccountDialog( ) { Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + readOnly = accountUiState.isLoading, value = freshRSSServerUrl, onValueChange = { freshRSSServerUrl = it }, label = stringResource(R.string.server_url), @@ -85,6 +102,8 @@ fun AddFreshRSSAccountDialog( ) Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + requestFocus = false, + readOnly = accountUiState.isLoading, value = freshRSSUsername, onValueChange = { freshRSSUsername = it }, label = stringResource(R.string.username), @@ -93,6 +112,8 @@ fun AddFreshRSSAccountDialog( ) Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + requestFocus = false, + readOnly = accountUiState.isLoading, value = freshRSSPassword, onValueChange = { freshRSSPassword = it }, isPassword = true, @@ -105,7 +126,10 @@ fun AddFreshRSSAccountDialog( }, confirmButton = { TextButton( - enabled = freshRSSServerUrl.isNotBlank() && freshRSSUsername.isNotEmpty() && freshRSSPassword.isNotEmpty(), + enabled = !accountUiState.isLoading + && freshRSSServerUrl.isNotBlank() + && freshRSSUsername.isNotEmpty() + && freshRSSPassword.isNotEmpty(), onClick = { focusManager.clearFocus() if (!freshRSSServerUrl.endsWith("/")) { @@ -138,6 +162,7 @@ fun AddFreshRSSAccountDialog( dismissButton = { TextButton( onClick = { + accountViewModel.cancelAdd() focusManager.clearFocus() viewModel.hideAddFreshRSSAccountDialog() } diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt index 4f0b841da..35fdea9c6 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddGoogleReaderAccountDialog.kt @@ -1,12 +1,18 @@ package me.ash.reader.ui.page.settings.accounts.addition -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +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.material.icons.Icons import androidx.compose.material.icons.rounded.RssFeed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -45,6 +51,7 @@ fun AddGoogleReaderAccountDialog( val context = LocalContext.current val focusManager = LocalFocusManager.current val uiState = viewModel.additionUiState.collectAsStateValue() + val accountUiState = accountViewModel.accountUiState.collectAsStateValue() var googleReaderServerUrl by rememberSaveable { mutableStateOf("") } var googleReaderUsername by rememberSaveable { mutableStateOf("") } @@ -56,14 +63,23 @@ fun AddGoogleReaderAccountDialog( properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddGoogleReaderAccountDialog() }, icon = { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.Rounded.RssFeed, - contentDescription = stringResource(R.string.google_reader), - ) + if (accountUiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp), + color = MaterialTheme.colorScheme.onSurface, + ) + } else { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Rounded.RssFeed, + contentDescription = stringResource(R.string.google_reader), + ) + } }, title = { Text( @@ -78,6 +94,7 @@ fun AddGoogleReaderAccountDialog( ) { Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + readOnly = accountUiState.isLoading, value = googleReaderServerUrl, onValueChange = { googleReaderServerUrl = it }, label = stringResource(R.string.server_url), @@ -86,6 +103,8 @@ fun AddGoogleReaderAccountDialog( ) Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + requestFocus = false, + readOnly = accountUiState.isLoading, value = googleReaderUsername, onValueChange = { googleReaderUsername = it }, label = stringResource(R.string.username), @@ -94,6 +113,8 @@ fun AddGoogleReaderAccountDialog( ) Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + requestFocus = false, + readOnly = accountUiState.isLoading, value = googleReaderPassword, onValueChange = { googleReaderPassword = it }, isPassword = true, @@ -106,7 +127,10 @@ fun AddGoogleReaderAccountDialog( }, confirmButton = { TextButton( - enabled = googleReaderServerUrl.isNotBlank() && googleReaderUsername.isNotEmpty() && googleReaderPassword.isNotEmpty(), + enabled = !accountUiState.isLoading + && googleReaderServerUrl.isNotBlank() + && googleReaderUsername.isNotEmpty() + && googleReaderPassword.isNotEmpty(), onClick = { focusManager.clearFocus() if (!googleReaderServerUrl.endsWith("/")) { @@ -140,6 +164,7 @@ fun AddGoogleReaderAccountDialog( TextButton( onClick = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddGoogleReaderAccountDialog() } ) { diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt index 3b5f82aa3..337938adb 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/accounts/addition/AddLocalAccountDialog.kt @@ -1,15 +1,25 @@ package me.ash.reader.ui.page.settings.accounts.addition -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +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.material.icons.Icons import androidx.compose.material.icons.rounded.RssFeed +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalFocusManager @@ -40,6 +50,7 @@ fun AddLocalAccountDialog( val context = LocalContext.current val focusManager = LocalFocusManager.current val uiState = viewModel.additionUiState.collectAsStateValue() + val accountUiState = accountViewModel.accountUiState.collectAsStateValue() var name by remember { mutableStateOf("") } @@ -49,14 +60,23 @@ fun AddLocalAccountDialog( properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddLocalAccountDialog() }, icon = { - Icon( - modifier = Modifier.size(24.dp), - imageVector = Icons.Rounded.RssFeed, - contentDescription = stringResource(R.string.local), - ) + if (accountUiState.isLoading) { + CircularProgressIndicator( + modifier = Modifier + .size(24.dp), + color = MaterialTheme.colorScheme.onSurface, + ) + } else { + Icon( + modifier = Modifier.size(24.dp), + imageVector = Icons.Rounded.RssFeed, + contentDescription = stringResource(R.string.local), + ) + } }, title = { Text( @@ -71,6 +91,7 @@ fun AddLocalAccountDialog( ) { Spacer(modifier = Modifier.height(10.dp)) RYOutlineTextField( + readOnly = accountUiState.isLoading, value = name, onValueChange = { name = it }, label = stringResource(R.string.name), @@ -81,7 +102,7 @@ fun AddLocalAccountDialog( }, confirmButton = { TextButton( - enabled = name.isNotBlank(), + enabled = !accountUiState.isLoading && name.isNotBlank(), onClick = { focusManager.clearFocus() accountViewModel.addAccount(Account( @@ -107,6 +128,7 @@ fun AddLocalAccountDialog( TextButton( onClick = { focusManager.clearFocus() + accountViewModel.cancelAdd() viewModel.hideAddLocalAccountDialog() } ) {