mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Preload grid card images & other minor optimizations (#1328)
## Description Preload ~6 rows worth of images on library grid pages. So depending on device CPU & memory, most cards do not show a placeholder image when scrolling unless you press and hold up/down to scroll as fast as possible. There's a couple other minor optimizations on the grid code as well, but nothing huge. ### Dev notes This PR makes the `cacheWindow` for pre-rendering cards applicable to their images. Previously, images would not be fetched until the card is laid out within the window. This was to get the width/height to pass to the server request. This PR instead calculates the card widths when the grid itself is laid out and then passes this width into the cards' composables. ### Related issues Related to #1246 ### Testing Tested with emulator, shield, fire stick 4k max ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
b94faceb2f
commit
d538b8d4f9
14 changed files with 167 additions and 125 deletions
|
|
@ -38,8 +38,8 @@ fun CoilConfig(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
okHttpClient = okHttpClient,
|
okHttpClient = okHttpClient,
|
||||||
debugLogging = false,
|
debugLogging = debugLogging,
|
||||||
enableCache = true,
|
enableCache = enableCache,
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -70,6 +71,8 @@ fun EpisodeCard(
|
||||||
val aspectRatio = item?.aspectRatio?.coerceAtLeast(AspectRatios.MIN) ?: AspectRatios.MIN
|
val aspectRatio = item?.aspectRatio?.coerceAtLeast(AspectRatios.MIN) ?: AspectRatios.MIN
|
||||||
val width = imageHeight * aspectRatio
|
val width = imageHeight * aspectRatio
|
||||||
val height = imageWidth * (1f / aspectRatio)
|
val height = imageWidth * (1f / aspectRatio)
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val imageWidthPx = remember(imageWidth) { with(density) { imageWidth.roundToPx() } }
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(spaceBetween),
|
verticalArrangement = Arrangement.spacedBy(spaceBetween),
|
||||||
modifier = modifier.size(width, height),
|
modifier = modifier.size(width, height),
|
||||||
|
|
@ -102,6 +105,7 @@ fun EpisodeCard(
|
||||||
watchedPercent = dto?.userData?.playedPercentage,
|
watchedPercent = dto?.userData?.playedPercentage,
|
||||||
numberOfVersions = dto?.mediaSourceCount ?: 0,
|
numberOfVersions = dto?.mediaSourceCount ?: 0,
|
||||||
useFallbackText = false,
|
useFallbackText = false,
|
||||||
|
fillWidth = imageWidthPx,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,8 @@ fun GridCard(
|
||||||
imageContentScale: ContentScale = ContentScale.Fit,
|
imageContentScale: ContentScale = ContentScale.Fit,
|
||||||
imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY,
|
imageType: ViewOptionImageType = ViewOptionImageType.PRIMARY,
|
||||||
showTitle: Boolean = true,
|
showTitle: Boolean = true,
|
||||||
|
fillWidth: Int? = null,
|
||||||
|
fillHeight: Int? = null,
|
||||||
) {
|
) {
|
||||||
val dto = item?.data
|
val dto = item?.data
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
|
@ -95,6 +97,8 @@ fun GridCard(
|
||||||
numberOfVersions = dto?.mediaSourceCount ?: 0,
|
numberOfVersions = dto?.mediaSourceCount ?: 0,
|
||||||
useFallbackText = false,
|
useFallbackText = false,
|
||||||
contentScale = imageContentScale,
|
contentScale = imageContentScale,
|
||||||
|
fillWidth = fillWidth,
|
||||||
|
fillHeight = fillHeight,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,9 @@ import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onLayoutRectChanged
|
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.IntSize
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -63,20 +61,19 @@ fun ItemCardImage(
|
||||||
imageType: ImageType = ImageType.PRIMARY,
|
imageType: ImageType = ImageType.PRIMARY,
|
||||||
useFallbackText: Boolean = true,
|
useFallbackText: Boolean = true,
|
||||||
contentScale: ContentScale = ContentScale.Fit,
|
contentScale: ContentScale = ContentScale.Fit,
|
||||||
|
fillWidth: Int? = null,
|
||||||
|
fillHeight: Int? = null,
|
||||||
) {
|
) {
|
||||||
val imageUrlService = LocalImageUrlService.current
|
val imageUrlService = LocalImageUrlService.current
|
||||||
var size by remember { mutableStateOf(IntSize.Zero) }
|
|
||||||
val imageUrl =
|
val imageUrl =
|
||||||
remember(size, item) {
|
remember(item) {
|
||||||
if (size != IntSize.Zero && item != null) {
|
item?.let {
|
||||||
imageUrlService.getItemImageUrl(
|
imageUrlService.getItemImageUrl(
|
||||||
item,
|
item,
|
||||||
imageType,
|
imageType,
|
||||||
fillWidth = size.width,
|
fillWidth = fillWidth,
|
||||||
fillHeight = size.height,
|
fillHeight = fillHeight,
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ItemCardImage(
|
ItemCardImage(
|
||||||
|
|
@ -88,13 +85,7 @@ fun ItemCardImage(
|
||||||
unwatchedCount = unwatchedCount,
|
unwatchedCount = unwatchedCount,
|
||||||
watchedPercent = watchedPercent,
|
watchedPercent = watchedPercent,
|
||||||
numberOfVersions = numberOfVersions,
|
numberOfVersions = numberOfVersions,
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier.onLayoutRectChanged(
|
|
||||||
throttleMillis = 100,
|
|
||||||
debounceMillis = 25,
|
|
||||||
) {
|
|
||||||
size = IntSize(width = it.width, height = it.height)
|
|
||||||
},
|
|
||||||
useFallbackText = useFallbackText,
|
useFallbackText = useFallbackText,
|
||||||
contentScale = contentScale,
|
contentScale = contentScale,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ import com.github.damontecres.wholphin.ui.data.ItemDetailsDialog
|
||||||
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
import com.github.damontecres.wholphin.ui.data.ItemDetailsDialogInfo
|
||||||
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
||||||
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
import com.github.damontecres.wholphin.ui.detail.CardGrid
|
||||||
|
import com.github.damontecres.wholphin.ui.detail.GridItemDetails
|
||||||
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
import com.github.damontecres.wholphin.ui.detail.ItemViewModel
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
import com.github.damontecres.wholphin.ui.detail.PlaylistDialog
|
||||||
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
import com.github.damontecres.wholphin.ui.detail.PlaylistLoadingState
|
||||||
|
|
@ -1060,7 +1061,7 @@ fun CollectionFolderGridContent(
|
||||||
position = newPosition
|
position = newPosition
|
||||||
positionCallback?.invoke(columns, newPosition)
|
positionCallback?.invoke(columns, newPosition)
|
||||||
},
|
},
|
||||||
cardContent = { item, onClick, onLongClick, mod ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
@ -1069,6 +1070,7 @@ fun CollectionFolderGridContent(
|
||||||
imageAspectRatio = viewOptions.aspectRatio.ratio,
|
imageAspectRatio = viewOptions.aspectRatio.ratio,
|
||||||
imageType = viewOptions.imageType,
|
imageType = viewOptions.imageType,
|
||||||
showTitle = viewOptions.showTitles,
|
showTitle = viewOptions.showTitles,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -1109,17 +1111,13 @@ data class PositionItem(
|
||||||
data class CollectionFolderGridParameters(
|
data class CollectionFolderGridParameters(
|
||||||
val columns: Int = 6,
|
val columns: Int = 6,
|
||||||
val spacing: Dp = 16.dp,
|
val spacing: Dp = 16.dp,
|
||||||
val cardContent: @Composable (
|
val cardContent: @Composable (GridItemDetails<BaseItem>) -> Unit = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
item: BaseItem?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
mod: Modifier,
|
|
||||||
) -> Unit = { item, onClick, onLongClick, mod ->
|
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
imageContentScale = ContentScale.FillBounds,
|
imageContentScale = ContentScale.FillBounds,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -1129,13 +1127,14 @@ data class CollectionFolderGridParameters(
|
||||||
CollectionFolderGridParameters(
|
CollectionFolderGridParameters(
|
||||||
columns = 6,
|
columns = 6,
|
||||||
spacing = 16.dp,
|
spacing = 16.dp,
|
||||||
cardContent = { item, onClick, onLongClick, mod ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
imageContentScale = ContentScale.FillBounds,
|
imageContentScale = ContentScale.FillBounds,
|
||||||
imageAspectRatio = AspectRatios.TALL,
|
imageAspectRatio = AspectRatios.TALL,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -1144,13 +1143,14 @@ data class CollectionFolderGridParameters(
|
||||||
CollectionFolderGridParameters(
|
CollectionFolderGridParameters(
|
||||||
columns = 4,
|
columns = 4,
|
||||||
spacing = 24.dp,
|
spacing = 24.dp,
|
||||||
cardContent = { item, onClick, onLongClick, mod ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
imageContentScale = ContentScale.Crop,
|
imageContentScale = ContentScale.Crop,
|
||||||
imageAspectRatio = AspectRatios.WIDE,
|
imageAspectRatio = AspectRatios.WIDE,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -1159,13 +1159,14 @@ data class CollectionFolderGridParameters(
|
||||||
CollectionFolderGridParameters(
|
CollectionFolderGridParameters(
|
||||||
columns = 6,
|
columns = 6,
|
||||||
spacing = 16.dp,
|
spacing = 16.dp,
|
||||||
cardContent = { item, onClick, onLongClick, mod ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
imageContentScale = ContentScale.FillBounds,
|
imageContentScale = ContentScale.FillBounds,
|
||||||
imageAspectRatio = AspectRatios.SQUARE,
|
imageAspectRatio = AspectRatios.SQUARE,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -309,7 +309,7 @@ fun GenreCardGrid(
|
||||||
},
|
},
|
||||||
columns = columns,
|
columns = columns,
|
||||||
spacing = spacing,
|
spacing = spacing,
|
||||||
cardContent = { item: Genre?, onClick: () -> Unit, onLongClick: () -> Unit, mod: Modifier ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GenreCard(
|
GenreCard(
|
||||||
genre = item,
|
genre = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
|
|
@ -133,11 +133,12 @@ fun ItemGrid(
|
||||||
showJumpButtons = false,
|
showJumpButtons = false,
|
||||||
showLetterButtons = false,
|
showLetterButtons = false,
|
||||||
spacing = 24.dp,
|
spacing = 24.dp,
|
||||||
cardContent = @Composable { item, onClick, onLongClick, mod ->
|
cardContent = @Composable { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
imageAspectRatio = AspectRatios.WIDE, // TODO
|
imageAspectRatio = AspectRatios.WIDE, // TODO
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -205,7 +205,7 @@ fun StudioCardGrid(
|
||||||
},
|
},
|
||||||
columns = columns,
|
columns = columns,
|
||||||
spacing = spacing,
|
spacing = spacing,
|
||||||
cardContent = { item: Studio?, onClick: () -> Unit, onLongClick: () -> Unit, mod: Modifier ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
StudioCard(
|
StudioCard(
|
||||||
studio = item,
|
studio = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -34,6 +35,7 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -51,7 +53,8 @@ import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
import androidx.compose.ui.input.key.onKeyEvent
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.onLayoutRectChanged
|
||||||
|
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.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
|
@ -62,10 +65,8 @@ import androidx.tv.material3.LocalContentColor
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
|
||||||
import com.github.damontecres.wholphin.ui.AppColors
|
import com.github.damontecres.wholphin.ui.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.FontAwesome
|
import com.github.damontecres.wholphin.ui.FontAwesome
|
||||||
import com.github.damontecres.wholphin.ui.cards.GridCard
|
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
import com.github.damontecres.wholphin.ui.playback.isBackwardButton
|
import com.github.damontecres.wholphin.ui.playback.isBackwardButton
|
||||||
import com.github.damontecres.wholphin.ui.playback.isForwardButton
|
import com.github.damontecres.wholphin.ui.playback.isForwardButton
|
||||||
|
|
@ -76,6 +77,7 @@ import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
private const val DEBUG = false
|
private const val DEBUG = false
|
||||||
|
|
||||||
|
|
@ -85,6 +87,15 @@ interface CardGridItem {
|
||||||
val sortName: String
|
val sortName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class GridItemDetails<T : CardGridItem>(
|
||||||
|
val item: T?,
|
||||||
|
val index: Int,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val onLongClick: () -> Unit,
|
||||||
|
val widthPx: Int,
|
||||||
|
val mod: Modifier,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a vertical grid of [CardGridItem]s
|
* Shows a vertical grid of [CardGridItem]s
|
||||||
*/
|
*/
|
||||||
|
|
@ -102,31 +113,21 @@ fun <T : CardGridItem> CardGrid(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
initialPosition: Int = 0,
|
initialPosition: Int = 0,
|
||||||
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
positionCallback: ((columns: Int, position: Int) -> Unit)? = null,
|
||||||
cardContent: @Composable (
|
cardContent: @Composable (GridItemDetails<T>) -> Unit,
|
||||||
item: T?,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
onLongClick: () -> Unit,
|
|
||||||
mod: Modifier,
|
|
||||||
) -> Unit = { item, onClick, onLongClick, mod ->
|
|
||||||
GridCard(
|
|
||||||
item = item as BaseItem?,
|
|
||||||
onClick = onClick,
|
|
||||||
onLongClick = onLongClick,
|
|
||||||
imageContentScale = ContentScale.FillBounds,
|
|
||||||
modifier = mod,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
columns: Int = 6,
|
columns: Int = 6,
|
||||||
spacing: Dp = 16.dp,
|
spacing: Dp = 16.dp,
|
||||||
bringIntoViewSpec: BringIntoViewSpec = LocalBringIntoViewSpec.current,
|
bringIntoViewSpec: BringIntoViewSpec = LocalBringIntoViewSpec.current,
|
||||||
) {
|
) {
|
||||||
val startPosition = initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
|
val startPosition =
|
||||||
|
remember(initialPosition, pager.size) {
|
||||||
|
initialPosition.coerceIn(0, (pager.size - 1).coerceAtLeast(0))
|
||||||
|
}
|
||||||
|
|
||||||
val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f)
|
|
||||||
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
|
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
|
||||||
|
val currentFocusedIndex by rememberUpdatedState(focusedIndex)
|
||||||
val gridState =
|
val gridState =
|
||||||
rememberLazyGridState(
|
rememberLazyGridState(
|
||||||
cacheWindow = fractionCacheWindow,
|
cacheWindow = LazyLayoutCacheWindow(aheadFraction = 2f, behindFraction = 0.5f),
|
||||||
initialFirstVisibleItemIndex = focusedIndex,
|
initialFirstVisibleItemIndex = focusedIndex,
|
||||||
)
|
)
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -135,13 +136,16 @@ fun <T : CardGridItem> CardGrid(
|
||||||
var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) }
|
var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
|
||||||
var alphabetFocus by remember { mutableStateOf(false) }
|
var alphabetFocus by remember { mutableStateOf(false) }
|
||||||
val focusOn = { index: Int ->
|
val focusOn =
|
||||||
if (DEBUG) Timber.v("focusOn: focusedIndex=$focusedIndex, index=$index")
|
remember {
|
||||||
if (index != focusedIndex) {
|
{ index: Int ->
|
||||||
|
if (DEBUG) Timber.v("focusOn: focusedIndex=$currentFocusedIndex, index=$index")
|
||||||
|
if (index != currentFocusedIndex) {
|
||||||
previouslyFocusedIndex = focusedIndex
|
previouslyFocusedIndex = focusedIndex
|
||||||
}
|
}
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Wait for a recomposition to focus
|
// Wait for a recomposition to focus
|
||||||
val alphabetFocusRequester = remember { FocusRequester() }
|
val alphabetFocusRequester = remember { FocusRequester() }
|
||||||
|
|
@ -180,7 +184,9 @@ fun <T : CardGridItem> CardGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val jump = { jump: Int ->
|
val jump =
|
||||||
|
remember {
|
||||||
|
{ jump: Int ->
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
val newPosition =
|
val newPosition =
|
||||||
(gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size)
|
(gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size)
|
||||||
|
|
@ -189,9 +195,12 @@ fun <T : CardGridItem> CardGrid(
|
||||||
gridState.scrollToItem(newPosition, 0)
|
gridState.scrollToItem(newPosition, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val jumpToTop = {
|
}
|
||||||
|
val jumpToTop =
|
||||||
|
remember {
|
||||||
|
{
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
if (focusedIndex < (columns * 6)) {
|
if (currentFocusedIndex < (columns * 6)) {
|
||||||
// If close, animate the scroll
|
// If close, animate the scroll
|
||||||
gridState.animateScrollToItem(0, 0)
|
gridState.animateScrollToItem(0, 0)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -201,6 +210,7 @@ fun <T : CardGridItem> CardGrid(
|
||||||
zeroFocus.tryRequestFocus()
|
zeroFocus.tryRequestFocus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val jumpToLetter: (Char) -> Unit =
|
val jumpToLetter: (Char) -> Unit =
|
||||||
remember {
|
remember {
|
||||||
|
|
@ -298,6 +308,9 @@ fun <T : CardGridItem> CardGrid(
|
||||||
modifier = Modifier.align(Alignment.CenterVertically),
|
modifier = Modifier.align(Alignment.CenterVertically),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var cardWidthPx by rememberSaveable { mutableIntStateOf(0) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
) {
|
) {
|
||||||
|
|
@ -314,20 +327,26 @@ fun <T : CardGridItem> CardGrid(
|
||||||
.focusGroup()
|
.focusGroup()
|
||||||
.focusRestorer(firstFocus)
|
.focusRestorer(firstFocus)
|
||||||
.focusProperties {
|
.focusProperties {
|
||||||
onExit = {
|
|
||||||
// Leaving the grid, so "forget" the position
|
|
||||||
// focusedIndex = -1
|
|
||||||
}
|
|
||||||
onEnter = {
|
onEnter = {
|
||||||
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
|
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
|
||||||
focusedIndex = startPosition
|
focusedIndex = startPosition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.onLayoutRectChanged(0, 0) {
|
||||||
|
val width = it.width
|
||||||
|
val spacingPx = with(density) { spacing.toPx() }
|
||||||
|
val cardWidth =
|
||||||
|
ceil((width - (spacingPx * (columns - 1))) / columns)
|
||||||
|
cardWidthPx = cardWidth.toInt()
|
||||||
|
Timber.v("cardWidthPx=%s", cardWidthPx)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
items(pager.size) { index ->
|
items(pager.size) { index ->
|
||||||
|
val item = pager[index]
|
||||||
|
val details =
|
||||||
|
remember(index, item) {
|
||||||
val mod =
|
val mod =
|
||||||
if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
|
if ((index == currentFocusedIndex) or (currentFocusedIndex < 0 && index == 0)) {
|
||||||
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
|
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
|
||||||
Modifier
|
Modifier
|
||||||
.focusRequester(firstFocus)
|
.focusRequester(firstFocus)
|
||||||
|
|
@ -336,19 +355,27 @@ fun <T : CardGridItem> CardGrid(
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
val item = pager[index]
|
GridItemDetails(
|
||||||
cardContent(
|
item = item,
|
||||||
item,
|
index = index,
|
||||||
{
|
onClick = {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
focusedIndex = index
|
focusedIndex = index
|
||||||
onClickItem.invoke(index, item)
|
onClickItem.invoke(index, item)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ if (item != null) onLongClickItem.invoke(index, item) },
|
onLongClick = {
|
||||||
|
if (item != null) {
|
||||||
|
onLongClickItem.invoke(index, item)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
widthPx = cardWidthPx,
|
||||||
|
mod =
|
||||||
mod
|
mod
|
||||||
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
|
.ifElse(
|
||||||
.onFocusChanged { focusState ->
|
index == 0,
|
||||||
|
Modifier.focusRequester(zeroFocus),
|
||||||
|
).onFocusChanged { focusState ->
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
Timber.v(
|
Timber.v(
|
||||||
"$index isFocused=${focusState.isFocused}",
|
"$index isFocused=${focusState.isFocused}",
|
||||||
|
|
@ -358,14 +385,12 @@ fun <T : CardGridItem> CardGrid(
|
||||||
// Focused, so set that up
|
// Focused, so set that up
|
||||||
focusOn(index)
|
focusOn(index)
|
||||||
positionCallback?.invoke(columns, index)
|
positionCallback?.invoke(columns, index)
|
||||||
} else if (focusedIndex == index) {
|
|
||||||
// savedFocusedIndex = index
|
|
||||||
// // Was focused on this, so mark unfocused
|
|
||||||
// focusedIndex = -1
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
cardContent.invoke(details)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (pager.isEmpty()) {
|
if (pager.isEmpty()) {
|
||||||
// focusedIndex = -1
|
// focusedIndex = -1
|
||||||
|
|
@ -429,11 +454,21 @@ fun <T : CardGridItem> CardGrid(
|
||||||
// Add end padding to push away from edge
|
// Add end padding to push away from edge
|
||||||
letterClicked = jumpToLetter,
|
letterClicked = jumpToLetter,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
// Spacer ensures card sizes do not change if the alphabet buttons are not shown
|
||||||
|
Spacer(
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
.width(letterHorizontalPadding * 2 + letterButtonSize),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val letterHorizontalPadding = 2.dp
|
||||||
|
private val letterButtonSize = 14.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun JumpButtons(
|
fun JumpButtons(
|
||||||
jump1: Int,
|
jump1: Int,
|
||||||
|
|
@ -500,7 +535,7 @@ fun AlphabetButtons(
|
||||||
var alphabetPickerFocused by remember { mutableStateOf(false) }
|
var alphabetPickerFocused by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
contentPadding = PaddingValues(vertical = 1.1.dp, horizontal = 2.dp),
|
contentPadding = PaddingValues(vertical = 1.1.dp, horizontal = letterHorizontalPadding),
|
||||||
verticalArrangement = Arrangement.spacedBy(1.1.dp),
|
verticalArrangement = Arrangement.spacedBy(1.1.dp),
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
|
|
@ -534,14 +569,14 @@ fun AlphabetButtons(
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(14.dp)
|
.size(letterButtonSize)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.alpha(itemAlpha),
|
.alpha(itemAlpha),
|
||||||
) {
|
) {
|
||||||
Button(
|
Button(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.size(14.dp)
|
.size(letterButtonSize)
|
||||||
.focusRequester(focusRequesters[index]),
|
.focusRequester(focusRequesters[index]),
|
||||||
contentPadding = PaddingValues(0.dp), // No padding to maximize text space
|
contentPadding = PaddingValues(0.dp), // No padding to maximize text space
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
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.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -781,6 +782,9 @@ fun PlaylistItem(
|
||||||
isQueued: Boolean = false,
|
isQueued: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
|
val imageWidth = 160.dp
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val imageWidthPx = remember(imageWidth) { with(density) { imageWidth.roundToPx() } }
|
||||||
ListItem(
|
ListItem(
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
@ -843,8 +847,9 @@ fun PlaylistItem(
|
||||||
unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1,
|
unwatchedCount = item?.data?.userData?.unplayedItemCount ?: -1,
|
||||||
watchedPercent = 0.0,
|
watchedPercent = 0.0,
|
||||||
numberOfVersions = item?.data?.mediaSourceCount ?: 0,
|
numberOfVersions = item?.data?.mediaSourceCount ?: 0,
|
||||||
modifier = Modifier.width(160.dp),
|
modifier = Modifier.width(imageWidth),
|
||||||
useFallbackText = false,
|
useFallbackText = false,
|
||||||
|
fillWidth = imageWidthPx,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ fun CollectionMixedGrid(
|
||||||
positionCallback = { _, newPosition ->
|
positionCallback = { _, newPosition ->
|
||||||
onFocusPosition.invoke(RowColumn(0, newPosition))
|
onFocusPosition.invoke(RowColumn(0, newPosition))
|
||||||
},
|
},
|
||||||
cardContent = { item, onClick, onLongClick, mod ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
GridCard(
|
GridCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
@ -69,6 +69,7 @@ fun CollectionMixedGrid(
|
||||||
imageAspectRatio = cardViewOptions.aspectRatio.ratio,
|
imageAspectRatio = cardViewOptions.aspectRatio.ratio,
|
||||||
imageType = cardViewOptions.imageType,
|
imageType = cardViewOptions.imageType,
|
||||||
showTitle = cardViewOptions.showTitles,
|
showTitle = cardViewOptions.showTitles,
|
||||||
|
fillWidth = widthPx,
|
||||||
modifier = mod,
|
modifier = mod,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ fun DiscoverPersonPage(
|
||||||
showJumpButtons = false,
|
showJumpButtons = false,
|
||||||
showLetterButtons = false,
|
showLetterButtons = false,
|
||||||
spacing = 16.dp,
|
spacing = 16.dp,
|
||||||
cardContent = @Composable { item, onClick, onLongClick, mod ->
|
cardContent = @Composable { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
DiscoverItemCard(
|
DiscoverItemCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
|
|
@ -182,7 +182,7 @@ fun DiscoverRequestGrid(
|
||||||
gridFocusRequester = gridFocusRequester,
|
gridFocusRequester = gridFocusRequester,
|
||||||
showJumpButtons = false,
|
showJumpButtons = false,
|
||||||
showLetterButtons = false,
|
showLetterButtons = false,
|
||||||
cardContent = { item, onClick, onLongClick, mod ->
|
cardContent = { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
DiscoverItemCard(
|
DiscoverItemCard(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@ fun SeerrRequestsPage(
|
||||||
showJumpButtons = false,
|
showJumpButtons = false,
|
||||||
showLetterButtons = false,
|
showLetterButtons = false,
|
||||||
spacing = 16.dp,
|
spacing = 16.dp,
|
||||||
cardContent = @Composable { item, onClick, onLongClick, mod ->
|
cardContent = @Composable { (item, index, onClick, onLongClick, widthPx, mod) ->
|
||||||
DiscoverItemCard(
|
DiscoverItemCard(
|
||||||
item = item?.item,
|
item = item?.item,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue