mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Update main page
This commit is contained in:
parent
933dc4fd5a
commit
805f63cb0e
4 changed files with 234 additions and 134 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<BaseItem?>,
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<BaseItem>,
|
||||
val title: String? = null,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class MainViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
val api: ApiClient,
|
||||
) : ViewModel() {
|
||||
val homeRows = MutableLiveData<List<HomeRow>>()
|
||||
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<HomeRow>>()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue