Simplify grid focus, handle items w/ missing images

This commit is contained in:
Damontecres 2025-10-08 15:49:31 -04:00
parent 2212e32c53
commit b63e933386
No known key found for this signature in database
13 changed files with 352 additions and 195 deletions

View file

@ -103,6 +103,7 @@ android {
buildTypes {
release {
isMinifyEnabled = false
signingConfig = signingConfigs.getByName("debug")
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",

View file

@ -90,6 +90,9 @@ data class BaseItem(
} else {
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
}
} else if (dto.imageTags == null || dto.imageTags!![ImageType.PRIMARY] == null) {
// TODO is this a bad assumption?
null
} else {
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY)
}

View file

@ -18,7 +18,7 @@ sealed class AppColors private constructor() {
}
}
const val DEFAULT_PAGE_SIZE = 50
const val DEFAULT_PAGE_SIZE = 100
val DefaultItemFields =
listOf(

View file

@ -13,11 +13,16 @@ import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
@ -30,6 +35,7 @@ import com.github.damontecres.dolphin.ui.isNotNullOrBlank
@Composable
fun BannerCard(
name: String?,
imageUrl: String?,
onClick: () -> Unit,
onLongClick: () -> Unit,
@ -41,19 +47,39 @@ fun BannerCard(
aspectRatio: Float = 16f / 9,
interactionSource: MutableInteractionSource? = null,
) {
var imageError by remember { mutableStateOf(false) }
Card(
modifier = modifier.size(cardHeight * aspectRatio, cardHeight),
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
) {
Box(modifier = Modifier.fillMaxSize()) {
Box(
modifier =
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceVariant),
) {
if (!imageError && imageUrl.isNotNullOrBlank()) {
AsyncImage(
model = imageUrl,
contentDescription = null,
contentScale = ContentScale.Fit,
onError = { imageError = true },
modifier = Modifier.fillMaxSize(),
)
} else {
Text(
text = name ?: "",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier =
Modifier
.padding(16.dp)
.align(Alignment.Center),
)
}
if (played || cornerText.isNotNullOrBlank()) {
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),

View file

@ -0,0 +1,117 @@
package com.github.damontecres.dolphin.ui.cards
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.tv.material3.Card
import androidx.tv.material3.CardDefaults
import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.enableMarquee
import kotlinx.coroutines.delay
@Composable
fun GridCard(
item: BaseItem?,
onClick: () -> Unit,
onLongClick: () -> Unit,
modifier: Modifier = Modifier,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
) {
val dto = item?.data
val focused by interactionSource.collectIsFocusedAsState()
val spaceBetween by animateDpAsState(if (focused) 12.dp else 4.dp)
val spaceBelow by animateDpAsState(if (focused) 4.dp else 12.dp)
var focusedAfterDelay by remember { mutableStateOf(false) }
val hideOverlayDelay = 500L
if (focused) {
LaunchedEffect(Unit) {
delay(hideOverlayDelay)
if (focused) {
focusedAfterDelay = true
} else {
focusedAfterDelay = false
}
}
} else {
focusedAfterDelay = false
}
Column(
verticalArrangement = Arrangement.spacedBy(spaceBetween),
modifier = modifier,
) {
Card(
modifier = Modifier.fillMaxWidth(),
onClick = onClick,
onLongClick = onLongClick,
interactionSource = interactionSource,
colors =
CardDefaults.colors(
containerColor = Color.Transparent,
),
) {
ItemCardImage(
imageUrl = item?.imageUrl,
name = item?.name,
// showOverlay = !focusedAfterDelay,
showOverlay = true,
favorite = dto?.userData?.isFavorite ?: false,
watched = dto?.userData?.played ?: false,
unwatchedCount = dto?.userData?.unplayedItemCount ?: -1,
watchedPercent = dto?.userData?.playedPercentage,
useFallbackText = false,
modifier =
Modifier
.fillMaxWidth()
.aspectRatio(2f / 3f) // TODO
.background(MaterialTheme.colorScheme.surfaceVariant),
)
}
Column(
verticalArrangement = Arrangement.spacedBy(0.dp),
modifier = Modifier.padding(bottom = spaceBelow).fillMaxWidth(),
) {
Text(
text = item?.name ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
Text(
text = item?.data?.productionYear?.toString() ?: "",
maxLines = 1,
textAlign = TextAlign.Center,
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp)
.enableMarquee(focusedAfterDelay),
)
}
}
}

View file

@ -3,6 +3,7 @@ package com.github.damontecres.dolphin.ui.cards
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState
@ -27,10 +28,13 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
@ -47,6 +51,7 @@ import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.enableMarquee
import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.util.seasonEpisode
import kotlinx.coroutines.delay
import org.jellyfin.sdk.model.api.BaseItemKind
@ -161,8 +166,11 @@ fun ItemCardImage(
unwatchedCount: Int,
watchedPercent: Double?,
modifier: Modifier = Modifier,
useFallbackText: Boolean = true,
) {
var imageError by remember { mutableStateOf(false) }
Box(modifier = modifier) {
if (!imageError && imageUrl.isNotNullOrBlank()) {
AsyncImage(
model = imageUrl,
contentDescription = name,
@ -173,12 +181,49 @@ fun ItemCardImage(
fallback = null,
onError = {
Timber.e(it.result.throwable, "Error loading image: $imageUrl")
imageError = true
},
modifier =
Modifier
.fillMaxSize()
.align(Alignment.TopCenter),
)
} else {
Box(
modifier =
Modifier
.background(MaterialTheme.colorScheme.surfaceVariant)
.fillMaxSize()
.align(Alignment.TopCenter),
) {
if (useFallbackText && name.isNotNullOrBlank()) {
Text(
text = name,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 14.sp,
modifier =
Modifier
.padding(8.dp)
.align(Alignment.Center),
)
} else {
Image(
painter = painterResource(id = R.drawable.video_solid),
contentDescription = null,
contentScale = ContentScale.Fit,
colorFilter =
ColorFilter.tint(
MaterialTheme.colorScheme.onSurfaceVariant,
BlendMode.SrcIn,
),
modifier =
Modifier
.fillMaxSize(.4f)
.align(Alignment.Center),
)
}
}
}
AnimatedVisibility(
visible = showOverlay,
enter = fadeIn(),

View file

@ -116,6 +116,7 @@ fun BannerItemRow(
modifier = modifier,
cardContent = { index, item, modifier, onClick, onLongClick ->
BannerCard(
name = title,
imageUrl = item?.imageUrl,
aspectRatio =
aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9),

View file

@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@ -44,7 +43,7 @@ import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.ui.cards.ItemCard
import com.github.damontecres.dolphin.ui.cards.GridCard
import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.playback.isBackwardButton
import com.github.damontecres.dolphin.ui.playback.isForwardButton
@ -53,7 +52,6 @@ import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.math.max
private const val DEBUG = false
@ -63,7 +61,6 @@ fun CardGrid(
itemOnClick: (BaseItem) -> Unit,
longClicker: (BaseItem) -> Unit,
letterPosition: suspend (Char) -> Int,
requestFocus: Boolean,
gridFocusRequester: FocusRequester,
showJumpButtons: Boolean,
modifier: Modifier = Modifier,
@ -79,45 +76,6 @@ fun CardGrid(
val zeroFocus = remember { FocusRequester() }
var previouslyFocusedIndex by rememberSaveable { mutableIntStateOf(0) }
var focusedIndex by rememberSaveable { mutableIntStateOf(initialPosition) }
var focusedIndexOnExit by rememberSaveable { mutableIntStateOf(-1) }
// Tracks whether the very first requestFocus has run, if the caller isn't requesting focus,
// then the first time will never run
var hasRequestFocusRun by rememberSaveable { mutableStateOf(!requestFocus) }
var savedFocusedIndex by rememberSaveable { mutableIntStateOf(-1) }
if (DEBUG) {
Timber.d(
"Grid: hasRun=$hasRequestFocusRun, requestFocus=$requestFocus, initialPosition=$initialPosition, focusedIndex=$focusedIndex",
)
}
LaunchedEffect(Unit) {
if (!hasRequestFocusRun) {
// On very first composition, if parent wants to focus on the grid, scroll to the item
if (requestFocus && initialPosition >= 0) {
if (DEBUG) {
Timber.d(
"focus on startPosition=$startPosition, from initialPosition=$initialPosition",
)
}
focusedIndex = startPosition
gridState.scrollToItem(startPosition, 0)
firstFocus.tryRequestFocus()
}
} else {
val index = savedFocusedIndex
if (DEBUG) Timber.d("savedFocusedIndex=$index")
if (index in 0..<pager.size) {
// If this is a recomposition, but not the first
// focus on the restored index
// gridState.scrollToItem(index, -columns)
firstFocus.tryRequestFocus()
}
savedFocusedIndex = -1
}
// hasRun = true
}
var alphabetFocus by remember { mutableStateOf(false) }
val focusOn = { index: Int ->
@ -129,12 +87,12 @@ fun CardGrid(
}
// Wait for a recomposition to focus
LaunchedEffect(alphabetFocus) {
if (alphabetFocus) {
firstFocus.tryRequestFocus()
}
alphabetFocus = false
}
// LaunchedEffect(alphabetFocus) {
// if (alphabetFocus) {
// firstFocus.tryRequestFocus()
// }
// alphabetFocus = false
// }
val useBackToJump = true // uiConfig.preferences.interfacePreferences.scrollTopOnBack
val showFooter = true // uiConfig.preferences.interfacePreferences.showPositionFooter
@ -169,7 +127,6 @@ fun CardGrid(
val newPosition =
(gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size)
if (DEBUG) Timber.d("newPosition=$newPosition")
savedFocusedIndex = newPosition
focusOn(newPosition)
gridState.scrollToItem(newPosition, 0)
}
@ -255,51 +212,38 @@ fun CardGrid(
.fillMaxSize()
.focusGroup()
.focusRestorer(firstFocus)
.focusRequester(gridFocusRequester)
.focusProperties {
onExit = {
// Leaving the grid, so "forget" the position
focusedIndexOnExit = focusedIndex
focusedIndex = -1
savedFocusedIndex = -1
}
onEnter = {
focusedIndexOnExit = -1
if (focusedIndex < 0 && gridState.firstVisibleItemIndex <= startPosition) {
Timber.d("onEnter: focusedIndex=$focusedIndex, savedFocusedIndex=$savedFocusedIndex")
focusedIndex = startPosition
firstFocus.tryRequestFocus()
}
}
},
) {
items(pager.size) { index ->
val mod =
if (index == savedFocusedIndex) {
if (DEBUG) Timber.d("Adding firstFocus to itemClickedIndex $index")
Modifier.focusRequester(firstFocus)
} else if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
if ((index == focusedIndex) or (focusedIndex < 0 && index == 0)) {
if (DEBUG) Timber.d("Adding firstFocus to focusedIndex $index")
Modifier.focusRequester(firstFocus)
Modifier
.focusRequester(firstFocus)
.focusRequester(gridFocusRequester)
} else {
Modifier
}
val item = pager[index]
if (!hasRequestFocusRun && requestFocus && initialPosition >= 0) {
// On very first composition, if parent wants to focus on the grid, do so
LaunchedEffect(Unit) {
if (DEBUG) {
Timber.d(
"non-null focus on startPosition=$startPosition, from initialPosition=$initialPosition",
)
GridCard(
item = item,
onClick = {
if (item != null) {
focusedIndex = index
itemOnClick.invoke(item)
}
// focus on startPosition
gridState.scrollToItem(startPosition, 0)
firstFocus.tryRequestFocus()
hasRequestFocusRun = true
}
}
ItemCard(
},
onLongClick = { if (item != null) longClicker.invoke(item) },
modifier =
mod
.ifElse(index == 0, Modifier.focusRequester(zeroFocus))
@ -314,20 +258,11 @@ fun CardGrid(
focusOn(index)
positionCallback?.invoke(columns, index)
} else if (focusedIndex == index) {
savedFocusedIndex = index
// Was focused on this, so mark unfocused
focusedIndex = -1
// savedFocusedIndex = index
// // Was focused on this, so mark unfocused
// focusedIndex = -1
}
},
item = item,
onClick = {
if (item != null) {
itemOnClick.invoke(item)
savedFocusedIndex = index
}
},
onLongClick = { if (item != null) longClicker.invoke(item) },
cardHeight = null,
)
}
}
@ -349,12 +284,12 @@ fun CardGrid(
.align(Alignment.BottomCenter)
.background(AppColors.TransparentBlack50),
) {
val index =
if (focusedIndex >= 0) {
focusedIndex + 1
} else {
max(savedFocusedIndex, focusedIndexOnExit) + 1
}
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,

View file

@ -243,7 +243,6 @@ fun CollectionDetails(
},
longClicker = {},
letterPosition = { 0 },
requestFocus = true,
gridFocusRequester = gridFocusRequester,
showJumpButtons = false, // TODO add preference
modifier = Modifier.fillMaxSize(),

View file

@ -226,6 +226,7 @@ fun SeriesOverviewContent(
)
}
BannerCard(
name = episode?.name,
imageUrl = episode?.imageUrl,
aspectRatio =
episode?.data?.primaryImageAspectRatio?.toFloat()

View file

@ -39,7 +39,6 @@ import androidx.tv.material3.Text
import coil3.compose.AsyncImage
import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.ui.cards.BannerCard
import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
@ -71,7 +70,7 @@ fun HomePage(
modifier: Modifier = Modifier,
viewModel: HomeViewModel = hiltViewModel(),
) {
OneTimeLaunchedEffect {
LaunchedEffect(Unit) {
viewModel.init(preferences)
}
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
@ -96,14 +95,8 @@ fun HomePageContent(
var position by rememberSaveable(stateSaver = RowColumnSaver) {
mutableStateOf(RowColumn(0, 0))
}
var focusedItem = position.let { homeRows.getOrNull(it.row)?.items?.getOrNull(it.column) }
var focusedItem by remember {
mutableStateOf<BaseItem?>(
homeRows.getOrNull(0)?.items?.getOrNull(
0,
),
)
}
val focusRequester = remember { FocusRequester() }
val positionFocusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
@ -181,6 +174,7 @@ fun HomePageContent(
cardContent = { index, item, cardModifier, onClick, onLongClick ->
// TODO better aspect ration handling?
BannerCard(
name = item?.data?.seriesName ?: item?.name,
imageUrl = item?.imageUrl,
aspectRatio = (2f / 3f),
cornerText = item?.data?.indexNumber?.let { "E$it" },

View file

@ -20,10 +20,12 @@ import org.jellyfin.sdk.api.client.extensions.tvShowsApi
import org.jellyfin.sdk.api.client.extensions.userApi
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import org.jellyfin.sdk.model.api.request.GetNextUpRequest
import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
@ -44,6 +46,7 @@ class HomeViewModel
"Error loading home page",
),
) {
Timber.d("init HomeViewModel")
val user by api.userApi.getCurrentUser()
// val displayPrefs =
// api.displayPreferencesApi
@ -72,68 +75,17 @@ class HomeViewModel
// )
// }
val latestMediaIncludes =
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
}
val views by api.userViewsApi.getUserViews()
val homeRows =
homeSections
.mapNotNull { section ->
Timber.Forest.v("Loading section: %s", section.name)
when (section) {
HomeSection.LATEST_MEDIA -> {
latestMediaIncludes.mapNotNull { viewId ->
val view =
views.items
.firstOrNull { it.id == viewId }
if (view?.collectionType in supportedCollectionTypes) {
val title =
view
?.name
?.let {
"Recently Added in $it"
}
val request =
GetLatestMediaRequest(
fields = DefaultItemFields,
imageTypeLimit = 1,
parentId = viewId,
groupItems = true,
limit = limit,
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
section = section,
items = latest,
title = title,
)
} else {
null
}
}
getLatest(user, limit)
}
HomeSection.RESUME -> {
val request =
GetResumeItemsRequest(
userId = user.id,
fields = DefaultItemFields,
limit = limit,
includeItemTypes = supportItemKinds,
)
val items =
api.itemsApi
.getResumeItems(request)
.content
.items
.map { BaseItem.Companion.from(it, api, true) }
val items = getResume(user.id, limit)
listOf(
HomeRow(
section = section,
@ -143,22 +95,12 @@ class HomeViewModel
}
HomeSection.NEXT_UP -> {
val request =
GetNextUpRequest(
fields = DefaultItemFields,
imageTypeLimit = 1,
parentId = null,
limit = limit,
enableResumable = false,
enableUserData = true,
enableRewatching = preferences.appPreferences.homePagePreferences.enableRewatchingNextUp,
)
val nextUp =
api.tvShowsApi
.getNextUp(request)
.content
.items
.map { BaseItem.Companion.from(it, api, true) }
getNextUp(
user.id,
limit,
preferences.appPreferences.homePagePreferences.enableRewatchingNextUp,
)
listOf(
HomeRow(
section = section,
@ -186,4 +128,92 @@ class HomeViewModel
}
}
}
private suspend fun getResume(
userId: UUID,
limit: Int,
): List<BaseItem> {
val request =
GetResumeItemsRequest(
userId = userId,
fields = DefaultItemFields,
limit = limit,
includeItemTypes = supportItemKinds,
)
val items =
api.itemsApi
.getResumeItems(request)
.content
.items
.map { BaseItem.Companion.from(it, api, true) }
return items
}
private suspend fun getNextUp(
userId: UUID,
limit: Int,
enableRewatching: Boolean,
): List<BaseItem> {
val request =
GetNextUpRequest(
userId = userId,
fields = DefaultItemFields,
imageTypeLimit = 1,
parentId = null,
limit = limit,
enableResumable = false,
enableUserData = true,
enableRewatching = enableRewatching,
)
val nextUp =
api.tvShowsApi
.getNextUp(request)
.content
.items
.map { BaseItem.Companion.from(it, api, true) }
return nextUp
}
private suspend fun getLatest(
user: UserDto,
limit: Int,
): List<HomeRow> {
val latestMediaIncludes =
user.configuration?.orderedViews.orEmpty().toMutableList().apply {
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
}
val views by api.userViewsApi.getUserViews()
val rows =
latestMediaIncludes
.mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } }
.filter { it.collectionType in supportedCollectionTypes }
.map { view ->
val title =
view
?.name
?.let {
"Recently Added in $it"
}
val request =
GetLatestMediaRequest(
fields = DefaultItemFields,
imageTypeLimit = 1,
parentId = view.id,
groupItems = true,
limit = limit,
)
val latest =
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.from(it, api, true) }
HomeRow(
section = HomeSection.LATEST_MEDIA,
items = latest,
title = title,
)
}
return rows
}
}

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="455dp" android:viewportHeight="512" android:viewportWidth="576" android:width="512dp">
<path android:fillColor="#FF000000" android:pathData="M0,128C0,92.7 28.7,64 64,64l256,0c35.3,0 64,28.7 64,64l0,256c0,35.3 -28.7,64 -64,64L64,448c-35.3,0 -64,-28.7 -64,-64L0,128zM559.1,99.8c10.4,5.6 16.9,16.4 16.9,28.2l0,256c0,11.8 -6.5,22.6 -16.9,28.2s-23,5 -32.9,-1.6l-96,-64L416,337.1l0,-17.1 0,-128 0,-17.1 14.2,-9.5 96,-64c9.8,-6.5 22.4,-7.2 32.9,-1.6z"/>
</vector>