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
This commit is contained in:
Ray 2026-02-23 15:20:25 -05:00 committed by GitHub
parent 3b162a2f98
commit 348a3022d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 104 additions and 82 deletions

View file

@ -6,7 +6,9 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.LocalBringIntoViewSpec
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.focus.focusRequester
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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.toServerString
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.ui.util.FilterUtils 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.ApiRequestPager
import com.github.damontecres.wholphin.util.DataLoadingState import com.github.damontecres.wholphin.util.DataLoadingState
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -735,6 +739,7 @@ fun CollectionFolderGrid(
} }
} }
@OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun CollectionFolderGridContent( fun CollectionFolderGridContent(
preferences: UserPreferences, preferences: UserPreferences,
@ -879,14 +884,16 @@ fun CollectionFolderGridContent(
} }
} }
} }
val defaultBringIntoViewSpec = LocalBringIntoViewSpec.current
val density = LocalDensity.current
AnimatedVisibility(viewOptions.showDetails) { AnimatedVisibility(viewOptions.showDetails) {
HomePageHeader( HomePageHeader(
item = focusedItem, item = focusedItem,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.height(140.dp) .height(200.dp)
.padding(16.dp), .padding(top = 48.dp, bottom = 32.dp, start = 8.dp),
) )
} }
when (val state = loadingState) { when (val state = loadingState) {
@ -932,6 +939,15 @@ fun CollectionFolderGridContent(
}, },
columns = viewOptions.columns, columns = viewOptions.columns,
spacing = viewOptions.spacing.dp, spacing = viewOptions.spacing.dp,
bringIntoViewSpec =
remember(viewOptions) {
val spacingPx = with(density) { viewOptions.spacing.dp.toPx() }
if (viewOptions.showDetails) {
ScrollToTopBringIntoViewSpec(spacingPx)
} else {
defaultBringIntoViewSpec
}
},
) )
AnimatedVisibility(showViewOptions) { AnimatedVisibility(showViewOptions) {
ViewOptionsDialog( ViewOptionsDialog(

View file

@ -4,6 +4,8 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup 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.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement 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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -112,6 +115,7 @@ fun <T : CardGridItem> CardGrid(
}, },
columns: Int = 6, columns: Int = 6,
spacing: Dp = 16.dp, spacing: Dp = 16.dp,
bringIntoViewSpec: BringIntoViewSpec = LocalBringIntoViewSpec.current,
) { ) {
val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0)) val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
@ -269,100 +273,102 @@ fun <T : CardGridItem> CardGrid(
Box( Box(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) { ) {
LazyVerticalGrid( CompositionLocalProvider(LocalBringIntoViewSpec provides bringIntoViewSpec) {
columns = GridCells.Fixed(columns), LazyVerticalGrid(
horizontalArrangement = Arrangement.spacedBy(spacing), columns = GridCells.Fixed(columns),
verticalArrangement = Arrangement.spacedBy(spacing), horizontalArrangement = Arrangement.spacedBy(spacing),
state = gridState, verticalArrangement = Arrangement.spacedBy(spacing),
contentPadding = PaddingValues(vertical = 16.dp), state = gridState,
modifier = contentPadding = PaddingValues(vertical = 16.dp),
Modifier modifier =
.fillMaxSize() Modifier
.focusGroup() .fillMaxSize()
.focusRestorer(firstFocus) .focusGroup()
.focusProperties { .focusRestorer(firstFocus)
onExit = { .focusProperties {
// Leaving the grid, so "forget" the position onExit = {
// Leaving the grid, so "forget" the position
// focusedIndex = -1 // focusedIndex = -1
}
onEnter = {
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
focusedIndex = startPosition
} }
} 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}",
)
} }
if (focusState.isFocused) { },
// Focused, so set that up ) {
focusOn(index) items(pager.size) { index ->
positionCallback?.invoke(columns, index) val mod =
} else if (focusedIndex == index) { 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 // savedFocusedIndex = index
// // Was focused on this, so mark unfocused // // Was focused on this, so mark unfocused
// focusedIndex = -1 // focusedIndex = -1
} }
}, },
) )
}
} }
} if (pager.isEmpty()) {
if (pager.isEmpty()) {
// focusedIndex = -1 // focusedIndex = -1
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Text( Text(
text = stringResource(R.string.no_results), text = stringResource(R.string.no_results),
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.align(Alignment.Center), modifier = Modifier.align(Alignment.Center),
) )
}
} }
} if (showFooter) {
if (showFooter) { // Footer
// Footer Box(
Box( modifier =
modifier = Modifier
Modifier .align(Alignment.BottomCenter)
.align(Alignment.BottomCenter) .background(AppColors.TransparentBlack50),
.background(AppColors.TransparentBlack50), ) {
) { val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?"
val index = (focusedIndex + 1).takeIf { it > 0 } ?: "?"
// if (focusedIndex >= 0) { // if (focusedIndex >= 0) {
// focusedIndex + 1 // focusedIndex + 1
// } else { // } else {
// max(savedFocusedIndex, focusedIndexOnExit) + 1 // max(savedFocusedIndex, focusedIndexOnExit) + 1
// } // }
Text( Text(
modifier = Modifier.padding(4.dp), modifier = Modifier.padding(4.dp),
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
text = "$index / ${pager.size}", text = "$index / ${pager.size}",
) )
}
} }
} }
} }