From 00f6de8ec4f45f5b4cc44cc6e81bb008800e7f7b Mon Sep 17 00:00:00 2001 From: Damontecres Date: Fri, 23 Jan 2026 19:01:21 -0500 Subject: [PATCH] Handle genre cards --- .../wholphin/data/model/BaseItem.kt | 1 + .../wholphin/ui/cards/BannerCard.kt | 13 ++- .../wholphin/ui/cards/GenreCard.kt | 31 +++-- .../wholphin/ui/components/GenreCardGrid.kt | 104 ++++++++++------- .../damontecres/wholphin/ui/main/HomePage.kt | 108 ++++++++++-------- .../ui/main/settings/HomePageSettings.kt | 28 ++++- 6 files changed, 181 insertions(+), 104 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt index 312b7446..c32f4a45 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/data/model/BaseItem.kt @@ -32,6 +32,7 @@ import kotlin.time.Duration data class BaseItem( val data: BaseItemDto, val useSeriesForPrimary: Boolean, + val imageUrlOverride: String? = null, ) : CardGridItem { val id get() = data.id diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt index 1c75cc6f..5999a269 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/BannerCard.kt @@ -78,12 +78,13 @@ fun BannerCard( val imageUrl = remember(item, fillHeight, imageType) { if (item != null) { - imageUrlService.getItemImageUrl( - item, - imageType, - fillWidth = null, - fillHeight = fillHeight, - ) + item.imageUrlOverride + ?: imageUrlService.getItemImageUrl( + item, + imageType, + fillWidth = null, + fillHeight = fillHeight, + ) } else { null } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GenreCard.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GenreCard.kt index 73cf5ba3..a32fece9 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GenreCard.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/cards/GenreCard.kt @@ -42,11 +42,29 @@ fun GenreCard( onLongClick: () -> Unit, modifier: Modifier = Modifier, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, +) = GenreCard( + genreId = genre?.id, + name = genre?.name, + imageUrl = genre?.imageUrl, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + interactionSource = interactionSource, +) + +@Composable +fun GenreCard( + genreId: UUID?, + name: String?, + imageUrl: String?, + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { - val background = rememberIdColor(genre?.id).copy(alpha = .6f) + val background = rememberIdColor(genreId).copy(alpha = .6f) Card( - modifier = - modifier, + modifier = modifier, onClick = onClick, onLongClick = onLongClick, interactionSource = interactionSource, @@ -63,12 +81,12 @@ fun GenreCard( .fillMaxSize() .clip(RoundedCornerShape(8.dp)), ) { - if (genre?.imageUrl.isNotNullOrBlank()) { + if (imageUrl != null) { AsyncImage( model = ImageRequest .Builder(LocalContext.current) - .data(genre.imageUrl) + .data(imageUrl) .crossfade(true) .build(), contentScale = ContentScale.FillBounds, @@ -88,7 +106,7 @@ fun GenreCard( .background(background), ) { Text( - text = genre?.name ?: "", + text = name ?: "", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, @@ -112,7 +130,6 @@ private fun GenreCardPreview() { UUID.randomUUID(), "Adventure", null, - Color.Black, ) GenreCard( genre = genre, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt index cdf18969..9d6f662b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/components/GenreCardGrid.kt @@ -5,12 +5,12 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp @@ -102,51 +102,21 @@ class GenreViewModel .execute(api, request) .content.items .map { - Genre(it.id, it.name ?: "", null, Color.Black) + Genre(it.id, it.name ?: "", null) } withContext(Dispatchers.Main) { this@GenreViewModel.genres.value = genres loading.value = LoadingState.Success } - val genreToUrl = ConcurrentHashMap() - val semaphore = Semaphore(4) - genres - .map { genre -> - viewModelScope.async(Dispatchers.IO) { - semaphore.withPermit { - val item = - GetItemsRequestHandler - .execute( - api, - GetItemsRequest( - parentId = itemId, - recursive = true, - limit = 1, - sortBy = listOf(ItemSortBy.RANDOM), - fields = listOf(ItemFields.GENRES), - imageTypes = listOf(ImageType.BACKDROP), - imageTypeLimit = 1, - includeItemTypes = includeItemTypes, - genreIds = listOf(genre.id), - enableTotalRecordCount = false, - ), - ).content.items - .firstOrNull() - if (item != null) { - genreToUrl[genre.id] = - imageUrlService.getItemImageUrl( - itemId = item.id, - itemType = item.type, - seriesId = null, - useSeriesForPrimary = true, - imageType = ImageType.BACKDROP, - imageTags = item.imageTags.orEmpty(), - fillWidth = cardWidthPx, - ) - } - } - } - }.awaitAll() + val genreToUrl = + getGenreImageMap( + api = api, + imageUrlService = imageUrlService, + genres = genres.map { it.id }, + parentId = itemId, + includeItemTypes = includeItemTypes, + cardWidthPx = cardWidthPx, + ) val genresWithImages = genres.map { it.copy( @@ -171,11 +141,61 @@ class GenreViewModel } } +suspend fun ViewModel.getGenreImageMap( + api: ApiClient, + imageUrlService: ImageUrlService, + genres: List, + parentId: UUID, + includeItemTypes: List?, + cardWidthPx: Int?, +): Map { + val genreToUrl = ConcurrentHashMap() + val semaphore = Semaphore(4) + genres + .map { genreId -> + viewModelScope.async(Dispatchers.IO) { + semaphore.withPermit { + val item = + GetItemsRequestHandler + .execute( + api, + GetItemsRequest( + parentId = parentId, + recursive = true, + limit = 1, + sortBy = listOf(ItemSortBy.RANDOM), + fields = listOf(ItemFields.GENRES), + imageTypes = listOf(ImageType.BACKDROP), + imageTypeLimit = 1, + includeItemTypes = includeItemTypes, + genreIds = listOf(genreId), + enableTotalRecordCount = false, + ), + ).content.items + .firstOrNull() + if (item != null) { + genreToUrl[genreId] = + imageUrlService.getItemImageUrl( + itemId = item.id, + itemType = item.type, + seriesId = null, + useSeriesForPrimary = true, + imageType = ImageType.BACKDROP, + imageTags = item.imageTags.orEmpty(), + fillWidth = cardWidthPx, + ) + } + } + } + }.awaitAll() + return genreToUrl +} + +@Stable data class Genre( val id: UUID, val name: String, val imageUrl: String?, - val color: Color, ) : CardGridItem { override val gridId: String get() = id.toString() override val playable: Boolean = false diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt index 88b2fd27..dbcf8fb0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/HomePage.kt @@ -48,6 +48,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.cards.BannerCard +import com.github.damontecres.wholphin.ui.cards.GenreCard import com.github.damontecres.wholphin.ui.cards.ItemRow import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.DialogParams @@ -326,52 +327,67 @@ fun HomePageContent( .animateItem(), horizontalPadding = viewOptions.spacing.dp, cardContent = { index, item, cardModifier, onClick, onLongClick -> - BannerCard( - name = item?.data?.seriesName ?: item?.name, - item = item, - aspectRatio = viewOptions.aspectRatio.ratio, - imageType = viewOptions.imageType.imageType, - imageContentScale = viewOptions.contentScale.scale, - cornerText = item?.ui?.episdodeUnplayedCornerText, - played = item?.data?.userData?.played ?: false, - favorite = item?.favorite ?: false, - playPercent = - item?.data?.userData?.playedPercentage - ?: 0.0, - onClick = onClick, - onLongClick = onLongClick, - modifier = - cardModifier - .onFocusChanged { - if (it.isFocused) { - position = RowColumn(rowIndex, index) -// item?.let(onUpdateBackdrop) - } - if (it.isFocused && onFocusPosition != null) { - val nonEmptyRowBefore = - homeRows - .subList(0, rowIndex) - .count { - it is HomeRowLoadingState.Success && it.items.isEmpty() - } - onFocusPosition.invoke( - RowColumn( - rowIndex - nonEmptyRowBefore, - index, - ), - ) - } - }.onKeyEvent { - if (isPlayKeyUp(it) && item?.type?.playable == true) { - Timber.v("Clicked play on ${item.id}") - onClickPlay.invoke(position, item) - return@onKeyEvent true - } - return@onKeyEvent false - }, - interactionSource = null, - cardHeight = viewOptions.heightDp.dp, - ) + val mod = + cardModifier + .onFocusChanged { + if (it.isFocused) { + position = RowColumn(rowIndex, index) + } + if (it.isFocused && onFocusPosition != null) { + val nonEmptyRowBefore = + homeRows + .subList(0, rowIndex) + .count { + it is HomeRowLoadingState.Success && it.items.isEmpty() + } + onFocusPosition.invoke( + RowColumn( + rowIndex - nonEmptyRowBefore, + index, + ), + ) + } + }.onKeyEvent { + if (isPlayKeyUp(it) && item?.type?.playable == true) { + Timber.v("Clicked play on ${item.id}") + onClickPlay.invoke(position, item) + return@onKeyEvent true + } + return@onKeyEvent false + } + when (item?.type) { + BaseItemKind.GENRE -> { + GenreCard( + genreId = item.id, + name = item.name, + imageUrl = item.imageUrlOverride, + onClick = onClick, + onLongClick = onLongClick, + modifier = mod.height(viewOptions.heightDp.dp), + ) + } + + else -> { + BannerCard( + name = item?.data?.seriesName ?: item?.name, + item = item, + aspectRatio = viewOptions.aspectRatio.ratio, + imageType = viewOptions.imageType.imageType, + imageContentScale = viewOptions.contentScale.scale, + cornerText = item?.ui?.episdodeUnplayedCornerText, + played = item?.data?.userData?.played ?: false, + favorite = item?.favorite ?: false, + playPercent = + item?.data?.userData?.playedPercentage + ?: 0.0, + onClick = onClick, + onLongClick = onLongClick, + modifier = mod, + interactionSource = null, + cardHeight = viewOptions.heightDp.dp, + ) + } + } }, ) } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt index 9372d547..c57d17b0 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/main/settings/HomePageSettings.kt @@ -32,9 +32,13 @@ import com.github.damontecres.wholphin.data.model.HomeRowConfig import com.github.damontecres.wholphin.data.model.HomeRowConfigDisplay import com.github.damontecres.wholphin.data.model.HomeRowViewOptions import com.github.damontecres.wholphin.services.BackdropService +import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.LatestNextUpService import com.github.damontecres.wholphin.services.UserPreferencesService +import com.github.damontecres.wholphin.ui.AspectRatio +import com.github.damontecres.wholphin.ui.Cards import com.github.damontecres.wholphin.ui.SlimItemFields +import com.github.damontecres.wholphin.ui.components.getGenreImageMap import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.main.HomePageContent import com.github.damontecres.wholphin.ui.main.LatestData @@ -65,6 +69,7 @@ class HomePageSettingsViewModel private val navDrawerItemRepository: NavDrawerItemRepository, private val backdropService: BackdropService, private val latestNextUpService: LatestNextUpService, + private val imageUrlService: ImageUrlService, ) : ViewModel() { private val _state = MutableStateFlow(HomePageSettingsState.EMPTY) val state: StateFlow = _state @@ -197,7 +202,21 @@ class HomePageSettingsViewModel GetGenresRequestHandler .execute(api, request) .content.items - .map { BaseItem(it, false) } + val genreIds = items.map { it.id } + val genreImages = + getGenreImageMap( + api = api, + imageUrlService = imageUrlService, + genres = genreIds, + parentId = row.parentId, + includeItemTypes = null, + cardWidthPx = null, + ) + val genres = + items.map { + BaseItem(it, false, genreImages[it.id]) + } + val name = _state.value.libraries .firstOrNull { it.itemId == row.parentId } @@ -208,7 +227,7 @@ class HomePageSettingsViewModel listOf( HomeRowLoadingState.Success( title, - items, + genres, viewOptions = row.viewOptions, ), ) @@ -347,7 +366,10 @@ class HomePageSettingsViewModel HomeRowConfig.Genres( UUID.randomUUID(), library.itemId, - HomeRowViewOptions(), + HomeRowViewOptions( + heightDp = (Cards.HEIGHT_2X3_DP * .75f).toInt(), + aspectRatio = AspectRatio.WIDE, + ), ), ) }