From b179edf0864ee43847138822b3b9b3e95d9a0d15 Mon Sep 17 00:00:00 2001 From: soraefir Date: Wed, 9 Sep 2026 00:28:53 +0200 Subject: [PATCH] UI Feedback --- .../android/main/BillsListComponents.kt | 55 ++++++++++++++----- .../cowspent/android/main/BillsListScreen.kt | 4 ++ .../android/main/BillsListViewActivity.kt | 6 +- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt index 5935ecc..ecfb978 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt @@ -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, diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt index 51a5261..ba009ae 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt @@ -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 -> { diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt index 7d6f58a..20acdfc 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -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. } }