mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +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
|
val context = LocalContext.current
|
||||||
var firstLoad by rememberSaveable { mutableStateOf(true) }
|
var firstLoad by rememberSaveable { mutableStateOf(true) }
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.init(preferences).join()
|
viewModel.init(firstLoad, preferences).join()
|
||||||
firstLoad = false
|
firstLoad = false
|
||||||
}
|
}
|
||||||
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
|
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) {
|
LaunchedEffect(loading) {
|
||||||
val state = loading
|
val state = loading
|
||||||
if (!firstLoad && state is LoadingState.Error) {
|
if (!firstLoad && state is LoadingState.Error) {
|
||||||
|
|
@ -118,7 +119,7 @@ fun HomePage(
|
||||||
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
var showPlaylistDialog by remember { mutableStateOf<UUID?>(null) }
|
||||||
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
val playlistState by playlistViewModel.playlistState.observeAsState(PlaylistLoadingState.Pending)
|
||||||
HomePageContent(
|
HomePageContent(
|
||||||
homeRows,
|
watchingRows + latestRows,
|
||||||
onClickItem = {
|
onClickItem = {
|
||||||
viewModel.navigationManager.navigateTo(it.destination())
|
viewModel.navigationManager.navigateTo(it.destination())
|
||||||
},
|
},
|
||||||
|
|
@ -277,7 +278,7 @@ fun HomePageContent(
|
||||||
-> {
|
-> {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = Modifier.animateItem(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = r.title,
|
text = r.title,
|
||||||
|
|
@ -295,7 +296,7 @@ fun HomePageContent(
|
||||||
is HomeRowLoadingState.Error -> {
|
is HomeRowLoadingState.Error -> {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
modifier = modifier,
|
modifier = Modifier.animateItem(),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = r.title,
|
text = r.title,
|
||||||
|
|
@ -323,7 +324,10 @@ fun HomePageContent(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLongClickItem = onLongClickItem,
|
onLongClickItem = onLongClickItem,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateItem(),
|
||||||
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
cardContent = { index, item, cardModifier, onClick, onLongClick ->
|
||||||
// TODO better aspect ration handling?
|
// TODO better aspect ration handling?
|
||||||
BannerCard(
|
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.SlimItemFields
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.ui.nav.ServerNavDrawerItem
|
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.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
import com.github.damontecres.wholphin.util.HomeRowLoadingState
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
|
@ -51,12 +52,18 @@ class HomeViewModel
|
||||||
val navDrawerItemRepository: NavDrawerItemRepository,
|
val navDrawerItemRepository: NavDrawerItemRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val loadingState = MutableLiveData<LoadingState>(LoadingState.Pending)
|
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
|
private lateinit var preferences: UserPreferences
|
||||||
|
|
||||||
fun init(preferences: UserPreferences): Job {
|
fun init(
|
||||||
loadingState.value = LoadingState.Loading
|
firstLoad: Boolean,
|
||||||
|
preferences: UserPreferences,
|
||||||
|
): Job {
|
||||||
|
if (firstLoad) {
|
||||||
|
loadingState.value = LoadingState.Loading
|
||||||
|
}
|
||||||
this.preferences = preferences
|
this.preferences = preferences
|
||||||
val prefs = preferences.appPreferences.homePagePreferences
|
val prefs = preferences.appPreferences.homePagePreferences
|
||||||
val limit = prefs.maxItemsPerRow
|
val limit = prefs.maxItemsPerRow
|
||||||
|
|
@ -84,9 +91,7 @@ class HomeViewModel
|
||||||
prefs.enableRewatchingNextUp,
|
prefs.enableRewatchingNextUp,
|
||||||
prefs.combineContinueNext,
|
prefs.combineContinueNext,
|
||||||
)
|
)
|
||||||
val latest = getLatest(userDto, limit, includedIds)
|
val watching =
|
||||||
|
|
||||||
val homeRows =
|
|
||||||
buildList {
|
buildList {
|
||||||
if (resume.isNotEmpty()) {
|
if (resume.isNotEmpty()) {
|
||||||
add(
|
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) {
|
withContext(Dispatchers.Main) {
|
||||||
this@HomeViewModel.homeRows.value = homeRows
|
this@HomeViewModel.watchingRows.value = watching
|
||||||
|
if (firstLoad) {
|
||||||
|
this@HomeViewModel.latestRows.value = pendingLatest
|
||||||
|
}
|
||||||
loadingState.value = LoadingState.Success
|
loadingState.value = LoadingState.Success
|
||||||
}
|
}
|
||||||
|
loadLatest(latest)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +152,7 @@ class HomeViewModel
|
||||||
.getResumeItems(request)
|
.getResumeItems(request)
|
||||||
.content
|
.content
|
||||||
.items
|
.items
|
||||||
.map { BaseItem.Companion.from(it, api, true) }
|
.map { BaseItem.from(it, api, true) }
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -174,70 +186,71 @@ class HomeViewModel
|
||||||
user: UserDto,
|
user: UserDto,
|
||||||
limit: Int,
|
limit: Int,
|
||||||
includedIds: List<UUID>,
|
includedIds: List<UUID>,
|
||||||
): List<HomeRowLoadingState> {
|
): List<LatestData> {
|
||||||
val latestMediaIncludes =
|
val excluded = user.configuration?.latestItemsExcludes.orEmpty()
|
||||||
user.configuration
|
|
||||||
?.orderedViews
|
|
||||||
.orEmpty()
|
|
||||||
.toMutableList()
|
|
||||||
.apply {
|
|
||||||
removeAll(user.configuration?.latestItemsExcludes.orEmpty())
|
|
||||||
}.filter { includedIds.contains(it) }
|
|
||||||
|
|
||||||
val views by api.userViewsApi.getUserViews()
|
val views by api.userViewsApi.getUserViews()
|
||||||
val rows =
|
val latestData =
|
||||||
latestMediaIncludes
|
views.items
|
||||||
.mapNotNull { viewId -> views.items.firstOrNull { it.id == viewId } }
|
.filter {
|
||||||
.filter { it.collectionType in supportedLatestCollectionTypes }
|
it.id in includedIds && it.id !in excluded &&
|
||||||
.mapNotNull { view ->
|
it.collectionType in supportedLatestCollectionTypes
|
||||||
|
}.mapNotNull { view ->
|
||||||
val title =
|
val title =
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
context.getString(R.string.recently_recorded)
|
context.getString(R.string.recently_recorded)
|
||||||
} else {
|
} else {
|
||||||
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
view.name?.let { context.getString(R.string.recently_added_in, it) }
|
||||||
} ?: context.getString(R.string.recently_added)
|
} ?: context.getString(R.string.recently_added)
|
||||||
try {
|
val viewId =
|
||||||
val viewId =
|
if (view.collectionType == CollectionType.LIVETV) {
|
||||||
if (view.collectionType == CollectionType.LIVETV) {
|
api.liveTvApi
|
||||||
api.liveTvApi
|
.getRecordingFolders(
|
||||||
.getRecordingFolders(
|
userId = user.id,
|
||||||
userId = user.id,
|
).content.items
|
||||||
).content.items
|
.firstOrNull()
|
||||||
.firstOrNull()
|
?.id
|
||||||
?.id
|
} else {
|
||||||
} else {
|
view.id
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
} catch (ex: Exception) {
|
viewId?.let {
|
||||||
Timber.e(ex, "Exception fetching %s", title)
|
val request =
|
||||||
HomeRowLoadingState.Error(
|
GetLatestMediaRequest(
|
||||||
title = title,
|
fields = SlimItemFields,
|
||||||
exception = ex,
|
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(
|
fun setWatched(
|
||||||
|
|
@ -250,7 +263,7 @@ class HomeViewModel
|
||||||
api.playStateApi.markUnplayedItem(itemId)
|
api.playStateApi.markUnplayedItem(itemId)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
init(preferences)
|
init(false, preferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -264,7 +277,7 @@ class HomeViewModel
|
||||||
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
api.userLibraryApi.unmarkFavoriteItem(itemId)
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
init(preferences)
|
init(false, preferences)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -275,3 +288,8 @@ val supportedLatestCollectionTypes =
|
||||||
CollectionType.TVSHOWS,
|
CollectionType.TVSHOWS,
|
||||||
CollectionType.LIVETV,
|
CollectionType.LIVETV,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class LatestData(
|
||||||
|
val title: String,
|
||||||
|
val request: GetLatestMediaRequest,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -148,6 +148,20 @@ fun DestinationContent(
|
||||||
|
|
||||||
BaseItemKind.USER_VIEW ->
|
BaseItemKind.USER_VIEW ->
|
||||||
when (destination.item?.data?.collectionType) {
|
when (destination.item?.data?.collectionType) {
|
||||||
|
CollectionType.TVSHOWS ->
|
||||||
|
CollectionFolderTv(
|
||||||
|
preferences,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
CollectionType.MOVIES ->
|
||||||
|
CollectionFolderMovie(
|
||||||
|
preferences,
|
||||||
|
destination,
|
||||||
|
modifier,
|
||||||
|
)
|
||||||
|
|
||||||
CollectionType.LIVETV ->
|
CollectionType.LIVETV ->
|
||||||
CollectionFolderLiveTv(
|
CollectionFolderLiveTv(
|
||||||
preferences,
|
preferences,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue