diff --git a/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt b/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt index d77d53bf..9f9ded53 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/data/model/BaseItem.kt @@ -61,6 +61,7 @@ data class BaseItem( fun from( dto: BaseItemDto, api: ApiClient, + useSeriesForPrimary: Boolean = false, ): BaseItem { val backdropImageUrl = if (dto.type == BaseItemKind.EPISODE) { @@ -73,9 +74,20 @@ data class BaseItem( } else { api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP) } + val primaryImageUrl = + if (useSeriesForPrimary && dto.type == BaseItemKind.EPISODE) { + val seriesId = dto.seriesId + if (seriesId != null) { + api.imageApi.getItemImageUrl(seriesId, ImageType.PRIMARY) + } else { + api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY) + } + } else { + api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY) + } return BaseItem( dto, - api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY), + primaryImageUrl, backdropImageUrl, ) } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt index 1893ad54..f9a0cbfa 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/cards/ItemRow.kt @@ -31,6 +31,19 @@ fun ItemRow( onClickItem: (BaseItem) -> Unit, onLongClickItem: (BaseItem) -> Unit, modifier: Modifier = Modifier, + cardContent: @Composable ( + item: BaseItem?, + modifier: Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit, + ) -> Unit = { item, modifier, onClick, onLongClick -> + ItemCard( + item = item, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + ) + }, focusPair: FocusPair? = null, cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null, ) { @@ -72,13 +85,47 @@ fun ItemRow( } cardOnFocus?.invoke(it.isFocused, index) } - ItemCard( - item = item, - onClick = { if (item != null) onClickItem.invoke(item) }, - onLongClick = { if (item != null) onLongClickItem.invoke(item) }, - modifier = cardModifier, + cardContent.invoke( + item, + cardModifier, + { if (item != null) onClickItem.invoke(item) }, + { if (item != null) onLongClickItem.invoke(item) }, ) } } } } + +@Composable +fun BannerItemRow( + title: String, + items: List, + onClickItem: (BaseItem) -> Unit, + onLongClickItem: (BaseItem) -> Unit, + modifier: Modifier = Modifier, + focusPair: FocusPair? = null, + cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null, + aspectRatioOverride: Float? = null, +) = ItemRow( + title = title, + items = items, + onClickItem = onClickItem, + onLongClickItem = onLongClickItem, + modifier = modifier, + cardContent = { item, modifier, onClick, onLongClick -> + BannerCard( + imageUrl = item?.imageUrl, + aspectRatio = + aspectRatioOverride ?: item?.data?.primaryImageAspectRatio?.toFloat() ?: (16f / 9), + cornerText = item?.data?.indexNumber?.let { "E$it" }, + played = item?.data?.userData?.played ?: false, + playPercent = item?.data?.userData?.playedPercentage ?: 0.0, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + interactionSource = null, + ) + }, + focusPair = focusPair, + cardOnFocus = cardOnFocus, +) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt index 033d4531..cb26a12e 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt @@ -30,15 +30,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import androidx.tv.material3.MaterialTheme 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.DefaultItemFields +import com.github.damontecres.dolphin.ui.cards.BannerCard import com.github.damontecres.dolphin.ui.cards.ItemRow import com.github.damontecres.dolphin.ui.components.DotSeparatedRow import com.github.damontecres.dolphin.ui.isNotNullOrBlank @@ -46,137 +43,15 @@ import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.roundMinutes import com.github.damontecres.dolphin.ui.timeRemaining import com.github.damontecres.dolphin.util.formatDateTime -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.jellyfin.sdk.api.client.ApiClient -import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi -import org.jellyfin.sdk.api.client.extensions.itemsApi -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.model.api.BaseItemKind -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 org.jellyfin.sdk.model.extensions.ticks -import timber.log.Timber -import javax.inject.Inject data class HomeRow( val section: HomeSection, val items: List, + val title: String? = null, ) -@HiltViewModel -class MainViewModel - @Inject - constructor( - val api: ApiClient, - ) : ViewModel() { - val homeRows = MutableLiveData>() - - init { - viewModelScope.launch(Dispatchers.IO) { - val user = api.userApi.getCurrentUser().content - val displayPrefs = - api.displayPreferencesApi - .getDisplayPreferences( - displayPreferencesId = "usersettings", - client = "emby", - ).content - val homeSections = - displayPrefs.customPrefs.entries - .filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() } - .sortedBy { it.key } - .map { HomeSection.fromKey(it.value ?: "") } - .filterNot { it == HomeSection.NONE } - - val homeRows = - homeSections - .mapNotNull { section -> - Timber.v("Loading section: %s", section.name) - when (section) { - HomeSection.LATEST_MEDIA -> { - user.configuration?.orderedViews?.firstOrNull()?.let { viewId -> - val request = - GetLatestMediaRequest( - fields = DefaultItemFields, - imageTypeLimit = 1, - parentId = viewId, - groupItems = true, - limit = 25, - ) - val latest = - api.userLibraryApi - .getLatestMedia(request) - .content - .map { BaseItem.from(it, api) } - HomeRow( - section = section, - items = latest, - ) - } - } - - // TODO - HomeSection.LIBRARY_TILES_SMALL -> null - HomeSection.RESUME -> { - val request = - GetResumeItemsRequest( - userId = user.id, - fields = DefaultItemFields, - // TODO, more params? - ) - val items = - api.itemsApi - .getResumeItems(request) - .content - .items - .map { BaseItem.from(it, api) } - HomeRow( - section = section, - items = items, - ) - } - HomeSection.ACTIVE_RECORDINGS -> null - HomeSection.NEXT_UP -> { - val request = - GetNextUpRequest( - fields = DefaultItemFields, - imageTypeLimit = 1, - parentId = null, - limit = 25, - enableResumable = false, - ) - val nextUp = - api.tvShowsApi - .getNextUp(request) - .content - .items - .map { BaseItem.from(it, api) } - HomeRow( - section = section, - items = nextUp, - ) - } - HomeSection.LIVE_TV -> null - - // TODO Not supported? - HomeSection.LIBRARY_BUTTONS -> null - HomeSection.RESUME_AUDIO -> null - HomeSection.RESUME_BOOK -> null - HomeSection.NONE -> null - } - }.filter { it.items.isNotEmpty() } - withContext(Dispatchers.Main) { - this@MainViewModel.homeRows.value = homeRows - } - } - } - } - @Composable fun MainPage( preferences: UserPreferences, @@ -241,7 +116,7 @@ fun MainPage( ) { items(homeRows) { row -> ItemRow( - title = stringResource(row.section.nameRes), + title = row.title ?: stringResource(row.section.nameRes), items = row.items, onClickItem = { navigationManager.navigateTo(it.destination()) @@ -251,6 +126,21 @@ fun MainPage( }, onLongClickItem = {}, modifier = Modifier.fillMaxWidth(), + cardContent = { item, modifier, onClick, onLongClick -> + // TODO better aspect ration handling? + BannerCard( + imageUrl = item?.imageUrl, + aspectRatio = (2f / 3f), + cornerText = item?.data?.indexNumber?.let { "E$it" }, + played = item?.data?.userData?.played ?: false, + playPercent = item?.data?.userData?.playedPercentage ?: 0.0, + onClick = onClick, + onLongClick = onLongClick, + modifier = modifier, + interactionSource = null, + cardHeight = 200.dp, + ) + }, ) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt new file mode 100644 index 00000000..fcfa1c8b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt @@ -0,0 +1,151 @@ +package com.github.damontecres.dolphin.ui.main + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.github.damontecres.dolphin.data.model.BaseItem +import com.github.damontecres.dolphin.ui.DefaultItemFields +import com.github.damontecres.dolphin.ui.isNotNullOrBlank +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jellyfin.sdk.api.client.ApiClient +import org.jellyfin.sdk.api.client.extensions.displayPreferencesApi +import org.jellyfin.sdk.api.client.extensions.itemsApi +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.request.GetLatestMediaRequest +import org.jellyfin.sdk.model.api.request.GetNextUpRequest +import org.jellyfin.sdk.model.api.request.GetResumeItemsRequest +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class MainViewModel + @Inject + constructor( + val api: ApiClient, + ) : ViewModel() { + val homeRows = MutableLiveData>() + + init { + viewModelScope.launch(Dispatchers.IO) { + val user = api.userApi.getCurrentUser().content + val displayPrefs = + api.displayPreferencesApi + .getDisplayPreferences( + displayPreferencesId = "usersettings", + client = "emby", + ).content + val homeSections = + displayPrefs.customPrefs.entries + .filter { it.key.startsWith("homesection") && it.value.isNotNullOrBlank() } + .sortedBy { it.key } + .map { HomeSection.fromKey(it.value ?: "") } + .filterNot { it == HomeSection.NONE } + + 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.map { viewId -> + val title = + views.items.firstOrNull { it.id == viewId }?.name?.let { + "Recently Added in $it" + } + val request = + GetLatestMediaRequest( + fields = DefaultItemFields, + imageTypeLimit = 1, + parentId = viewId, + groupItems = true, + limit = 25, + ) + val latest = + api.userLibraryApi + .getLatestMedia(request) + .content + .map { BaseItem.Companion.from(it, api, true) } + HomeRow( + section = section, + items = latest, + title = title, + ) + } + } + + HomeSection.RESUME -> { + val request = + GetResumeItemsRequest( + userId = user.id, + fields = DefaultItemFields, + // TODO, more params? + ) + val items = + api.itemsApi + .getResumeItems(request) + .content + .items + .map { BaseItem.Companion.from(it, api, true) } + listOf( + HomeRow( + section = section, + items = items, + ), + ) + } + + HomeSection.NEXT_UP -> { + val request = + GetNextUpRequest( + fields = DefaultItemFields, + imageTypeLimit = 1, + parentId = null, + limit = 25, + enableResumable = false, + ) + val nextUp = + api.tvShowsApi + .getNextUp(request) + .content + .items + .map { BaseItem.Companion.from(it, api) } + listOf( + HomeRow( + section = section, + items = nextUp, + ), + ) + } + + // TODO + HomeSection.LIVE_TV -> null + HomeSection.ACTIVE_RECORDINGS -> null + + // TODO Not supported? + HomeSection.LIBRARY_TILES_SMALL -> null + HomeSection.LIBRARY_BUTTONS -> null + HomeSection.RESUME_AUDIO -> null + HomeSection.RESUME_BOOK -> null + HomeSection.NONE -> null + } + }.flatten() + .filter { it.items.isNotEmpty() } + withContext(Dispatchers.Main) { + this@MainViewModel.homeRows.value = homeRows + } + } + } + }