UI Feedback

This commit is contained in:
2026-09-09 00:28:53 +02:00
parent dde1042701
commit b179edf086
3 changed files with 48 additions and 17 deletions
@@ -3,6 +3,9 @@ package net.helcel.cowspent.android.main
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -69,13 +72,43 @@ fun EmptyProjectsState(onConfigureNextcloud: () -> Unit, onAddManually: () -> Un
}
}
/**
* A full-height, centred state that a pull to refresh can still act on.
*
* PullRefresh reads the gesture from a nested scroll source, and a plain Column offers none - so
* on an empty list, which is exactly when a refresh is wanted most, the pull never fires. A
* single-item LazyColumn has nowhere to scroll but does provide that source.
*/
@Composable
private fun RefreshableFullScreenState(content: @Composable ColumnScope.() -> Unit) {
LazyColumn(modifier = Modifier.fillMaxSize()) {
item {
Column(
modifier = Modifier.fillParentMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
content = content
)
}
}
}
@Composable
fun LoadingBillsState() {
RefreshableFullScreenState {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(R.string.error_loading),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
@Composable
fun EmptyMembersState() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
RefreshableFullScreenState {
Text(
text = stringResource(R.string.error_no_members).uppercase(),
style = MaterialTheme.typography.subtitle1,
@@ -94,11 +127,7 @@ fun EmptyMembersState() {
@Composable
fun EmptyBillsState() {
Column(
modifier = Modifier.fillMaxSize().padding(16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
RefreshableFullScreenState {
Text(
text = stringResource(R.string.error_no_bills).uppercase(),
style = MaterialTheme.typography.subtitle1,
@@ -203,11 +232,7 @@ fun SectionHeader(title: String) {
@Composable
fun EmptyState() {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
RefreshableFullScreenState {
Text(
text = stringResource(R.string.error_no_bills).uppercase(),
style = MaterialTheme.typography.subtitle1,
@@ -409,6 +409,10 @@ fun BillsListScreen(
when {
viewModel.showNoProjects -> EmptyProjectsState(onAccountSwitcherClick, onAddProjectClick)
viewModel.showNoMembers -> EmptyMembersState()
// A large project takes a long time on its first sync. Until that finishes there
// is nothing stored for it yet, and reporting that as "no bills" tells the user
// the project is empty when it is still downloading.
viewModel.isRefreshing && viewModel.bills.isEmpty() -> LoadingBillsState()
viewModel.showNoBills -> EmptyBillsState()
viewModel.bills.isEmpty() -> EmptyState()
else -> {
@@ -94,14 +94,16 @@ class BillsListViewActivity :
private val syncCallBack = object : ICallback {
override fun onFinish() {
mActionMode?.finish()
refreshLists()
viewModel.isRefreshing = false
refreshLists()
}
override fun onFinish(result: String, message: String) {}
override fun onScheduled() {
viewModel.isRefreshing = false
// Being queued is not being done. Clearing the indicator here stops the spinner while a
// large project is still downloading; synchronize() already releases it when nothing
// actually started.
}
}