View more from home page & recommended rows (#1392)

## Description
Adds a card at the end of the rows on the home page and recommended tabs
to "View More" which goes to a grid of the items in the row.

The grid will endlessly scroll all of the available items except for a
few cases where the first 1000 are shown because the server does not
(yet) support pagination: genres, studios, people (added in v12), and
latest grouped tv shows.

Dev notes: This PR also rewrites the `RecommendedViewModel` and
consolidates the previous multiple view models into one.

### Related issues
Closes #1371
Closes #1035
Similar to #1198

### Testing
Emulator

## Screenshots
N/A, see #1198

## AI or LLM usage
None
This commit is contained in:
Damontecres 2026-05-16 11:35:11 -04:00 • committed by GitHub
parent 98ffaf51ac
commit f4bdb8446e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1417 additions and 1075 deletions

View file

@ -19,9 +19,12 @@ import com.github.damontecres.wholphin.ui.main.settings.favoriteOptions
import com.github.damontecres.wholphin.ui.playback.getTypeFor
import com.github.damontecres.wholphin.ui.toBaseItems
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.ApiRequestPager
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetLiveTvChannelsRequestHandler
import com.github.damontecres.wholphin.util.GetPersonsHandler
import com.github.damontecres.wholphin.util.GetRecordingsRequestHandler
import com.github.damontecres.wholphin.util.GetStudiosRequestHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Error
@ -56,6 +59,7 @@ import org.jellyfin.sdk.model.api.UserDto
import org.jellyfin.sdk.model.api.request.GetGenresRequest
import org.jellyfin.sdk.model.api.request.GetItemsRequest
import org.jellyfin.sdk.model.api.request.GetLatestMediaRequest
import org.jellyfin.sdk.model.api.request.GetLiveTvChannelsRequest
import org.jellyfin.sdk.model.api.request.GetPersonsRequest
import org.jellyfin.sdk.model.api.request.GetRecommendedProgramsRequest
import org.jellyfin.sdk.model.api.request.GetRecordingsRequest
@ -572,6 +576,7 @@ class HomeSettingsService
libraries: List<Library>,
limit: Int = prefs.maxItemsPerRow,
isRefresh: Boolean,
usePaging: Boolean = false,
): HomeRowLoadingState =
when (row) {
is HomeRowConfig.ContinueWatching -> {
@ -631,10 +636,11 @@ class HomeSettingsService
Success(
title = context.getString(R.string.continue_watching),
items =
latestNextUpService.buildCombined(
resume,
nextUp,
),
latestNextUpService
.buildCombined(
resume,
nextUp,
).take(limit),
viewOptions = row.viewOptions,
rowType = row,
)
@ -769,7 +775,7 @@ class HomeSettingsService
api.userLibraryApi
.getLatestMedia(request)
.content
.map { BaseItem.Companion.from(it, api, row.viewOptions.useSeries) }
.map { BaseItem(it, row.viewOptions.useSeries) }
.let {
Success(
title,
@ -799,18 +805,27 @@ class HomeSettingsService
fields = DefaultItemFields,
recursive = true,
)
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem.Companion.from(it, api, row.viewOptions.useSeries) }
.let {
Success(
title,
it,
row.viewOptions,
rowType = row,
)
}
if (usePaging) {
ApiRequestPager(
api,
request,
GetItemsRequestHandler,
scope,
useSeriesForPrimary = row.viewOptions.useSeries,
).init()
} else {
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem.from(it, api, row.viewOptions.useSeries) }
}.let {
Success(
title,
it,
row.viewOptions,
rowType = row,
)
}
}
is HomeRowConfig.ByParent -> {
@ -824,22 +839,29 @@ class HomeSettingsService
limit = limit,
fields = DefaultItemFields,
)
val name =
api.userLibraryApi
.getItem(itemId = row.parentId)
.content.name
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
.let {
Success(
name ?: context.getString(R.string.collection),
it,
row.viewOptions,
rowType = row,
)
}
val name = getItemName(row.parentId)
if (usePaging) {
ApiRequestPager(
api,
request,
GetItemsRequestHandler,
scope,
useSeriesForPrimary = row.viewOptions.useSeries,
).init()
} else {
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
}.let {
Success(
name ?: context.getString(R.string.collection),
it,
row.viewOptions,
rowType = row,
)
}
}
is HomeRowConfig.GetItems -> {
@ -856,18 +878,27 @@ class HomeSettingsService
)
}
}
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
.let {
Success(
row.name,
it,
row.viewOptions,
rowType = row,
)
}
if (usePaging) {
ApiRequestPager(
api,
request,
GetItemsRequestHandler,
scope,
useSeriesForPrimary = row.viewOptions.useSeries,
).init()
} else {
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
}.let {
Success(
row.name,
it,
row.viewOptions,
rowType = row,
)
}
}
is HomeRowConfig.Favorite -> {
@ -907,18 +938,27 @@ class HomeSettingsService
includeItemTypes = listOf(row.kind),
isFavorite = true,
)
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
.let {
Success(
title,
it,
row.viewOptions,
rowType = row,
)
}
if (usePaging) {
ApiRequestPager(
api,
request,
GetItemsRequestHandler,
scope,
useSeriesForPrimary = row.viewOptions.useSeries,
).init()
} else {
GetItemsRequestHandler
.execute(api, request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
}.let {
Success(
title,
it,
row.viewOptions,
rowType = row,
)
}
}
}
@ -931,19 +971,29 @@ class HomeSettingsService
limit = limit,
enableImages = true,
enableUserData = true,
enableTotalRecordCount = false,
)
api.liveTvApi
.getRecordings(request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
.let {
Success(
context.getString(R.string.active_recordings),
it,
row.viewOptions,
rowType = row,
)
}
if (usePaging) {
ApiRequestPager(
api,
request,
GetRecordingsRequestHandler,
scope,
useSeriesForPrimary = row.viewOptions.useSeries,
).init()
} else {
api.liveTvApi
.getRecordings(request)
.content.items
.map { BaseItem(it, row.viewOptions.useSeries) }
}.let {
Success(
context.getString(R.string.active_recordings),
it,
row.viewOptions,
rowType = row,
)
}
}
is HomeRowConfig.TvPrograms -> {
@ -957,6 +1007,7 @@ class HomeSettingsService
enableImageTypes = listOf(ImageType.PRIMARY, ImageType.LOGO),
imageTypeLimit = 1,
)
// paging not supported
api.liveTvApi
.getRecommendedPrograms(request)
.content.items
@ -972,21 +1023,33 @@ class HomeSettingsService
}
is HomeRowConfig.TvChannels -> {
api.liveTvApi
.getLiveTvChannels(
val request =
GetLiveTvChannelsRequest(
userId = userDto.id,
fields = DefaultItemFields,
limit = limit,
enableImages = true,
).toBaseItems(api, row.viewOptions.useSeries)
.let {
Success(
context.getString(R.string.channels),
it,
row.viewOptions,
rowType = row,
)
}
)
if (usePaging) {
ApiRequestPager(
api,
request,
GetLiveTvChannelsRequestHandler,
scope,
useSeriesForPrimary = row.viewOptions.useSeries,
).init()
} else {
api.liveTvApi
.getLiveTvChannels(request)
.toBaseItems(api, row.viewOptions.useSeries)
}.let {
Success(
context.getString(R.string.channels),
it,
row.viewOptions,
rowType = row,
)
}
}
is HomeRowConfig.Suggestions -> {

View file

@ -70,13 +70,14 @@ class LatestNextUpService
remove(BaseItemKind.EPISODE)
}
},
enableTotalRecordCount = false,
)
val items =
api.itemsApi
.getResumeItems(request)
.content
.items
.map { BaseItem.from(it, api, useSeriesForPrimary) }
.map { BaseItem(it, useSeriesForPrimary) }
return items
}
@ -105,13 +106,14 @@ class LatestNextUpService
enableUserData = true,
enableRewatching = enableRewatching,
nextUpDateCutoff = nextUpDateCutoff,
enableTotalRecordCount = false,
)
val nextUp =
api.tvShowsApi
.getNextUp(request)
.content
.items
.map { BaseItem.from(it, api, useSeriesForPrimary) }
.map { BaseItem(it, useSeriesForPrimary) }
.filter {
val seriesId = it.data.seriesId
if (seriesId != null && seriesId in removedSeries) {