Add recordings tab to live tv

This commit is contained in:
Damontecres 2025-10-21 15:30:22 -04:00
parent efeecc4b98
commit 80b766f469
No known key found for this signature in database
3 changed files with 91 additions and 8 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -22,30 +23,73 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.datastore.core.DataStore
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Tab import androidx.tv.material3.Tab
import androidx.tv.material3.TabDefaults import androidx.tv.material3.TabDefaults
import androidx.tv.material3.TabRow import androidx.tv.material3.TabRow
import androidx.tv.material3.TabRowDefaults import androidx.tv.material3.TabRowDefaults
import androidx.tv.material3.Text import androidx.tv.material3.Text
import com.github.damontecres.wholphin.data.ServerRepository
import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.data.model.BaseItem
import com.github.damontecres.wholphin.data.model.GetItemsFilter
import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.preferences.rememberTab import com.github.damontecres.wholphin.preferences.rememberTab
import com.github.damontecres.wholphin.ui.components.CollectionFolderGrid
import com.github.damontecres.wholphin.ui.components.ErrorMessage import com.github.damontecres.wholphin.ui.components.ErrorMessage
import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule import com.github.damontecres.wholphin.ui.detail.livetv.DvrSchedule
import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid import com.github.damontecres.wholphin.ui.detail.livetv.TvGuideGrid
import com.github.damontecres.wholphin.ui.ifElse import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.Destination import com.github.damontecres.wholphin.ui.nav.Destination
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel import com.github.damontecres.wholphin.ui.nav.NavigationManager
import com.github.damontecres.wholphin.ui.setValueOnMain
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import dagger.hilt.android.lifecycle.HiltViewModel
import org.jellyfin.sdk.api.client.ApiClient
import org.jellyfin.sdk.api.client.extensions.liveTvApi
import java.util.UUID
import javax.inject.Inject
@HiltViewModel
class LiveTvCollectionViewModel
@Inject
constructor(
val api: ApiClient,
val serverRepository: ServerRepository,
val navigationManager: NavigationManager,
val preferenceDataStore: DataStore<AppPreferences>,
) : ViewModel() {
val recordingFolders = MutableLiveData<List<TabId>>()
init {
viewModelScope.launchIO {
val folders =
api.liveTvApi
.getRecordingFolders(userId = serverRepository.currentUser?.id)
.content.items
.map { TabId(it.name ?: "Recordings", it.id) }
this@LiveTvCollectionViewModel.recordingFolders.setValueOnMain(folders)
}
}
}
data class TabId(
val title: String,
val id: UUID,
)
@Composable @Composable
fun CollectionFolderLiveTv( fun CollectionFolderLiveTv(
preferences: UserPreferences, preferences: UserPreferences,
destination: Destination.MediaItem, destination: Destination.MediaItem,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
preferencesViewModel: PreferencesViewModel = hiltViewModel(), viewModel: LiveTvCollectionViewModel = hiltViewModel(),
) { ) {
val uiPrefs = preferences.appPreferences.interfacePreferences val uiPrefs = preferences.appPreferences.interfacePreferences
val rememberedTabIndex = val rememberedTabIndex =
@ -54,8 +98,14 @@ fun CollectionFolderLiveTv(
} else { } else {
0 0
} }
val folders by viewModel.recordingFolders.observeAsState(listOf())
val tabs =
listOf(
TabId("Guide", UUID.randomUUID()),
TabId("DVR Schedule", UUID.randomUUID()),
) + folders
val tabs = listOf("Guide", "DVR Schedule")
var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var focusTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) } var selectedTabIndex by rememberSaveable { mutableIntStateOf(rememberedTabIndex) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -65,13 +115,13 @@ fun CollectionFolderLiveTv(
if (uiPrefs.rememberSelectedTab) { if (uiPrefs.rememberSelectedTab) {
LaunchedEffect(selectedTabIndex) { LaunchedEffect(selectedTabIndex) {
preferencesViewModel.preferenceDataStore.updateData { viewModel.preferenceDataStore.updateData {
preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex) preferences.appPreferences.rememberTab(destination.itemId, selectedTabIndex)
} }
} }
} }
val onClickItem = { item: BaseItem -> val onClickItem = { item: BaseItem ->
preferencesViewModel.navigationManager.navigateTo(item.destination()) viewModel.navigationManager.navigateTo(item.destination())
} }
var showHeader by rememberSaveable { mutableStateOf(true) } var showHeader by rememberSaveable { mutableStateOf(true) }
@ -111,7 +161,7 @@ fun CollectionFolderLiveTv(
} }
}, },
tabs = { tabs = {
tabs.forEachIndexed { index, title -> tabs.forEachIndexed { index, tabId ->
Tab( Tab(
selected = focusTabIndex == index, selected = focusTabIndex == index,
onClick = { selectedTabIndex = index }, onClick = { selectedTabIndex = index },
@ -122,7 +172,7 @@ fun CollectionFolderLiveTv(
), ),
) { ) {
Text( Text(
text = title, text = tabId.title,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = modifier =
Modifier Modifier
@ -155,7 +205,28 @@ fun CollectionFolderLiveTv(
) )
} }
else -> ErrorMessage("Invalid tab index $selectedTabIndex", null) else -> {
val folderIndex = selectedTabIndex - 2
if (folderIndex in folders.indices) {
CollectionFolderGrid(
preferences = preferences,
onClickItem = { viewModel.navigationManager.navigateTo(it.destination()) },
itemId = folders[folderIndex].id,
item = null,
initialFilter = GetItemsFilter(),
showTitle = showHeader,
recursive = false,
modifier =
Modifier
.padding(start = 16.dp),
positionCallback = { columns, position ->
showHeader = position < columns
},
)
} else {
ErrorMessage("Invalid tab index $selectedTabIndex", null)
}
}
} }
} }
} }

View file

@ -24,6 +24,7 @@ import com.github.damontecres.wholphin.ui.setup.SwitchUserContent
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.CollectionType
import org.jellyfin.sdk.model.api.DeviceProfile import org.jellyfin.sdk.model.api.DeviceProfile
import timber.log.Timber
/** /**
* Chose the page for the [Destination] * Chose the page for the [Destination]
@ -162,7 +163,17 @@ fun DestinationContent(
) )
} }
BaseItemKind.FOLDER ->
CollectionFolderGeneric(
preferences,
destination.itemId,
destination.item,
false,
modifier,
)
else -> { else -> {
Timber.w("Unsupported item type: ${destination.type}")
Text("Unsupported item type: ${destination.type}") Text("Unsupported item type: ${destination.type}")
} }
} }

View file

@ -19,6 +19,7 @@ val supportItemKinds =
BaseItemKind.VIDEO, BaseItemKind.VIDEO,
BaseItemKind.SEASON, BaseItemKind.SEASON,
BaseItemKind.COLLECTION_FOLDER, BaseItemKind.COLLECTION_FOLDER,
BaseItemKind.FOLDER,
BaseItemKind.USER_VIEW, BaseItemKind.USER_VIEW,
BaseItemKind.TRAILER, BaseItemKind.TRAILER,
BaseItemKind.TV_CHANNEL, BaseItemKind.TV_CHANNEL,