mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Home page improvements (#190)
Separates the loading logic into two groups: 1. Continue Watching & Next Up 2. Latest media The home page displays once the first group is ready and shows placeholders while the latest media is fetched. This also speeds up refreshes when returning home after watching an episode. Also ensure grouped Movie/TV libraries are displayed as Movie/TV libraries instead of falling back to a generic grid display. Fixes #189
This commit is contained in:
parent
98fc996bd5
commit
ce8cab0d1f
3 changed files with 106 additions and 70 deletions
|
|
@ -87,11 +87,12 @@ fun HomePage(
|
|||
val context = LocalContext.current
|
||||
var firstLoad by rememberSaveable { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.init(preferences).join()
|
||||
viewModel.init(firstLoad, preferences).join()
|
||||
firstLoad = false
|
||||
}
|
||||
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
||||
val homeRows by viewModel.homeRows.observeAsState(listOf())
|
||||
val watchingRows by viewModel.watchingRows.observeAsState(listOf())
|
||||
val latestRows by viewModel.latestRows.observeAsState(listOf())
|
||||
LaunchedEffect(loading) {
|
||||
val state = loading
|
||||
if (!firstLoad && state is LoadingState.Error) {
|
||||
|
|
@ -118,7 +119,7 @@ fun HomePage(
|
|||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||
HomePageContent(
|
||||
homeRows,
|
||||
watchingRows + latestRows,
|
||||
onClickItem = {
|
||||
viewModel.navigationManager.navigateTo(it.destination())
|
||||
},
|
||||
|
|
@ -277,7 +278,7 @@ fun HomePageContent(
|
|||
-> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
|
|
@ -295,7 +296,7 @@ fun HomePageContent(
|
|||
is HomeRowLoadingState.Error -> {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier,
|
||||
modifier = Modifier.animateItem(),
|
||||
) {
|
||||
Text(
|
||||
text = r.title,
|
||||
|
|
@ -323,7 +324,10 @@ fun HomePageContent(
|
|||
}
|
||||
},
|
||||
onLongClickItem = onLongClickItem,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(),
|
||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||
// TODO better aspect ration handling?
|
||||
BannerCard(
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.github.damontecres.wholphin.preferences.UserPreferences
|
|||
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
||||
import com.github.damontecres.wholphin.ui.setValueOnMain
|
||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||
|
|
@ -51,12 +52,18 @@ class HomeViewModel
|
|||
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||
) : ViewModel() {
|
||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val homeRows = MutableLiveData<List<HomeRowLoadingState>>()
|
||||
val watchingRows = MutableLiveData<List<HomeRowLoadingState>>(listOf())
|
||||
val latestRows = MutableLiveData<List<HomeRowLoadingState>>(listOf())
|
||||
|
||||
private lateinit var preferences: UserPreferences
|
||||
|
||||
fun init(preferences: UserPreferences): Job {
|
||||
loadingState.value = LoadingState.Loading
|
||||
fun init(
|
||||
firstLoad: Boolean,
|
||||
preferences: UserPreferences,
|
||||
): Job {
|
||||
if (firstLoad) {
|
||||
loadingState.value = LoadingState.Loading
|
||||
}
|
||||
this.preferences = preferences
|
||||
val prefs = preferences.appPreferences.homePagePreferences
|
||||
val limit = prefs.maxItemsPerRow
|
||||
|
|
@ -84,9 +91,7 @@ class HomeViewModel
|
|||
prefs.enableRewatchingNextUp,
|
||||
prefs.combineContinueNext,
|
||||
)
|
||||
val latest = getLatest(userDto, limit, includedIds)
|
||||
|
||||
val homeRows =
|
||||
val watching =
|
||||
buildList {
|
||||
if (resume.isNotEmpty()) {
|
||||
add(
|
||||
|
|
@ -104,12 +109,19 @@ class HomeViewModel
|
|||
),
|
||||
)
|
||||
}
|
||||
addAll(latest)
|
||||
}
|
||||
|
||||
val latest = getLatest(userDto, limit, includedIds)
|
||||
val pendingLatest = latest.map { HomeRowLoadingState.Loading(it.title) }
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
this@HomeViewModel.homeRows.value = homeRows
|
||||
this@HomeViewModel.watchingRows.value = watching
|
||||
if (firstLoad) {
|
||||
this@HomeViewModel.latestRows.value = pendingLatest
|
||||
}
|
||||
loadingState.value = LoadingState.Success
|
||||
}
|
||||
loadLatest(latest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -140,7 +152,7 @@ class HomeViewModel
|
|||
.getResumeItems(request)
|
||||
.content
|
||||
.items
|
||||
.map { BaseItem.Companion.from(it, api, true) }
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
return items
|
||||
}
|
||||
|
||||
|
|
@ -174,70 +186,71 @@ class HomeViewModel
|
|||
user: UserDto,
|
||||
limit: Int,
|
||||
includedIds: List<UUID>,
|
||||
): List<HomeRowLoadingState> {
|
||||
val latestMediaIncludes =
|
||||
user.configuration
|
||||
?.orderedViews
|
||||
.orEmpty()
|
||||
.toMutableList()
|
||||
.apply {
|
||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
||||
}.filter { includedIds.contains(it) }
|
||||
|
||||
): List<LatestData> {
|
||||
val excluded = user.configuration?.latestItemsExcludes.orEmpty()
|
||||
val views by api.userViewsApi.getUserViews()
|
||||
val rows =
|
||||
latestMediaIncludes
|
||||
.mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } }
|
||||
.filter { it.collectionType in supportedLatestCollectionTypes }
|
||||
.mapNotNull { view ->
|
||||
val latestData =
|
||||
views.items
|
||||
.filter {
|
||||
it.id in includedIds && it.id !in excluded &&
|
||||
it.collectionType in supportedLatestCollectionTypes
|
||||
}.mapNotNull { view ->
|
||||
val title =
|
||||
if (view.collectionType == CollectionType.LIVETV) {
|
||||
context.getString(R.string.recently_recorded)
|
||||
} else {
|
||||
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||
} ?: context.getString(R.string.recently_added)
|
||||
try {
|
||||
val viewId =
|
||||
if (view.collectionType == CollectionType.LIVETV) {
|
||||
api.liveTvApi
|
||||
.getRecordingFolders(
|
||||
userId = user.id,
|
||||
).content.items
|
||||
.firstOrNull()
|
||||
?.id
|
||||
} else {
|
||||
view.id
|
||||
}
|
||||
viewId?.let {
|
||||
val request =
|
||||
GetLatestMediaRequest(
|
||||
fields = SlimItemFields,
|
||||
imageTypeLimit = 1,
|
||||
parentId = viewId,
|
||||
groupItems = true,
|
||||
limit = limit,
|
||||
isPlayed = null, // Server will handle user's preference
|
||||
)
|
||||
val latest =
|
||||
api.userLibraryApi
|
||||
.getLatestMedia(request)
|
||||
.content
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
HomeRowLoadingState.Success(
|
||||
title = title,
|
||||
items = latest,
|
||||
)
|
||||
val viewId =
|
||||
if (view.collectionType == CollectionType.LIVETV) {
|
||||
api.liveTvApi
|
||||
.getRecordingFolders(
|
||||
userId = user.id,
|
||||
).content.items
|
||||
.firstOrNull()
|
||||
?.id
|
||||
} else {
|
||||
view.id
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching %s", title)
|
||||
HomeRowLoadingState.Error(
|
||||
title = title,
|
||||
exception = ex,
|
||||
)
|
||||
viewId?.let {
|
||||
val request =
|
||||
GetLatestMediaRequest(
|
||||
fields = SlimItemFields,
|
||||
imageTypeLimit = 1,
|
||||
parentId = viewId,
|
||||
groupItems = true,
|
||||
limit = limit,
|
||||
isPlayed = null, // Server will handle user's preference
|
||||
)
|
||||
LatestData(title, request)
|
||||
}
|
||||
}
|
||||
|
||||
return rows
|
||||
return latestData
|
||||
}
|
||||
|
||||
private suspend fun loadLatest(latestData: List<LatestData>) {
|
||||
val rows =
|
||||
latestData.map { (title, request) ->
|
||||
try {
|
||||
val latest =
|
||||
api.userLibraryApi
|
||||
.getLatestMedia(request)
|
||||
.content
|
||||
.map { BaseItem.from(it, api, true) }
|
||||
HomeRowLoadingState.Success(
|
||||
title = title,
|
||||
items = latest,
|
||||
)
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Exception fetching %s", title)
|
||||
HomeRowLoadingState.Error(
|
||||
title = title,
|
||||
exception = ex,
|
||||
)
|
||||
}
|
||||
}
|
||||
latestRows.setValueOnMain(rows)
|
||||
}
|
||||
|
||||
fun setWatched(
|
||||
|
|
@ -250,7 +263,7 @@ class HomeViewModel
|
|||
api.playStateApi.markUnplayedItem(itemId)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
init(preferences)
|
||||
init(false, preferences)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,7 +277,7 @@ class HomeViewModel
|
|||
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
init(preferences)
|
||||
init(false, preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -275,3 +288,8 @@ val supportedLatestCollectionTypes =
|
|||
CollectionType.TVSHOWS,
|
||||
CollectionType.LIVETV,
|
||||
)
|
||||
|
||||
data class LatestData(
|
||||
val title: String,
|
||||
val request: GetLatestMediaRequest,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -148,6 +148,20 @@ fun DestinationContent(
|
|||
|
||||
BaseItemKind.USER_VIEW ->
|
||||
when (destination.item?.data?.collectionType) {
|
||||
CollectionType.TVSHOWS ->
|
||||
CollectionFolderTv(
|
||||
preferences,
|
||||
destination,
|
||||
modifier,
|
||||
)
|
||||
|
||||
CollectionType.MOVIES ->
|
||||
CollectionFolderMovie(
|
||||
preferences,
|
||||
destination,
|
||||
modifier,
|
||||
)
|
||||
|
||||
CollectionType.LIVETV ->
|
||||
CollectionFolderLiveTv(
|
||||
preferences,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue