fix(android): pull-to-refresh works on Empty/Error states

Audit v2 silent-breakage: PullToRefreshBox needs scrollable content
in its nested-scroll connection to detect the gesture. Empty/Error
branches used a plain centered Column → no nested-scroll
participation → swipe gesture silently ignored → user can't recover
from a cold-load error.

Fix: re-house EmptyState inside a single-item LazyColumn with
fillParentMaxSize. Visual is identical (centered icon + title +
body) but LazyColumn participates in nested-scroll dispatch so
PullToRefreshBox fires on swipe-down.

Covers Empty + Error on every PullToRefreshScaffold-wrapped screen
(Home, Library tabs, Album/Artist/Playlist detail, Discover,
Requests, Admin*). Loading-state pull-to-refresh remains broken on
screens using per-screen LoadingCentered helpers — that's a
transient state and lower priority; separate follow-up if it
becomes a real friction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 19:47:54 -04:00
parent 4d7a4312db
commit 118c687847
@@ -1,10 +1,11 @@
package com.fabledsword.minstrel.shared.widgets package com.fabledsword.minstrel.shared.widgets
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -25,6 +26,12 @@ import com.composables.icons.lucide.Lucide
* Per the FabledSword design system: sentence case copy, understated * Per the FabledSword design system: sentence case copy, understated
* tone. The icon defaults to Lucide's `Inbox`; callers pass their own * tone. The icon defaults to Lucide's `Inbox`; callers pass their own
* when a more topical icon helps. * when a more topical icon helps.
*
* Renders inside a single-item LazyColumn so the widget participates
* in nested-scroll dispatch when wrapped by PullToRefreshBox —
* pull-to-refresh fires on Empty / Error states without the content
* actually scrolling. The fillParentMaxSize on the inner Box keeps
* the centered visual identical to the pre-LazyColumn layout.
*/ */
@Composable @Composable
fun EmptyState( fun EmptyState(
@@ -33,32 +40,37 @@ fun EmptyState(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector = Lucide.Inbox, icon: ImageVector = Lucide.Inbox,
) { ) {
Column( LazyColumn(modifier = modifier.fillMaxSize()) {
modifier = modifier item {
.fillMaxSize() Box(
.padding(32.dp), modifier = Modifier
verticalArrangement = Arrangement.Center, .fillParentMaxSize()
horizontalAlignment = Alignment.CenterHorizontally, .padding(32.dp),
) { contentAlignment = Alignment.Center,
Icon( ) {
imageVector = icon, Column(horizontalAlignment = Alignment.CenterHorizontally) {
contentDescription = null, Icon(
modifier = Modifier.size(48.dp), imageVector = icon,
tint = MaterialTheme.colorScheme.onSurfaceVariant, contentDescription = null,
) modifier = Modifier.size(48.dp),
Text( tint = MaterialTheme.colorScheme.onSurfaceVariant,
text = title, )
modifier = Modifier.padding(top = 16.dp), Text(
style = MaterialTheme.typography.titleMedium, text = title,
color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(top = 16.dp),
textAlign = TextAlign.Center, style = MaterialTheme.typography.titleMedium,
) color = MaterialTheme.colorScheme.onSurface,
Text( textAlign = TextAlign.Center,
text = body, )
modifier = Modifier.padding(top = 8.dp), Text(
style = MaterialTheme.typography.bodyMedium, text = body,
color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(top = 8.dp),
textAlign = TextAlign.Center, style = MaterialTheme.typography.bodyMedium,
) color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
}
} }
} }