From 118c687847fca103dc6a8b13354605d961d59145 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 26 May 2026 19:47:54 -0400 Subject: [PATCH] fix(android): pull-to-refresh works on Empty/Error states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../minstrel/shared/widgets/EmptyState.kt | 68 +++++++++++-------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/EmptyState.kt b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/EmptyState.kt index aff0d686..26694390 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/EmptyState.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/EmptyState.kt @@ -1,10 +1,11 @@ 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.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,6 +26,12 @@ import com.composables.icons.lucide.Lucide * Per the FabledSword design system: sentence case copy, understated * tone. The icon defaults to Lucide's `Inbox`; callers pass their own * 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 fun EmptyState( @@ -33,32 +40,37 @@ fun EmptyState( modifier: Modifier = Modifier, icon: ImageVector = Lucide.Inbox, ) { - Column( - modifier = modifier - .fillMaxSize() - .padding(32.dp), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = title, - modifier = Modifier.padding(top = 16.dp), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - ) - Text( - text = body, - modifier = Modifier.padding(top = 8.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, - ) + LazyColumn(modifier = modifier.fillMaxSize()) { + item { + Box( + modifier = Modifier + .fillParentMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = title, + modifier = Modifier.padding(top = 16.dp), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + ) + Text( + text = body, + modifier = Modifier.padding(top = 8.dp), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } } }