mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-09 08:01:20 +02:00
Handle genre cards
This commit is contained in:
parent
45e023d1b2
commit
00f6de8ec4
6 changed files with 181 additions and 104 deletions
|
|
@ -32,6 +32,7 @@ import kotlin.time.Duration
|
||||||
data class BaseItem(
|
data class BaseItem(
|
||||||
val data: BaseItemDto,
|
val data: BaseItemDto,
|
||||||
val useSeriesForPrimary: Boolean,
|
val useSeriesForPrimary: Boolean,
|
||||||
|
val imageUrlOverride: String? = null,
|
||||||
) : CardGridItem {
|
) : CardGridItem {
|
||||||
val id get() = data.id
|
val id get() = data.id
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,12 +78,13 @@ fun BannerCard(
|
||||||
val imageUrl =
|
val imageUrl =
|
||||||
remember(item, fillHeight, imageType) {
|
remember(item, fillHeight, imageType) {
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
imageUrlService.getItemImageUrl(
|
item.imageUrlOverride
|
||||||
item,
|
?: imageUrlService.getItemImageUrl(
|
||||||
imageType,
|
item,
|
||||||
fillWidth = null,
|
imageType,
|
||||||
fillHeight = fillHeight,
|
fillWidth = null,
|
||||||
)
|
fillHeight = fillHeight,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,29 @@ fun GenreCard(
|
||||||
onLongClick: () -> Unit,
|
onLongClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
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(
|
Card(
|
||||||
modifier =
|
modifier = modifier,
|
||||||
modifier,
|
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
|
|
@ -63,12 +81,12 @@ fun GenreCard(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
) {
|
) {
|
||||||
if (genre?.imageUrl.isNotNullOrBlank()) {
|
if (imageUrl != null) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model =
|
model =
|
||||||
ImageRequest
|
ImageRequest
|
||||||
.Builder(LocalContext.current)
|
.Builder(LocalContext.current)
|
||||||
.data(genre.imageUrl)
|
.data(imageUrl)
|
||||||
.crossfade(true)
|
.crossfade(true)
|
||||||
.build(),
|
.build(),
|
||||||
contentScale = ContentScale.FillBounds,
|
contentScale = ContentScale.FillBounds,
|
||||||
|
|
@ -88,7 +106,7 @@ fun GenreCard(
|
||||||
.background(background),
|
.background(background),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = genre?.name ?: "",
|
text = name ?: "",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
|
|
@ -112,7 +130,6 @@ private fun GenreCardPreview() {
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Adventure",
|
"Adventure",
|
||||||
null,
|
null,
|
||||||
Color.Black,
|
|
||||||
)
|
)
|
||||||
GenreCard(
|
GenreCard(
|
||||||
genre = genre,
|
genre = genre,
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -102,51 +102,21 @@ class GenreViewModel
|
||||||
.execute(api, request)
|
.execute(api, request)
|
||||||
.content.items
|
.content.items
|
||||||
.map {
|
.map {
|
||||||
Genre(it.id, it.name ?: "", null, Color.Black)
|
Genre(it.id, it.name ?: "", null)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
this@GenreViewModel.genres.value = genres
|
this@GenreViewModel.genres.value = genres
|
||||||
loading.value = LoadingState.Success
|
loading.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
val genreToUrl = ConcurrentHashMap<UUID, String?>()
|
val genreToUrl =
|
||||||
val semaphore = Semaphore(4)
|
getGenreImageMap(
|
||||||
genres
|
api = api,
|
||||||
.map { genre ->
|
imageUrlService = imageUrlService,
|
||||||
viewModelScope.async(Dispatchers.IO) {
|
genres = genres.map { it.id },
|
||||||
semaphore.withPermit {
|
parentId = itemId,
|
||||||
val item =
|
includeItemTypes = includeItemTypes,
|
||||||
GetItemsRequestHandler
|
cardWidthPx = cardWidthPx,
|
||||||
.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 genresWithImages =
|
val genresWithImages =
|
||||||
genres.map {
|
genres.map {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|
@ -171,11 +141,61 @@ class GenreViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun ViewModel.getGenreImageMap(
|
||||||
|
api: ApiClient,
|
||||||
|
imageUrlService: ImageUrlService,
|
||||||
|
genres: List<UUID>,
|
||||||
|
parentId: UUID,
|
||||||
|
includeItemTypes: List<BaseItemKind>?,
|
||||||
|
cardWidthPx: Int?,
|
||||||
|
): Map<UUID, String?> {
|
||||||
|
val genreToUrl = ConcurrentHashMap<UUID, String?>()
|
||||||
|
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(
|
data class Genre(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val name: String,
|
val name: String,
|
||||||
val imageUrl: String?,
|
val imageUrl: String?,
|
||||||
val color: Color,
|
|
||||||
) : CardGridItem {
|
) : CardGridItem {
|
||||||
override val gridId: String get() = id.toString()
|
override val gridId: String get() = id.toString()
|
||||||
override val playable: Boolean = false
|
override val playable: Boolean = false
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import com.github.damontecres.wholphin.ui.Cards
|
import com.github.damontecres.wholphin.ui.Cards
|
||||||
import com.github.damontecres.wholphin.ui.cards.BannerCard
|
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.cards.ItemRow
|
||||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||||
|
|
@ -326,52 +327,67 @@ fun HomePageContent(
|
||||||
.animateItem(),
|
.animateItem(),
|
||||||
horizontalPadding = viewOptions.spacing.dp,
|
horizontalPadding = viewOptions.spacing.dp,
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
BannerCard(
|
val mod =
|
||||||
name = item?.data?.seriesName ?: item?.name,
|
cardModifier
|
||||||
item = item,
|
.onFocusChanged {
|
||||||
aspectRatio = viewOptions.aspectRatio.ratio,
|
if (it.isFocused) {
|
||||||
imageType = viewOptions.imageType.imageType,
|
position = RowColumn(rowIndex, index)
|
||||||
imageContentScale = viewOptions.contentScale.scale,
|
}
|
||||||
cornerText = item?.ui?.episdodeUnplayedCornerText,
|
if (it.isFocused && onFocusPosition != null) {
|
||||||
played = item?.data?.userData?.played ?: false,
|
val nonEmptyRowBefore =
|
||||||
favorite = item?.favorite ?: false,
|
homeRows
|
||||||
playPercent =
|
.subList(0, rowIndex)
|
||||||
item?.data?.userData?.playedPercentage
|
.count {
|
||||||
?: 0.0,
|
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
||||||
onClick = onClick,
|
}
|
||||||
onLongClick = onLongClick,
|
onFocusPosition.invoke(
|
||||||
modifier =
|
RowColumn(
|
||||||
cardModifier
|
rowIndex - nonEmptyRowBefore,
|
||||||
.onFocusChanged {
|
index,
|
||||||
if (it.isFocused) {
|
),
|
||||||
position = RowColumn(rowIndex, index)
|
)
|
||||||
// item?.let(onUpdateBackdrop)
|
}
|
||||||
}
|
}.onKeyEvent {
|
||||||
if (it.isFocused && onFocusPosition != null) {
|
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
||||||
val nonEmptyRowBefore =
|
Timber.v("Clicked play on ${item.id}")
|
||||||
homeRows
|
onClickPlay.invoke(position, item)
|
||||||
.subList(0, rowIndex)
|
return@onKeyEvent true
|
||||||
.count {
|
}
|
||||||
it is HomeRowLoadingState.Success && it.items.isEmpty()
|
return@onKeyEvent false
|
||||||
}
|
}
|
||||||
onFocusPosition.invoke(
|
when (item?.type) {
|
||||||
RowColumn(
|
BaseItemKind.GENRE -> {
|
||||||
rowIndex - nonEmptyRowBefore,
|
GenreCard(
|
||||||
index,
|
genreId = item.id,
|
||||||
),
|
name = item.name,
|
||||||
)
|
imageUrl = item.imageUrlOverride,
|
||||||
}
|
onClick = onClick,
|
||||||
}.onKeyEvent {
|
onLongClick = onLongClick,
|
||||||
if (isPlayKeyUp(it) && item?.type?.playable == true) {
|
modifier = mod.height(viewOptions.heightDp.dp),
|
||||||
Timber.v("Clicked play on ${item.id}")
|
)
|
||||||
onClickPlay.invoke(position, item)
|
}
|
||||||
return@onKeyEvent true
|
|
||||||
}
|
else -> {
|
||||||
return@onKeyEvent false
|
BannerCard(
|
||||||
},
|
name = item?.data?.seriesName ?: item?.name,
|
||||||
interactionSource = null,
|
item = item,
|
||||||
cardHeight = viewOptions.heightDp.dp,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.HomeRowConfigDisplay
|
||||||
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
import com.github.damontecres.wholphin.data.model.HomeRowViewOptions
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
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.LatestNextUpService
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
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.SlimItemFields
|
||||||
|
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
import com.github.damontecres.wholphin.ui.main.HomePageContent
|
||||||
import com.github.damontecres.wholphin.ui.main.LatestData
|
import com.github.damontecres.wholphin.ui.main.LatestData
|
||||||
|
|
@ -65,6 +69,7 @@ class HomePageSettingsViewModel
|
||||||
private val navDrawerItemRepository: NavDrawerItemRepository,
|
private val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
private val backdropService: BackdropService,
|
private val backdropService: BackdropService,
|
||||||
private val latestNextUpService: LatestNextUpService,
|
private val latestNextUpService: LatestNextUpService,
|
||||||
|
private val imageUrlService: ImageUrlService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _state = MutableStateFlow(HomePageSettingsState.EMPTY)
|
private val _state = MutableStateFlow(HomePageSettingsState.EMPTY)
|
||||||
val state: StateFlow<HomePageSettingsState> = _state
|
val state: StateFlow<HomePageSettingsState> = _state
|
||||||
|
|
@ -197,7 +202,21 @@ class HomePageSettingsViewModel
|
||||||
GetGenresRequestHandler
|
GetGenresRequestHandler
|
||||||
.execute(api, request)
|
.execute(api, request)
|
||||||
.content.items
|
.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 =
|
val name =
|
||||||
_state.value.libraries
|
_state.value.libraries
|
||||||
.firstOrNull { it.itemId == row.parentId }
|
.firstOrNull { it.itemId == row.parentId }
|
||||||
|
|
@ -208,7 +227,7 @@ class HomePageSettingsViewModel
|
||||||
listOf(
|
listOf(
|
||||||
HomeRowLoadingState.Success(
|
HomeRowLoadingState.Success(
|
||||||
title,
|
title,
|
||||||
items,
|
genres,
|
||||||
viewOptions = row.viewOptions,
|
viewOptions = row.viewOptions,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -347,7 +366,10 @@ class HomePageSettingsViewModel
|
||||||
HomeRowConfig.Genres(
|
HomeRowConfig.Genres(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
library.itemId,
|
library.itemId,
|
||||||
HomeRowViewOptions(),
|
HomeRowViewOptions(
|
||||||
|
heightDp = (Cards.HEIGHT_2X3_DP * .75f).toInt(),
|
||||||
|
aspectRatio = AspectRatio.WIDE,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue