mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Add dvr schedule page
This commit is contained in:
parent
132e4f46b3
commit
a908f0d6e5
3 changed files with 267 additions and 3 deletions
|
|
@ -33,6 +33,7 @@ import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
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.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.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.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -146,8 +147,11 @@ fun CollectionFolderLiveTv(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
1 -> {
|
1 -> {
|
||||||
Text(
|
DvrSchedule(
|
||||||
text = "Not yet implemented",
|
true,
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,255 @@
|
||||||
|
package com.github.damontecres.wholphin.ui.detail.livetv
|
||||||
|
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
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.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.tv.material3.ListItem
|
||||||
|
import androidx.tv.material3.ListItemDefaults
|
||||||
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Text
|
||||||
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
|
import com.github.damontecres.wholphin.ui.SlimItemFields
|
||||||
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import com.github.damontecres.wholphin.util.seasonEpisode
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.liveTvApi
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
import java.time.ZoneId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DvrScheduleViewModel
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
private val api: ApiClient,
|
||||||
|
) : ViewModel() {
|
||||||
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
val active = MutableLiveData<List<BaseItem>>()
|
||||||
|
val scheduled = MutableLiveData<Map<LocalDate, List<BaseItem>>>()
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
loading.value = LoadingState.Loading
|
||||||
|
viewModelScope.launchIO(LoadingExceptionHandler(loading, "Error fetching DVR Schedule")) {
|
||||||
|
val active =
|
||||||
|
api.liveTvApi
|
||||||
|
.getRecordings(
|
||||||
|
isInProgress = true,
|
||||||
|
fields = SlimItemFields,
|
||||||
|
limit = 100,
|
||||||
|
).content.items
|
||||||
|
.map { BaseItem.from(it, api, true) }
|
||||||
|
val scheduled =
|
||||||
|
api.liveTvApi
|
||||||
|
.getTimers(
|
||||||
|
isActive = false,
|
||||||
|
isScheduled = true,
|
||||||
|
).content.items
|
||||||
|
.map { BaseItem.from(it.programInfo!!, api, true) } // TODO this probably breaks for time based recordings
|
||||||
|
.groupBy {
|
||||||
|
it.data.startDate!!.toLocalDate()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
this@DvrScheduleViewModel.active.value = active
|
||||||
|
this@DvrScheduleViewModel.scheduled.value = scheduled
|
||||||
|
loading.value = LoadingState.Success
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DvrSchedule(
|
||||||
|
requestFocusAfterLoading: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: DvrScheduleViewModel = hiltViewModel(),
|
||||||
|
) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
viewModel.init()
|
||||||
|
}
|
||||||
|
val loading by viewModel.loading.observeAsState(LoadingState.Pending)
|
||||||
|
val active by viewModel.active.observeAsState(listOf())
|
||||||
|
val recordings by viewModel.scheduled.observeAsState(mapOf())
|
||||||
|
when (val state = loading) {
|
||||||
|
is LoadingState.Error -> ErrorMessage(state, modifier)
|
||||||
|
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Pending,
|
||||||
|
-> LoadingPage(modifier)
|
||||||
|
|
||||||
|
LoadingState.Success -> {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
if (requestFocusAfterLoading) {
|
||||||
|
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
|
||||||
|
}
|
||||||
|
DvrScheduleContent(
|
||||||
|
activeRecordings = active,
|
||||||
|
scheduledRecordings = recordings,
|
||||||
|
onClickItem = {},
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DvrScheduleContent(
|
||||||
|
activeRecordings: List<BaseItem>,
|
||||||
|
scheduledRecordings: Map<LocalDate, List<BaseItem>>,
|
||||||
|
onClickItem: (BaseItem) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.TopCenter,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.6f)
|
||||||
|
.background(MaterialTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
if (activeRecordings.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = "Active",
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(activeRecordings) { item ->
|
||||||
|
Recording(
|
||||||
|
item = item,
|
||||||
|
onClick = { onClickItem.invoke(item) },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scheduledRecordings.keys.sorted().forEach { date ->
|
||||||
|
val formattedDate =
|
||||||
|
DateUtils.formatDateTime(
|
||||||
|
context,
|
||||||
|
date
|
||||||
|
.atStartOfDay(ZoneId.systemDefault())
|
||||||
|
.toInstant()
|
||||||
|
.epochSecond * 1000,
|
||||||
|
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_DATE,
|
||||||
|
)
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = formattedDate,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val programs = scheduledRecordings[date].orEmpty()
|
||||||
|
items(programs) { item ->
|
||||||
|
Recording(
|
||||||
|
item = item,
|
||||||
|
onClick = { onClickItem.invoke(item) },
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Recording(
|
||||||
|
item: BaseItem,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
ListItem(
|
||||||
|
selected = false,
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
colors =
|
||||||
|
ListItemDefaults.colors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),
|
||||||
|
),
|
||||||
|
leadingContent = {
|
||||||
|
// TODO
|
||||||
|
// AsyncImage(
|
||||||
|
// model = item.imageUrl,
|
||||||
|
// contentDescription = null,
|
||||||
|
// )
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = item.title ?: "",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
if (item.data.isSeries ?: false) {
|
||||||
|
listOfNotNull(
|
||||||
|
item.data.seasonEpisode,
|
||||||
|
item.data.episodeTitle,
|
||||||
|
).joinToString(" - ")
|
||||||
|
.ifBlank { null }
|
||||||
|
?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
val time =
|
||||||
|
DateUtils.formatDateRange(
|
||||||
|
context,
|
||||||
|
item.data.startDate!!
|
||||||
|
.toInstant(OffsetDateTime.now().offset)
|
||||||
|
.epochSecond * 1000,
|
||||||
|
item.data.endDate!!
|
||||||
|
.toInstant(OffsetDateTime.now().offset)
|
||||||
|
.epochSecond * 1000,
|
||||||
|
DateUtils.FORMAT_SHOW_TIME,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = time,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -468,7 +468,12 @@ fun TvGuideGrid(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { index ->
|
) { index ->
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
) {
|
||||||
val differentDay =
|
val differentDay =
|
||||||
start.toLocalDate() !=
|
start.toLocalDate() !=
|
||||||
start
|
start
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue