Improved home page support for Live TV & Recordings (#890)

## Description
Improves home page customization for Live TV & Recordings
- Add row for live tv channels
- Add row for "on now" programs
- Clicking on a TV channel or TV program starts playback
- Better name for "Recently recorded" instead of "Recently added in
Recordings"

Also fixes a back stack issue after adding rows

### Related issues
Follow up from #803 

### Testing
Emulator

## Screenshots
<img width="663" height="190" alt="image"
src="https://github.com/user-attachments/assets/49d41953-42ab-4621-92f1-b7936aab7e97"
/>

## AI or LLM usage
None
This commit is contained in:
Ray 2026-02-13 11:41:47 -05:00 committed by GitHub
parent 7fcd66f407
commit 91900af7a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 229 additions and 64 deletions

View file

@ -12,13 +12,14 @@ import com.github.damontecres.wholphin.preferences.HomePagePreferences
import com.github.damontecres.wholphin.ui.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.getGenreImageMap
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
import com.github.damontecres.wholphin.ui.main.settings.Library
import com.github.damontecres.wholphin.ui.toBaseItems
import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.GetGenresRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetPersonsHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Error
import com.github.damontecres.wholphin.util.HomeRowLoadingState.Success
import com.github.damontecres.wholphin.util.supportedHomeCollectionTypes
import dagger.hilt.android.qualifiers.ApplicationContext
@ -43,6 +44,7 @@ import org.jellyfin.sdk.api.client.extensions.userLibraryApi
import org.jellyfin.sdk.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.api.CollectionType
import org.jellyfin.sdk.model.api.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder
@ -255,11 +257,12 @@ class HomeSettingsService
suspend fun createDefault(userId: UUID): HomePageResolvedSettings {
Timber.v("Creating default settings")
val user = serverRepository.currentUser.value?.takeIf { it.id == userId }
val userDto = serverRepository.currentUserDto.value?.takeIf { it.id == userId }
val libraries =
if (user != null) {
navDrawerService.getFilteredUserLibraries(user)
navDrawerService.getFilteredUserLibraries(user, userDto?.tvAccess ?: false)
} else {
navDrawerService.getAllUserLibraries(userId)
navDrawerService.getAllUserLibraries(userId, userDto?.tvAccess ?: false)
}
val prefs =
@ -269,18 +272,23 @@ class HomeSettingsService
libraries
.mapIndexed { index, it ->
val parentId = it.itemId
val name = it.name.takeIf { it.isNotNullOrBlank() }
val title =
name?.let { context.getString(R.string.recently_added_in, it) }
?: context.getString(R.string.recently_added)
HomeRowConfigDisplay(
id = index,
title = title,
config = HomeRowConfig.RecentlyAdded(parentId),
)
val title = getRecentlyAddedTitle(context, it)
if (it.collectionType == CollectionType.LIVETV) {
HomeRowConfigDisplay(
id = index,
title = context.getString(R.string.live_tv),
config = HomeRowConfig.TvPrograms(),
)
} else {
HomeRowConfigDisplay(
id = index,
title = title,
config = HomeRowConfig.RecentlyAdded(parentId),
)
}
}
val continueWatchingRows =
if (prefs.combineContinueNext) { // TODO
if (prefs.combineContinueNext) {
listOf(
HomeRowConfigDisplay(
id = includedIds.size + 1,
@ -363,7 +371,7 @@ class HomeSettingsService
}
HomeSectionType.LIVE_TV -> {
if (userDto.policy?.enableLiveTvAccess == true) {
if (userDto.tvAccess) {
HomeRowConfigDisplay(
id = id++,
title = context.getString(R.string.live_tv),
@ -523,6 +531,14 @@ class HomeSettingsService
)
}
is HomeRowConfig.TvChannels -> {
HomeRowConfigDisplay(
id = id,
title = context.getString(R.string.channels),
config,
)
}
is HomeRowConfig.Suggestions -> {
val name =
api.userLibraryApi
@ -654,13 +670,10 @@ class HomeSettingsService
}
is HomeRowConfig.RecentlyAdded -> {
val name =
val library =
libraries
.firstOrNull { it.itemId == row.parentId }
?.name
val title =
name?.let { context.getString(R.string.recently_added_in, it) }
?: context.getString(R.string.recently_added)
val title = getRecentlyAddedTitle(context, library)
val request =
GetLatestMediaRequest(
fields = SlimItemFields,
@ -862,6 +875,23 @@ class HomeSettingsService
}
}
is HomeRowConfig.TvChannels -> {
api.liveTvApi
.getLiveTvChannels(
userId = userDto.id,
fields = DefaultItemFields,
limit = limit,
enableImages = true,
).toBaseItems(api, row.viewOptions.useSeries)
.let {
Success(
context.getString(R.string.channels),
it,
row.viewOptions,
)
}
}
is HomeRowConfig.Suggestions -> {
val library =
api.userLibraryApi
@ -888,7 +918,7 @@ class HomeSettingsService
row.viewOptions,
)
} else {
HomeRowLoadingState.Error(
Error(
title,
message = "Unsupported type ${library.collectionType}",
)
@ -949,3 +979,14 @@ class UnsupportedHomeSettingsVersionException(
val unsupportedVersion: Int?,
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion")
fun getRecentlyAddedTitle(
context: Context,
library: Library?,
): String =
if (library?.isRecordingFolder == true) {
context.getString(R.string.recently_recorded)
} else {
library?.name?.let { context.getString(R.string.recently_added_in, it) }
?: context.getString(R.string.recently_added)
}

View file

@ -4,7 +4,6 @@ import android.content.Context
import androidx.lifecycle.asFlow
import com.github.damontecres.wholphin.data.ServerPreferencesDao
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
@ -68,25 +67,49 @@ class NavDrawerService
}.launchIn(coroutineScope)
}
suspend fun getAllUserLibraries(userId: UUID): List<Library> {
suspend fun getAllUserLibraries(
userId: UUID,
tvAccess: Boolean,
): List<Library> {
val userViews =
api.userViewsApi
.getUserViews(userId = userId)
.content.items
val recordingFolders =
if (tvAccess) {
api.liveTvApi
.getRecordingFolders(userId = userId)
.content.items
.map { it.id }
.toSet()
} else {
setOf()
}
val libraries =
userViews
.filter { it.collectionType in supportedCollectionTypes }
.map { Library(it.id, it.name ?: "", it.collectionType ?: CollectionType.UNKNOWN) }
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
.map {
Library(
itemId = it.id,
name = it.name ?: "",
type = it.type,
collectionType = it.collectionType ?: CollectionType.UNKNOWN,
isRecordingFolder = it.id in recordingFolders,
)
}
return libraries
}
suspend fun getFilteredUserLibraries(user: JellyfinUser): List<Library> {
suspend fun getFilteredUserLibraries(
user: JellyfinUser,
tvAccess: Boolean,
): List<Library> {
val pins =
serverPreferencesDao
.getNavDrawerPinnedItems(user)
.associateBy { it.itemId }
val libraries =
getAllUserLibraries(user.id)
getAllUserLibraries(user.id, tvAccess)
.filterNot { pins[ServerNavDrawerItem.getId(it.itemId)]?.type == NavPinType.UNPINNED }
return libraries
}
@ -95,39 +118,26 @@ class NavDrawerService
user: JellyfinUser,
userDto: UserDto,
) {
val tvAccess = userDto.policy?.enableLiveTvAccess ?: false
val userViews =
api.userViewsApi
.getUserViews(userId = user.id)
.content.items
val recordingFolders =
if (tvAccess) {
api.liveTvApi
.getRecordingFolders(userId = user.id)
.content.items
.map { it.id }
.toSet()
} else {
setOf()
}
val builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
val allLibraries = getAllUserLibraries(user.id, userDto.tvAccess)
val libraries =
userViews
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
allLibraries
.map {
val destination =
if (it.id in recordingFolders) {
Destination.Recordings(it.id)
if (it.isRecordingFolder) {
Destination.Recordings(it.itemId)
} else {
BaseItem.from(it, api).destination()
Destination.MediaItem(
it.itemId,
it.type,
it.collectionType,
)
}
ServerNavDrawerItem(
itemId = it.id,
name = it.name ?: it.id.toString(),
itemId = it.itemId,
name = it.name,
destination = destination,
type = it.collectionType ?: CollectionType.UNKNOWN,
type = it.collectionType,
)
}
val allItems = builtins + libraries
@ -170,3 +180,5 @@ data class NavDrawerItemState(
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
}
}
val UserDto.tvAccess: Boolean get() = policy?.enableLiveTvAccess == true