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

@ -187,6 +187,25 @@ data class BaseItem(
) )
} }
BaseItemKind.TV_CHANNEL -> {
Destination.Playback(
itemId = id,
positionMs = 0L,
)
}
BaseItemKind.PROGRAM -> {
val channelId = data.channelId
if (channelId != null) {
Destination.Playback(
itemId = channelId,
positionMs = 0L,
)
} else {
Destination.MediaItem(this)
}
}
else -> { else -> {
Destination.MediaItem(this) Destination.MediaItem(this)
} }

View file

@ -111,7 +111,7 @@ sealed interface HomeRowConfig {
} }
/** /**
* * Currently recording
*/ */
@Serializable @Serializable
@SerialName("Recordings") @SerialName("Recordings")
@ -122,7 +122,7 @@ sealed interface HomeRowConfig {
} }
/** /**
* * Programs on now/recommended
*/ */
@Serializable @Serializable
@SerialName("TvPrograms") @SerialName("TvPrograms")
@ -132,6 +132,17 @@ sealed interface HomeRowConfig {
override fun updateViewOptions(viewOptions: HomeRowViewOptions): TvPrograms = this.copy(viewOptions = viewOptions) override fun updateViewOptions(viewOptions: HomeRowViewOptions): TvPrograms = this.copy(viewOptions = viewOptions)
} }
/**
* Live TV channels
*/
@Serializable
@SerialName("TvChannels")
data class TvChannels(
override val viewOptions: HomeRowViewOptions = HomeRowViewOptions(),
) : HomeRowConfig {
override fun updateViewOptions(viewOptions: HomeRowViewOptions): TvChannels = this.copy(viewOptions = viewOptions)
}
/** /**
* Fetch suggestions from [com.github.damontecres.wholphin.services.SuggestionService] for the given parent ID * Fetch suggestions from [com.github.damontecres.wholphin.services.SuggestionService] for the given parent ID
*/ */
@ -217,5 +228,11 @@ data class HomeRowViewOptions(
heightDp = Cards.HEIGHT_EPISODE, heightDp = Cards.HEIGHT_EPISODE,
aspectRatio = AspectRatio.WIDE, aspectRatio = AspectRatio.WIDE,
) )
val channelsDefault =
HomeRowViewOptions(
heightDp = 96,
aspectRatio = AspectRatio.WIDE,
)
} }
} }

View file

@ -660,6 +660,7 @@ sealed interface AppPreference<Pref, T> {
AppDestinationPreference<AppPreferences>( AppDestinationPreference<AppPreferences>(
title = R.string.customize_home, title = R.string.customize_home,
destination = Destination.HomeSettings, destination = Destination.HomeSettings,
summary = R.string.customize_home_summary,
) )
val SendCrashReports = val SendCrashReports =

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.DefaultItemFields
import com.github.damontecres.wholphin.ui.SlimItemFields import com.github.damontecres.wholphin.ui.SlimItemFields
import com.github.damontecres.wholphin.ui.components.getGenreImageMap 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.main.settings.Library
import com.github.damontecres.wholphin.ui.toBaseItems
import com.github.damontecres.wholphin.ui.toServerString import com.github.damontecres.wholphin.ui.toServerString
import com.github.damontecres.wholphin.util.GetGenresRequestHandler import com.github.damontecres.wholphin.util.GetGenresRequestHandler
import com.github.damontecres.wholphin.util.GetItemsRequestHandler import com.github.damontecres.wholphin.util.GetItemsRequestHandler
import com.github.damontecres.wholphin.util.GetPersonsHandler import com.github.damontecres.wholphin.util.GetPersonsHandler
import com.github.damontecres.wholphin.util.HomeRowLoadingState 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.HomeRowLoadingState.Success
import com.github.damontecres.wholphin.util.supportedHomeCollectionTypes import com.github.damontecres.wholphin.util.supportedHomeCollectionTypes
import dagger.hilt.android.qualifiers.ApplicationContext 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.api.client.extensions.userViewsApi
import org.jellyfin.sdk.model.UUID import org.jellyfin.sdk.model.UUID
import org.jellyfin.sdk.model.api.BaseItemKind 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.ImageType
import org.jellyfin.sdk.model.api.ItemSortBy import org.jellyfin.sdk.model.api.ItemSortBy
import org.jellyfin.sdk.model.api.SortOrder import org.jellyfin.sdk.model.api.SortOrder
@ -255,11 +257,12 @@ class HomeSettingsService
suspend fun createDefault(userId: UUID): HomePageResolvedSettings { suspend fun createDefault(userId: UUID): HomePageResolvedSettings {
Timber.v("Creating default settings") Timber.v("Creating default settings")
val user = serverRepository.currentUser.value?.takeIf { it.id == userId } val user = serverRepository.currentUser.value?.takeIf { it.id == userId }
val userDto = serverRepository.currentUserDto.value?.takeIf { it.id == userId }
val libraries = val libraries =
if (user != null) { if (user != null) {
navDrawerService.getFilteredUserLibraries(user) navDrawerService.getFilteredUserLibraries(user, userDto?.tvAccess ?: false)
} else { } else {
navDrawerService.getAllUserLibraries(userId) navDrawerService.getAllUserLibraries(userId, userDto?.tvAccess ?: false)
} }
val prefs = val prefs =
@ -269,18 +272,23 @@ class HomeSettingsService
libraries libraries
.mapIndexed { index, it -> .mapIndexed { index, it ->
val parentId = it.itemId val parentId = it.itemId
val name = it.name.takeIf { it.isNotNullOrBlank() } val title = getRecentlyAddedTitle(context, it)
val title = if (it.collectionType == CollectionType.LIVETV) {
name?.let { context.getString(R.string.recently_added_in, it) } HomeRowConfigDisplay(
?: context.getString(R.string.recently_added) id = index,
title = context.getString(R.string.live_tv),
config = HomeRowConfig.TvPrograms(),
)
} else {
HomeRowConfigDisplay( HomeRowConfigDisplay(
id = index, id = index,
title = title, title = title,
config = HomeRowConfig.RecentlyAdded(parentId), config = HomeRowConfig.RecentlyAdded(parentId),
) )
} }
}
val continueWatchingRows = val continueWatchingRows =
if (prefs.combineContinueNext) { // TODO if (prefs.combineContinueNext) {
listOf( listOf(
HomeRowConfigDisplay( HomeRowConfigDisplay(
id = includedIds.size + 1, id = includedIds.size + 1,
@ -363,7 +371,7 @@ class HomeSettingsService
} }
HomeSectionType.LIVE_TV -> { HomeSectionType.LIVE_TV -> {
if (userDto.policy?.enableLiveTvAccess == true) { if (userDto.tvAccess) {
HomeRowConfigDisplay( HomeRowConfigDisplay(
id = id++, id = id++,
title = context.getString(R.string.live_tv), 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 -> { is HomeRowConfig.Suggestions -> {
val name = val name =
api.userLibraryApi api.userLibraryApi
@ -654,13 +670,10 @@ class HomeSettingsService
} }
is HomeRowConfig.RecentlyAdded -> { is HomeRowConfig.RecentlyAdded -> {
val name = val library =
libraries libraries
.firstOrNull { it.itemId == row.parentId } .firstOrNull { it.itemId == row.parentId }
?.name val title = getRecentlyAddedTitle(context, library)
val title =
name?.let { context.getString(R.string.recently_added_in, it) }
?: context.getString(R.string.recently_added)
val request = val request =
GetLatestMediaRequest( GetLatestMediaRequest(
fields = SlimItemFields, 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 -> { is HomeRowConfig.Suggestions -> {
val library = val library =
api.userLibraryApi api.userLibraryApi
@ -888,7 +918,7 @@ class HomeSettingsService
row.viewOptions, row.viewOptions,
) )
} else { } else {
HomeRowLoadingState.Error( Error(
title, title,
message = "Unsupported type ${library.collectionType}", message = "Unsupported type ${library.collectionType}",
) )
@ -949,3 +979,14 @@ class UnsupportedHomeSettingsVersionException(
val unsupportedVersion: Int?, val unsupportedVersion: Int?,
val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION, val maxSupportedVersion: Int = SUPPORTED_HOME_PAGE_SETTINGS_VERSION,
) : Exception("Unsupported version $unsupportedVersion, max supported is $maxSupportedVersion") ) : 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 androidx.lifecycle.asFlow
import com.github.damontecres.wholphin.data.ServerPreferencesDao import com.github.damontecres.wholphin.data.ServerPreferencesDao
import com.github.damontecres.wholphin.data.ServerRepository 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.JellyfinUser
import com.github.damontecres.wholphin.data.model.NavPinType import com.github.damontecres.wholphin.data.model.NavPinType
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
@ -68,25 +67,49 @@ class NavDrawerService
}.launchIn(coroutineScope) }.launchIn(coroutineScope)
} }
suspend fun getAllUserLibraries(userId: UUID): List<Library> { suspend fun getAllUserLibraries(
userId: UUID,
tvAccess: Boolean,
): List<Library> {
val userViews = val userViews =
api.userViewsApi api.userViewsApi
.getUserViews(userId = userId) .getUserViews(userId = userId)
.content.items .content.items
val recordingFolders =
if (tvAccess) {
api.liveTvApi
.getRecordingFolders(userId = userId)
.content.items
.map { it.id }
.toSet()
} else {
setOf()
}
val libraries = val libraries =
userViews userViews
.filter { it.collectionType in supportedCollectionTypes } .filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
.map { Library(it.id, it.name ?: "", it.collectionType ?: CollectionType.UNKNOWN) } .map {
Library(
itemId = it.id,
name = it.name ?: "",
type = it.type,
collectionType = it.collectionType ?: CollectionType.UNKNOWN,
isRecordingFolder = it.id in recordingFolders,
)
}
return libraries return libraries
} }
suspend fun getFilteredUserLibraries(user: JellyfinUser): List<Library> { suspend fun getFilteredUserLibraries(
user: JellyfinUser,
tvAccess: Boolean,
): List<Library> {
val pins = val pins =
serverPreferencesDao serverPreferencesDao
.getNavDrawerPinnedItems(user) .getNavDrawerPinnedItems(user)
.associateBy { it.itemId } .associateBy { it.itemId }
val libraries = val libraries =
getAllUserLibraries(user.id) getAllUserLibraries(user.id, tvAccess)
.filterNot { pins[ServerNavDrawerItem.getId(it.itemId)]?.type == NavPinType.UNPINNED } .filterNot { pins[ServerNavDrawerItem.getId(it.itemId)]?.type == NavPinType.UNPINNED }
return libraries return libraries
} }
@ -95,39 +118,26 @@ class NavDrawerService
user: JellyfinUser, user: JellyfinUser,
userDto: UserDto, 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 builtins = listOf(NavDrawerItem.Favorites, NavDrawerItem.Discover)
val allLibraries = getAllUserLibraries(user.id, userDto.tvAccess)
val libraries = val libraries =
userViews allLibraries
.filter { it.collectionType in supportedCollectionTypes || it.id in recordingFolders }
.map { .map {
val destination = val destination =
if (it.id in recordingFolders) { if (it.isRecordingFolder) {
Destination.Recordings(it.id) Destination.Recordings(it.itemId)
} else { } else {
BaseItem.from(it, api).destination() Destination.MediaItem(
it.itemId,
it.type,
it.collectionType,
)
} }
ServerNavDrawerItem( ServerNavDrawerItem(
itemId = it.id, itemId = it.itemId,
name = it.name ?: it.id.toString(), name = it.name,
destination = destination, destination = destination,
type = it.collectionType ?: CollectionType.UNKNOWN, type = it.collectionType,
) )
} }
val allItems = builtins + libraries val allItems = builtins + libraries
@ -170,3 +180,5 @@ data class NavDrawerItemState(
val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false) val EMPTY = NavDrawerItemState(emptyList(), emptyList(), false)
} }
} }
val UserDto.tvAccess: Boolean get() = policy?.enableLiveTvAccess == true

View file

@ -84,7 +84,7 @@ val PhotoItemFields =
object Cards { object Cards {
const val HEIGHT_2X3_DP = 172 const val HEIGHT_2X3_DP = 172
val height2x3 = HEIGHT_2X3_DP.dp val height2x3 = HEIGHT_2X3_DP.dp
val HEIGHT_EPISODE = (HEIGHT_2X3_DP * .75f).toInt().let { it - it % 4 } const val HEIGHT_EPISODE = 128
val heightEpisode = HEIGHT_EPISODE.dp val heightEpisode = HEIGHT_EPISODE.dp
val playedPercentHeight = 6.dp val playedPercentHeight = 6.dp
val serverUserCircle = height2x3 * .75f val serverUserCircle = height2x3 * .75f

View file

@ -15,6 +15,7 @@ import com.github.damontecres.wholphin.services.MediaReportService
import com.github.damontecres.wholphin.services.NavDrawerService import com.github.damontecres.wholphin.services.NavDrawerService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.tvAccess
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.ExceptionHandler
@ -68,7 +69,8 @@ class HomeViewModel
val prefs = preferences.appPreferences.homePagePreferences val prefs = preferences.appPreferences.homePagePreferences
serverRepository.currentUserDto.value?.let { userDto -> serverRepository.currentUserDto.value?.let { userDto ->
val libraries = navDrawerService.getAllUserLibraries(userDto.id) val libraries =
navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess)
val settings = val settings =
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY } homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
val state = state.value val state = state.value

View file

@ -23,6 +23,7 @@ import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.services.SuggestionsWorker import com.github.damontecres.wholphin.services.SuggestionsWorker
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import org.jellyfin.sdk.model.api.CollectionType
@Composable @Composable
fun HomeLibraryRowTypeList( fun HomeLibraryRowTypeList(
@ -63,11 +64,38 @@ fun HomeLibraryRowTypeList(
} }
fun getSupportedRowTypes(library: Library): List<LibraryRowType> { fun getSupportedRowTypes(library: Library): List<LibraryRowType> {
val itemKind = SuggestionsWorker.getTypeForCollection(library.collectionType) val supportsSuggestions = SuggestionsWorker.getTypeForCollection(library.collectionType) != null
return if (itemKind != null) { return when {
LibraryRowType.entries library.isRecordingFolder -> {
} else { listOf(
LibraryRowType.entries.toMutableList().apply { remove(LibraryRowType.SUGGESTIONS) } LibraryRowType.RECENTLY_RECORDED,
LibraryRowType.GENRES,
)
}
library.collectionType == CollectionType.LIVETV -> {
listOf(
LibraryRowType.TV_CHANNELS,
LibraryRowType.TV_PROGRAMS,
)
}
supportsSuggestions -> {
listOf(
LibraryRowType.RECENTLY_ADDED,
LibraryRowType.RECENTLY_RELEASED,
LibraryRowType.SUGGESTIONS,
LibraryRowType.GENRES,
)
}
else -> {
listOf(
LibraryRowType.RECENTLY_ADDED,
LibraryRowType.RECENTLY_RELEASED,
LibraryRowType.GENRES,
)
}
} }
} }
@ -78,4 +106,7 @@ enum class LibraryRowType(
RECENTLY_RELEASED(R.string.recently_released), RECENTLY_RELEASED(R.string.recently_released),
SUGGESTIONS(R.string.suggestions), SUGGESTIONS(R.string.suggestions),
GENRES(R.string.genres), GENRES(R.string.genres),
TV_CHANNELS(R.string.channels),
TV_PROGRAMS(R.string.live_tv),
RECENTLY_RECORDED(R.string.recently_recorded),
} }

View file

@ -61,7 +61,9 @@ fun HomeSettingsPage(
// Adds a row, waits until its done loading, then scrolls to the new row // Adds a row, waits until its done loading, then scrolls to the new row
fun addRow(func: () -> Job) { fun addRow(func: () -> Job) {
scope.launch(ExceptionHandler(autoToast = true)) { scope.launch(ExceptionHandler(autoToast = true)) {
backStack.add(HomeSettingsDestination.RowList) while (backStack.size > 1) {
backStack.removeAt(backStack.lastIndex)
}
func.invoke().join() func.invoke().join()
listState.animateScrollToItem(state.rows.lastIndex) listState.animateScrollToItem(state.rows.lastIndex)
} }

View file

@ -30,6 +30,7 @@ import com.github.damontecres.wholphin.services.SeerrServerRepository
import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException import com.github.damontecres.wholphin.services.UnsupportedHomeSettingsVersionException
import com.github.damontecres.wholphin.services.UserPreferencesService import com.github.damontecres.wholphin.services.UserPreferencesService
import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope import com.github.damontecres.wholphin.services.hilt.IoCoroutineScope
import com.github.damontecres.wholphin.services.tvAccess
import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.showToast import com.github.damontecres.wholphin.ui.showToast
import com.github.damontecres.wholphin.util.HomeRowLoadingState import com.github.damontecres.wholphin.util.HomeRowLoadingState
@ -83,8 +84,8 @@ class HomeSettingsViewModel
init { init {
addCloseable { saveToLocal() } addCloseable { saveToLocal() }
viewModelScope.launchIO { viewModelScope.launchIO {
val userId = serverRepository.currentUser.value?.id ?: return@launchIO val userDto = serverRepository.currentUserDto.value ?: return@launchIO
val libraries = navDrawerService.getAllUserLibraries(userId) val libraries = navDrawerService.getAllUserLibraries(userDto.id, userDto.tvAccess)
val currentSettings = val currentSettings =
homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY } homeSettingsService.currentSettings.first { it != HomePageResolvedSettings.EMPTY }
Timber.v("currentSettings=%s", currentSettings) Timber.v("currentSettings=%s", currentSettings)
@ -292,6 +293,36 @@ class HomeSettingsViewModel
config = Suggestions(library.itemId), config = Suggestions(library.itemId),
) )
} }
LibraryRowType.TV_CHANNELS -> {
val title = context.getString(R.string.channels)
HomeRowConfigDisplay(
id = id,
title = title,
config =
HomeRowConfig.TvChannels(
viewOptions = HomeRowViewOptions.channelsDefault,
),
)
}
LibraryRowType.TV_PROGRAMS -> {
val title = context.getString(R.string.watch_live)
HomeRowConfigDisplay(
id = id,
title = title,
config = HomeRowConfig.TvPrograms(),
)
}
LibraryRowType.RECENTLY_RECORDED -> {
val title = context.getString(R.string.recently_recorded)
HomeRowConfigDisplay(
id = id,
title = title,
config = RecentlyAdded(library.itemId),
)
}
} }
updateState { updateState {
it.copy( it.copy(
@ -614,6 +645,10 @@ class HomeSettingsViewModel
is HomeRowConfig.TvPrograms -> { is HomeRowConfig.TvPrograms -> {
it.config.updateViewOptions(preset.tvLibrary) it.config.updateViewOptions(preset.tvLibrary)
} }
is HomeRowConfig.TvChannels -> {
it.config
}
} }
it.copy(config = newConfig) it.copy(config = newConfig)
} }
@ -658,5 +693,7 @@ data class HomePageSettingsState(
data class Library( data class Library(
@Serializable(UUIDSerializer::class) val itemId: UUID, @Serializable(UUIDSerializer::class) val itemId: UUID,
val name: String, val name: String,
val type: BaseItemKind,
val collectionType: CollectionType, val collectionType: CollectionType,
val isRecordingFolder: Boolean,
) )

View file

@ -214,7 +214,7 @@ fun DestinationContent(
else -> { else -> {
Timber.w("Unsupported item type: ${destination.type}") Timber.w("Unsupported item type: ${destination.type}")
Text("Unsupported item type: ${destination.type}") Text("Unsupported item type: ${destination.type}", modifier)
} }
} }
} }

View file

@ -520,6 +520,7 @@
<string name="settings_saved">Settings saved</string> <string name="settings_saved">Settings saved</string>
<string name="display_presets">Display presets</string> <string name="display_presets">Display presets</string>
<string name="display_presets_description">Built-in presets to quickly style all rows</string> <string name="display_presets_description">Built-in presets to quickly style all rows</string>
<string name="customize_home_summary">Choose rows and images on the home page</string>
<string-array name="theme_song_volume"> <string-array name="theme_song_volume">
<item>Disabled</item> <item>Disabled</item>

View file

@ -74,6 +74,7 @@ class TestHomeRowSamples {
HomeRowConfig.Favorite(kind = BaseItemKind.SERIES), HomeRowConfig.Favorite(kind = BaseItemKind.SERIES),
HomeRowConfig.Recordings(), HomeRowConfig.Recordings(),
HomeRowConfig.TvPrograms(), HomeRowConfig.TvPrograms(),
HomeRowConfig.TvChannels(),
HomeRowConfig.Suggestions(parentId = UUID.randomUUID()), HomeRowConfig.Suggestions(parentId = UUID.randomUUID()),
) )
} }
@ -96,6 +97,7 @@ class TestHomeRowSamples {
is HomeRowConfig.Recordings -> foundTypes.add(it::class) is HomeRowConfig.Recordings -> foundTypes.add(it::class)
is HomeRowConfig.TvPrograms -> foundTypes.add(it::class) is HomeRowConfig.TvPrograms -> foundTypes.add(it::class)
is HomeRowConfig.Suggestions -> foundTypes.add(it::class) is HomeRowConfig.Suggestions -> foundTypes.add(it::class)
is HomeRowConfig.TvChannels -> foundTypes.add(it::class)
} }
} }
Assert.assertEquals(HomeRowConfig::class.sealedSubclasses.size, foundTypes.size) Assert.assertEquals(HomeRowConfig::class.sealedSubclasses.size, foundTypes.size)