mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Wire up live tv pages
This commit is contained in:
parent
fb4e36168c
commit
dc6a921693
7 changed files with 413 additions and 106 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<List<TvProgram>>()
|
||||
val programsByChannel = MutableLiveData<Map<UUID, List<TvProgram>>>(mapOf())
|
||||
|
||||
val fetchingItem = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||
val fetchedItem = MutableLiveData<BaseItem?>(null)
|
||||
|
||||
private val programMap = mutableMapOf<UUID, MutableLiveData<List<TvProgram?>>>()
|
||||
|
||||
fun init() {
|
||||
|
|
@ -153,59 +161,55 @@ class LiveTvViewModel
|
|||
}
|
||||
}
|
||||
|
||||
// fun getPrograms(channelId: UUID): MutableLiveData<List<TvProgram?>> =
|
||||
// programMap.getOrPut(channelId) {
|
||||
// val data = MutableLiveData<List<TvProgram?>>(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<TvProgram?>() {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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<Int?>(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<TvChannel>,
|
||||
programList: List<TvProgram>,
|
||||
programs: Map<UUID, List<TvProgram>>,
|
||||
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(
|
||||
|
|
@ -85,9 +85,6 @@ sealed class Destination(
|
|||
val recursive: Boolean,
|
||||
) : Destination(false)
|
||||
|
||||
@Serializable
|
||||
data object LiveTvGuide : Destination(false)
|
||||
|
||||
@Serializable
|
||||
data object UpdateApp : Destination(true)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -35,4 +35,5 @@ val supportedCollectionTypes =
|
|||
CollectionType.HOMEVIDEOS,
|
||||
CollectionType.PLAYLISTS,
|
||||
CollectionType.BOXSETS,
|
||||
CollectionType.LIVETV,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue