mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +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(
|
fun from(
|
||||||
dto: BaseItemDto,
|
dto: BaseItemDto,
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
|
useSeriesForPrimary: Boolean = false,
|
||||||
): BaseItem {
|
): BaseItem {
|
||||||
val backdropImageUrl =
|
val backdropImageUrl =
|
||||||
if (dto.type == BaseItemKind.EPISODE) {
|
if (dto.type == BaseItemKind.EPISODE) {
|
||||||
|
|
@ -73,9 +74,20 @@ data class BaseItem(
|
||||||
} else {
|
} else {
|
||||||
api.imageApi.getItemImageUrl(dto.id, ImageType.BACKDROP)
|
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(
|
return BaseItem(
|
||||||
dto,
|
dto,
|
||||||
api.imageApi.getItemImageUrl(dto.id, ImageType.PRIMARY),
|
primaryImageUrl,
|
||||||
backdropImageUrl,
|
backdropImageUrl,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,19 @@ fun ItemRow(
|
||||||
onClickItem: (BaseItem) -> Unit,
|
onClickItem: (BaseItem) -> Unit,
|
||||||
onLongClickItem: (BaseItem) -> Unit,
|
onLongClickItem: (BaseItem) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
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,
|
focusPair: FocusPair? = null,
|
||||||
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,
|
cardOnFocus: ((isFocused: Boolean, index: Int) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
|
|
@ -72,13 +85,47 @@ fun ItemRow(
|
||||||
}
|
}
|
||||||
cardOnFocus?.invoke(it.isFocused, index)
|
cardOnFocus?.invoke(it.isFocused, index)
|
||||||
}
|
}
|
||||||
ItemCard(
|
cardContent.invoke(
|
||||||
item = item,
|
item,
|
||||||
onClick = { if (item != null) onClickItem.invoke(item) },
|
cardModifier,
|
||||||
onLongClick = { if (item != null) onLongClickItem.invoke(item) },
|
{ if (item != null) onClickItem.invoke(item) },
|
||||||
modifier = cardModifier,
|
{ 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.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
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.MaterialTheme
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
import com.github.damontecres.dolphin.data.model.BaseItem
|
import com.github.damontecres.dolphin.data.model.BaseItem
|
||||||
import com.github.damontecres.dolphin.preferences.UserPreferences
|
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.cards.ItemRow
|
||||||
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
|
||||||
import com.github.damontecres.dolphin.ui.isNotNullOrBlank
|
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.roundMinutes
|
||||||
import com.github.damontecres.dolphin.ui.timeRemaining
|
import com.github.damontecres.dolphin.ui.timeRemaining
|
||||||
import com.github.damontecres.dolphin.util.formatDateTime
|
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.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 org.jellyfin.sdk.model.extensions.ticks
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
data class HomeRow(
|
data class HomeRow(
|
||||||
val section: HomeSection,
|
val section: HomeSection,
|
||||||
val items: List<BaseItem>,
|
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
|
@Composable
|
||||||
fun MainPage(
|
fun MainPage(
|
||||||
preferences: UserPreferences,
|
preferences: UserPreferences,
|
||||||
|
|
@ -241,7 +116,7 @@ fun MainPage(
|
||||||
) {
|
) {
|
||||||
items(homeRows) { row ->
|
items(homeRows) { row ->
|
||||||
ItemRow(
|
ItemRow(
|
||||||
title = stringResource(row.section.nameRes),
|
title = row.title ?: stringResource(row.section.nameRes),
|
||||||
items = row.items,
|
items = row.items,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
navigationManager.navigateTo(it.destination())
|
navigationManager.navigateTo(it.destination())
|
||||||
|
|
@ -251,6 +126,21 @@ fun MainPage(
|
||||||
},
|
},
|
||||||
onLongClickItem = {},
|
onLongClickItem = {},
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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