diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt new file mode 100644 index 00000000..bd593571 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/CollectionFolderLiveTv.kt @@ -0,0 +1,157 @@ +package com.github.damontecres.wholphin.ui.detail + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.focusRestorer +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.unit.dp +import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Tab +import androidx.tv.material3.TabDefaults +import androidx.tv.material3.TabRow +import androidx.tv.material3.TabRowDefaults +import androidx.tv.material3.Text +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.preferences.UserPreferences +import com.github.damontecres.wholphin.preferences.rememberTab +import com.github.damontecres.wholphin.ui.components.ErrorMessage +import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid +import com.github.damontecres.wholphin.ui.ifElse +import com.github.damontecres.wholphin.ui.nav.Destination +import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel +import com.github.damontecres.wholphin.ui.tryRequestFocus + +@Composable +fun CollectionFolderLiveTv( + preferences: UserPreferences, + destination: Destination.MediaItem, + modifier: Modifier = Modifier, + preferencesViewModel: PreferencesViewModel = hiltViewModel(), +) { + val uiPrefs = preferences.appPreferences.interfacePreferences + val rememberedTabIndex = + if (uiPrefs.rememberSelectedTab) { + uiPrefs.getRememberedTabsOrDefault(destination.itemId.toString(), 0) + } else { + 0 + } + + val tabs = listOf("Guide", "DVR Schedule") + var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } + var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } + val focusRequester = remember { FocusRequester() } + + val firstTabFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { firstTabFocusRequester.tryRequestFocus() } + + if (uiPrefs.rememberSelectedTab) { + LaunchedEffect(selectedTabIndex) { + preferencesViewModel.preferenceDataStore.updateData { + preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex) + } + } + } + val onClickItem = { item: BaseItem -> + preferencesViewModel.navigationManager.navigateTo(item.destination()) + } + + var showHeader by rememberSaveable { mutableStateOf(true) } + + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + Column( + modifier = modifier, + ) { + AnimatedVisibility( + showHeader, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + TabRow( + selectedTabIndex = focusTabIndex, + modifier = + Modifier + .padding(start = 32.dp, top = 16.dp, bottom = 16.dp) + .focusRestorer(firstTabFocusRequester) + .onFocusChanged { + if (!it.isFocused) { + focusTabIndex = selectedTabIndex + } + }, + indicator = + @Composable { tabPositions, doesTabRowHaveFocus -> + tabPositions.getOrNull(focusTabIndex)?.let { currentTabPosition -> +// TabRowDefaults.PillIndicator( +// currentTabPosition = currentTabPosition, +// doesTabRowHaveFocus = doesTabRowHaveFocus, +// ) + TabRowDefaults.UnderlinedIndicator( + currentTabPosition = currentTabPosition, + doesTabRowHaveFocus = doesTabRowHaveFocus, + activeColor = MaterialTheme.colorScheme.border, + ) + } + }, + tabs = { + tabs.forEachIndexed { index, title -> + Tab( + selected = focusTabIndex == index, + onClick = { selectedTabIndex = index }, + onFocus = { focusTabIndex = index }, + colors = + TabDefaults.pillIndicatorTabColors( + focusedContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = + Modifier + .padding(8.dp) + .ifElse( + index == selectedTabIndex, + Modifier.focusRequester(firstTabFocusRequester), + ), + ) + } + } + }, + ) + } + when (selectedTabIndex) { + 0 -> { + TvGuideGrid( + true, + Modifier + .fillMaxSize() + .focusRequester(focusRequester), + ) + } + 1 -> { + Text( + text = "Not yet implemented", + ) + } + + else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt index 0968ac01..aa3a5252 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/LiveTvViewModel.kt @@ -3,9 +3,13 @@ package com.github.damontecres.wholphin.ui.detail.livetv import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode import com.github.damontecres.wholphin.ui.isNotNullOrBlank +import com.github.damontecres.wholphin.ui.launchIO import com.github.damontecres.wholphin.ui.nav.NavigationManager +import com.github.damontecres.wholphin.ui.setValueOnMain +import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.GetProgramsDtoHandler import com.github.damontecres.wholphin.util.LoadingExceptionHandler import com.github.damontecres.wholphin.util.LoadingState @@ -16,6 +20,7 @@ import kotlinx.coroutines.withContext import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.imageApi import org.jellyfin.sdk.api.client.extensions.liveTvApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.GetProgramsDto import org.jellyfin.sdk.model.api.ImageType import org.jellyfin.sdk.model.api.ItemSortBy @@ -47,6 +52,9 @@ class LiveTvViewModel val programs = MutableLiveData>() val programsByChannel = MutableLiveData>>(mapOf()) + val fetchingItem = MutableLiveData(LoadingState.Pending) + val fetchedItem = MutableLiveData(null) + private val programMap = mutableMapOf>>() fun init() { @@ -153,59 +161,55 @@ class LiveTvViewModel } } -// fun getPrograms(channelId: UUID): MutableLiveData> = -// programMap.getOrPut(channelId) { -// val data = MutableLiveData>(listOf()) -// viewModelScope.launch(Dispatchers.IO + ExceptionHandler()) { -// val request = -// GetProgramsDto( -// minStartDate = start, -// channelIds = listOf(channelId), -// ) -// val pager = ApiRequestPager(api, request, GetProgramsDtoHandler, viewModelScope).init() -// val programList = -// if (pager.isEmpty()) { -// object : AbstractList() { -// override fun get(index: Int): TvProgram? { -// val start = start.plusHours(index.toLong()) -// val end = start.plusHours(1L) -// return TvProgram( -// id = UUID.randomUUID(), -// channelId = channelId, -// start = start, -// end = start, -// duration = 60.minutes, -// name = "Unknown", -// subtitle = null, -// seasonEpisode = null, // TODO -// ) -// } -// -// override val size: Int -// get() = Int.MAX_VALUE -// } -// } else { -// LazyList(pager) { item -> -// item?.data?.let { dto -> -// TvProgram( -// id = dto.id, -// channelId = channelId, -// start = dto.startDate!!, -// end = dto.endDate!!, -// duration = dto.runTimeTicks!!.ticks, -// name = dto.seriesName ?: dto.name, -// subtitle = dto.name.takeIf { dto.seriesName.isNullOrBlank() }, -// seasonEpisode = null, // TODO -// ) -// } -// } -// } -// withContext(Dispatchers.Main) { -// data.value = programList -// } -// } -// data -// } + fun getItem(itemId: UUID) { + fetchingItem.value = LoadingState.Loading + viewModelScope.launchIO(LoadingExceptionHandler(fetchingItem, "Error")) { + val result = + api.userLibraryApi + .getItem(itemId = itemId) + .content + .let { BaseItem.from(it, api) } + withContext(Dispatchers.Main) { + fetchedItem.value = result + fetchingItem.value = LoadingState.Success + } + } + } + + fun cancelRecording( + series: Boolean, + timerId: String?, + ) { + if (timerId != null) { + viewModelScope.launch(ExceptionHandler(autoToast = true)) { + if (series) { + api.liveTvApi.cancelSeriesTimer(timerId) + } else { + api.liveTvApi.cancelTimer(timerId) + } + } + } + } + + suspend fun refreshProgram( + index: Int, + timerId: String?, + seriesTimerId: String?, + ) { + val newProgram = + programs.value?.getOrNull(index)?.copy( + isRecording = timerId.isNotNullOrBlank(), + isSeriesRecording = seriesTimerId.isNotNullOrBlank(), + ) + if (newProgram != null) { + programs.value + ?.toMutableList() + ?.apply { set(index, newProgram) } + ?.let { + programs.setValueOnMain(it) + } + } + } } /** diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt similarity index 67% rename from app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt rename to app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt index b7411798..ac26523c 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGrid.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/livetv/TvGuideGrid.kt @@ -16,12 +16,15 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType @@ -29,8 +32,11 @@ import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel +import androidx.tv.material3.Button import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Surface import androidx.tv.material3.SurfaceDefaults @@ -38,13 +44,18 @@ import androidx.tv.material3.Text import androidx.tv.material3.contentColorFor import androidx.tv.material3.surfaceColorAtElevation import coil3.compose.AsyncImage +import com.github.damontecres.wholphin.data.model.BaseItem +import com.github.damontecres.wholphin.ui.components.CircularProgress import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.enableMarquee +import com.github.damontecres.wholphin.ui.isNotNullOrBlank import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.rememberInt +import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.util.ExceptionHandler import com.github.damontecres.wholphin.util.LoadingState +import com.github.damontecres.wholphin.util.seasonEpisode import com.google.common.cache.CacheBuilder import com.google.common.cache.CacheLoader import eu.wewox.programguide.ProgramGuide @@ -57,7 +68,8 @@ import java.time.LocalDateTime import java.util.UUID @Composable -fun TvGrid( +fun TvGuideGrid( + requestFocusAfterLoading: Boolean, modifier: Modifier = Modifier, viewModel: LiveTvViewModel = hiltViewModel(), ) { @@ -69,43 +81,193 @@ fun TvGrid( val programs by viewModel.programs.observeAsState(listOf()) val programsByChannel by viewModel.programsByChannel.observeAsState(mapOf()) when (val state = loading) { - is LoadingState.Error -> ErrorMessage(state) + is LoadingState.Error -> ErrorMessage(state, modifier) LoadingState.Loading, LoadingState.Pending, - -> LoadingPage() + -> LoadingPage(modifier) - LoadingState.Success -> + LoadingState.Success -> { + val fetchedItem by viewModel.fetchedItem.observeAsState(null) + val loadingItem by viewModel.fetchingItem.observeAsState(LoadingState.Pending) + var showItemDialog by remember { mutableStateOf(null) } + val focusRequester = remember { FocusRequester() } + if (requestFocusAfterLoading) { + LaunchedEffect(Unit) { + focusRequester.tryRequestFocus() + } + } Column(modifier = modifier) { - TvGrid( + TvGuideGrid( channels = channels, programList = programs, programs = programsByChannel, start = viewModel.start, - onClick = { program -> - viewModel.navigationManager.navigateTo( - Destination.Playback( - itemId = program.channelId, - positionMs = 0L, - ), - ) + onClick = { index, program -> + viewModel.getItem(program.id) + showItemDialog = index }, modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.surface), + .background(MaterialTheme.colorScheme.surface) + .focusRequester(focusRequester), ) } + if (showItemDialog != null) { + val onDismissRequest = { showItemDialog = null } + ProgramDialog( + item = fetchedItem, + loading = loadingItem, + onDismissRequest = onDismissRequest, + onWatch = { + onDismissRequest.invoke() + fetchedItem?.data?.channelId?.let { + viewModel.navigationManager.navigateTo( + Destination.Playback( + itemId = it, + positionMs = 0L, + ), + ) + } + }, + onRecord = { series -> + onDismissRequest.invoke() + }, + onCancelRecord = { series -> + onDismissRequest.invoke() + fetchedItem?.data?.let { + viewModel.cancelRecording( + series, + if (series) it.seriesTimerId else it.timerId, + ) + } + }, + ) + } + } } } @Composable -fun TvGrid( +fun ProgramDialog( + item: BaseItem?, + loading: LoadingState, + onDismissRequest: () -> Unit, + onWatch: () -> Unit, + onRecord: (series: Boolean) -> Unit, + onCancelRecord: (series: Boolean) -> Unit, +) { + Dialog( + onDismissRequest = onDismissRequest, + ) { + Box( + modifier = + Modifier.background( + MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp), + shape = RoundedCornerShape(16.dp), + ), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .padding(16.dp), + ) { + when (val st = loading) { + is LoadingState.Error -> ErrorMessage(st) + LoadingState.Loading, + LoadingState.Pending, + -> + CircularProgress( + Modifier + .padding(8.dp) + .size(48.dp), + ) + + LoadingState.Success -> + item?.let { item -> + val now = LocalDateTime.now() + val dto = item.data + val isRecording = dto.timerId.isNotNullOrBlank() + val isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank() + Text( + text = item.name ?: "", + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleLarge, + ) + if (dto.isSeries ?: false) { + listOfNotNull(dto.seasonEpisode, dto.episodeTitle) + .joinToString(" - ") + .ifBlank { null } + ?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.titleMedium, + ) + } + } + dto.overview?.let { overview -> + Text( + text = overview, + color = MaterialTheme.colorScheme.onSurface, + style = MaterialTheme.typography.bodyMedium, + overflow = TextOverflow.Ellipsis, + maxLines = 3, + ) + } + if (dto.isSeries ?: false) { + Button( + onClick = { + if (isSeriesRecording) { + onCancelRecord.invoke(true) + } else { + onRecord.invoke(true) + } + }, + ) { + Text( + text = if (isSeriesRecording) "Cancel Series Recording" else "Record Series", + ) + } + } + Button( + onClick = { + if (isRecording) { + onCancelRecord.invoke(false) + } else { + onRecord.invoke(false) + } + }, + ) { + Text( + text = if (isRecording) "Cancel Recording" else "Record Program", + ) + } + if (now.isAfter(dto.startDate!!) && now.isBefore(dto.endDate!!)) { + Button( + onClick = onWatch, + ) { + Text( + text = "Watch live", + ) + } + } + } + } + } + } + } +} + +@Composable +fun TvGuideGrid( channels: List, programList: List, programs: Map>, start: LocalDateTime, - onClick: (TvProgram) -> Unit, + onClick: (Int, TvProgram) -> Unit, modifier: Modifier = Modifier, ) { val focusManager = LocalFocusManager.current @@ -224,7 +386,7 @@ fun TvGrid( Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> { val program = programList[focusedProgramIndex] Timber.v("Clicked on %s", program) - onClick.invoke(program) + onClick.invoke(focusedProgramIndex, program) null } @@ -255,7 +417,7 @@ fun TvGrid( }, ) { Surface( - colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary), + colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary.copy(alpha = .5f)), modifier = Modifier, ) { // Empty @@ -293,7 +455,7 @@ fun TvGrid( if (focused) { MaterialTheme.colorScheme.inverseSurface } else { - MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp) } val textColor = MaterialTheme.colorScheme.contentColorFor(background) Box( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt index 18d85518..89f2e203 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/Destination.kt @@ -85,9 +85,6 @@ sealed class Destination( val recursive: Boolean, ) : Destination(false) - @Serializable - data object LiveTvGuide : Destination(false) - @Serializable data object UpdateApp : Destination(true) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt index 3c1144fa..f5732751 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/DestinationContent.kt @@ -6,12 +6,12 @@ import androidx.tv.material3.Text import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.ui.components.LicenseInfo import com.github.damontecres.wholphin.ui.detail.CollectionFolderGeneric +import com.github.damontecres.wholphin.ui.detail.CollectionFolderLiveTv import com.github.damontecres.wholphin.ui.detail.CollectionFolderMovie import com.github.damontecres.wholphin.ui.detail.CollectionFolderTv import com.github.damontecres.wholphin.ui.detail.DebugPage import com.github.damontecres.wholphin.ui.detail.PlaylistDetails import com.github.damontecres.wholphin.ui.detail.SeriesDetails -import com.github.damontecres.wholphin.ui.detail.livetv.TvGrid import com.github.damontecres.wholphin.ui.detail.movie.MovieDetails import com.github.damontecres.wholphin.ui.detail.series.SeriesOverview import com.github.damontecres.wholphin.ui.main.HomePage @@ -144,13 +144,23 @@ fun DestinationContent( ) BaseItemKind.USER_VIEW -> - CollectionFolderGeneric( - preferences, - destination.itemId, - destination.item, - true, - modifier, - ) + when (destination.item?.data?.collectionType) { + CollectionType.LIVETV -> + CollectionFolderLiveTv( + preferences, + destination, + modifier, + ) + + else -> + CollectionFolderGeneric( + preferences, + destination.itemId, + destination.item, + true, + modifier, + ) + } else -> { Text("Unsupported item type: ${destination.type}") @@ -167,11 +177,6 @@ fun DestinationContent( modifier = modifier, ) - Destination.LiveTvGuide -> - TvGrid( - modifier = modifier, - ) - Destination.UpdateApp -> InstallUpdatePage(preferences, modifier) Destination.License -> LicenseInfo(modifier) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt index 5695518a..14559c34 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/nav/NavDrawer.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AccountCircle -import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings @@ -213,24 +212,6 @@ fun NavDrawer( ), ) } - - // TODO placeholder - item { - IconNavItem( - text = "Live TV", - icon = Icons.Default.Build, - selected = selectedIndex == -3, - onClick = { - viewModel.setIndex(-3) - viewModel.navigationManager.navigateToFromDrawer(Destination.LiveTvGuide) - }, - modifier = - Modifier.ifElse( - selectedIndex == -2, - Modifier.focusRequester(focusRequester), - ), - ) - } itemsIndexed(libraries) { index, it -> LibraryNavItem( library = it, diff --git a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt index ffdfc4c6..e22dc66e 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/util/Constants.kt @@ -35,4 +35,5 @@ val supportedCollectionTypes = CollectionType.HOMEVIDEOS, CollectionType.PLAYLISTS, CollectionType.BOXSETS, + CollectionType.LIVETV, )