From 348a3022d6420a8ed4b821d9bdd07a17c4298607 Mon Sep 17 00:00:00 2001 From: Ray <154766448+damontecres@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:20:25 -0500 Subject: [PATCH] Fix cut off cards on grids with show details enabled (#963) ## Description Fixes the previous row of cards showing the bottom cut off when the "Show details" view option is enabled. ### Related issues Fixes #404 Fixes #877 Related to #882 ### Testing Emulator ## Screenshots ![grid_show_details Large](https://github.com/user-attachments/assets/3376275e-261e-4e5a-b874-18be7606a9bf) ## AI or LLM usage None --- .../ui/components/CollectionFolderGrid.kt | 20 ++- .../wholphin/ui/detail/CardGrid.kt | 166 +++++++++--------- 2 files changed, 104 insertions(+), 82 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt index c5dc912d..4a71404a 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/CollectionFolderGrid.kt @@ -6,7 +6,9 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.LocalBringIntoViewSpec import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,6 +35,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -84,6 +87,7 @@ import com.github.damontecres.wholphin.ui.setValueOnMain import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.util.FilterUtils +import com.github.damontecres.wholphin.ui.util.ScrollToTopBringIntoViewSpec import com.github.damontecres.wholphin.util.ApiRequestPager import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.ExceptionHandler @@ -735,6 +739,7 @@ fun CollectionFolderGrid( } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun CollectionFolderGridContent( preferences: UserPreferences, @@ -879,14 +884,16 @@ fun CollectionFolderGridContent( } } } + val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current + val density = LocalDensity.current AnimatedVisibility(viewOptions.showDetails) { HomePageHeader( item = focusedItem, modifier = Modifier .fillMaxWidth() - .height(140.dp) - .padding(16.dp), + .height(200.dp) + .padding(top = 48.dp, bottom = 32.dp, start = 8.dp), ) } when (val state = loadingState) { @@ -932,6 +939,15 @@ fun CollectionFolderGridContent( }, columns = viewOptions.columns, spacing = viewOptions.spacing.dp, + bringIntoViewSpec = + remember(viewOptions) { + val spacingPx = with(density) { viewOptions.spacing.dp.toPx() } + if (viewOptions.showDetails) { + ScrollToTopBringIntoViewSpec(spacingPx) + } else { + defaultBringIntoViewSpec + } + }, ) AnimatedVisibility(showViewOptions) { ViewOptionsDialog( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt index 8893246c..ac72a144 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CardGrid.kt @@ -4,6 +4,8 @@ import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup +import androidx.compose.foundation.gestures.BringIntoViewSpec +import androidx.compose.foundation.gestures.LocalBringIntoViewSpec import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement @@ -24,6 +26,7 @@ import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -112,6 +115,7 @@ fun CardGrid( }, columns: Int = 6, spacing: Dp = 16.dp, + bringIntoViewSpec: BringIntoViewSpec = LocalBringIntoViewSpec.current, ) { val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0)) @@ -269,100 +273,102 @@ fun CardGrid( Box( modifier = Modifier.weight(1f), ) { - LazyVerticalGrid( - columns = GridCells.Fixed(columns), - horizontalArrangement = Arrangement.spacedBy(spacing), - verticalArrangement = Arrangement.spacedBy(spacing), - state = gridState, - contentPadding = PaddingValues(vertical = 16.dp), - modifier = - Modifier - .fillMaxSize() - .focusGroup() - .focusRestorer(firstFocus) - .focusProperties { - onExit = { - // Leaving the grid, so "forget" the position + CompositionLocalProvider(LocalBringIntoViewSpec provides bringIntoViewSpec) { + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + horizontalArrangement = Arrangement.spacedBy(spacing), + verticalArrangement = Arrangement.spacedBy(spacing), + state = gridState, + contentPadding = PaddingValues(vertical = 16.dp), + modifier = + Modifier + .fillMaxSize() + .focusGroup() + .focusRestorer(firstFocus) + .focusProperties { + onExit = { + // Leaving the grid, so "forget" the position // focusedIndex = -1 - } - onEnter = { - if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { - focusedIndex = startPosition } - } - }, - ) { - items(pager.size) { index -> - val mod = - if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) { - if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index") - Modifier - .focusRequester(firstFocus) - .focusRequester(gridFocusRequester) - .focusRequester(alphabetFocusRequester) - } else { - Modifier - } - val item = pager[index] - cardContent( - item, - { - if (item != null) { - focusedIndex = index - onClickItem.invoke(index, item) - } - }, - { if (item != null) onLongClickItem.invoke(index, item) }, - mod - .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) - .onFocusChanged { focusState -> - if (DEBUG) { - Timber.v( - "$index isFocused=${focusState.isFocused}", - ) + onEnter = { + if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) { + focusedIndex = startPosition + } } - if (focusState.isFocused) { - // Focused, so set that up - focusOn(index) - positionCallback?.invoke(columns, index) - } else if (focusedIndex == index) { + }, + ) { + items(pager.size) { index -> + val mod = + if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) { + if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index") + Modifier + .focusRequester(firstFocus) + .focusRequester(gridFocusRequester) + .focusRequester(alphabetFocusRequester) + } else { + Modifier + } + val item = pager[index] + cardContent( + item, + { + if (item != null) { + focusedIndex = index + onClickItem.invoke(index, item) + } + }, + { if (item != null) onLongClickItem.invoke(index, item) }, + mod + .ifElse(index == 0, Modifier.focusRequester(zeroFocus)) + .onFocusChanged { focusState -> + if (DEBUG) { + Timber.v( + "$index isFocused=${focusState.isFocused}", + ) + } + if (focusState.isFocused) { + // Focused, so set that up + focusOn(index) + positionCallback?.invoke(columns, index) + } else if (focusedIndex == index) { // savedFocusedIndex = index // // Was focused on this, so mark unfocused // focusedIndex = -1 - } - }, - ) + } + }, + ) + } } - } - if (pager.isEmpty()) { + if (pager.isEmpty()) { // focusedIndex = -1 - Box(modifier = Modifier.fillMaxSize()) { - Text( - text = stringResource(R.string.no_results), - color = MaterialTheme.colorScheme.onBackground, - modifier = Modifier.align(Alignment.Center), - ) + Box(modifier = Modifier.fillMaxSize()) { + Text( + text = stringResource(R.string.no_results), + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.align(Alignment.Center), + ) + } } - } - if (showFooter) { - // Footer - Box( - modifier = - Modifier - .align(Alignment.BottomCenter) - .background(AppColors.TransparentBlack50), - ) { - val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?" + if (showFooter) { + // Footer + Box( + modifier = + Modifier + .align(Alignment.BottomCenter) + .background(AppColors.TransparentBlack50), + ) { + val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?" // if (focusedIndex >= 0) { // focusedIndex + 1 // } else { // max(savedFocusedIndex, focusedIndexOnExit) + 1 // } - Text( - modifier = Modifier.padding(4.dp), - color = MaterialTheme.colorScheme.onBackground, - text = "$index / ${pager.size}", - ) + Text( + modifier = Modifier.padding(4.dp), + color = MaterialTheme.colorScheme.onBackground, + text = "$index / ${pager.size}", + ) + } } } }