mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
Mark recordings
This commit is contained in:
parent
753acb7b38
commit
fb4e36168c
2 changed files with 96 additions and 19 deletions
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisode
|
||||||
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
import com.github.damontecres.wholphin.ui.nav.NavigationManager
|
||||||
import com.github.damontecres.wholphin.util.GetProgramsDtoHandler
|
import com.github.damontecres.wholphin.util.GetProgramsDtoHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
import com.github.damontecres.wholphin.util.LoadingExceptionHandler
|
||||||
|
|
@ -40,7 +41,7 @@ class LiveTvViewModel
|
||||||
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
val loading = MutableLiveData<LoadingState>(LoadingState.Pending)
|
||||||
|
|
||||||
val start =
|
val start =
|
||||||
LocalDateTime.now().minusMinutes(30).truncatedTo(ChronoUnit.HOURS)
|
LocalDateTime.now().truncatedTo(ChronoUnit.HOURS)
|
||||||
|
|
||||||
val channels = MutableLiveData<List<TvChannel>>()
|
val channels = MutableLiveData<List<TvChannel>>()
|
||||||
val programs = MutableLiveData<List<TvProgram>>()
|
val programs = MutableLiveData<List<TvProgram>>()
|
||||||
|
|
@ -91,14 +92,8 @@ class LiveTvViewModel
|
||||||
channelId = dto.channelId!!,
|
channelId = dto.channelId!!,
|
||||||
start = dto.startDate!!,
|
start = dto.startDate!!,
|
||||||
end = dto.endDate!!,
|
end = dto.endDate!!,
|
||||||
startHours =
|
startHours = hoursBetween(start, dto.startDate!!),
|
||||||
java.time.Duration
|
endHours = hoursBetween(start, dto.endDate!!),
|
||||||
.between(start, dto.startDate!!)
|
|
||||||
.seconds / (60f * 60f),
|
|
||||||
endHours =
|
|
||||||
java.time.Duration
|
|
||||||
.between(start, dto.endDate!!)
|
|
||||||
.seconds / (60f * 60f),
|
|
||||||
duration = dto.runTimeTicks!!.ticks,
|
duration = dto.runTimeTicks!!.ticks,
|
||||||
name = dto.seriesName ?: dto.name,
|
name = dto.seriesName ?: dto.name,
|
||||||
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
subtitle = dto.episodeTitle.takeIf { dto.isSeries ?: false },
|
||||||
|
|
@ -108,6 +103,8 @@ class LiveTvViewModel
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
},
|
},
|
||||||
|
isRecording = dto.timerId.isNotNullOrBlank(),
|
||||||
|
isSeriesRecording = dto.seriesTimerId.isNotNullOrBlank(),
|
||||||
)
|
)
|
||||||
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
|
}.filter { it.startHours >= 0 && it.endHours >= 0 } // TODO shouldn't need to filter client side
|
||||||
|
|
||||||
|
|
@ -130,6 +127,8 @@ class LiveTvViewModel
|
||||||
name = "No data",
|
name = "No data",
|
||||||
subtitle = null,
|
subtitle = null,
|
||||||
seasonEpisode = null,
|
seasonEpisode = null,
|
||||||
|
isRecording = false,
|
||||||
|
isSeriesRecording = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
put(channel.id, fakePrograms)
|
put(channel.id, fakePrograms)
|
||||||
|
|
@ -209,6 +208,17 @@ class LiveTvViewModel
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of hours between two [LocalDateTime]
|
||||||
|
*/
|
||||||
|
fun hoursBetween(
|
||||||
|
start: LocalDateTime,
|
||||||
|
target: LocalDateTime,
|
||||||
|
): Float =
|
||||||
|
java.time.Duration
|
||||||
|
.between(start, target)
|
||||||
|
.seconds / (60f * 60f)
|
||||||
|
|
||||||
data class TvChannel(
|
data class TvChannel(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val number: String?,
|
val number: String?,
|
||||||
|
|
@ -227,4 +237,6 @@ data class TvProgram(
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val subtitle: String?,
|
val subtitle: String?,
|
||||||
val seasonEpisode: SeasonEpisode?,
|
val seasonEpisode: SeasonEpisode?,
|
||||||
|
val isRecording: Boolean,
|
||||||
|
val isSeriesRecording: Boolean,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,20 @@ import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
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.livedata.observeAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusDirection
|
import androidx.compose.ui.focus.FocusDirection
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.key.Key
|
import androidx.compose.ui.input.key.Key
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.key
|
import androidx.compose.ui.input.key.key
|
||||||
|
|
@ -28,6 +32,8 @@ import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
import androidx.tv.material3.Surface
|
||||||
|
import androidx.tv.material3.SurfaceDefaults
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import androidx.tv.material3.contentColorFor
|
import androidx.tv.material3.contentColorFor
|
||||||
import androidx.tv.material3.surfaceColorAtElevation
|
import androidx.tv.material3.surfaceColorAtElevation
|
||||||
|
|
@ -39,6 +45,8 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.rememberInt
|
import com.github.damontecres.wholphin.ui.rememberInt
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.LoadingState
|
import com.github.damontecres.wholphin.util.LoadingState
|
||||||
|
import com.google.common.cache.CacheBuilder
|
||||||
|
import com.google.common.cache.CacheLoader
|
||||||
import eu.wewox.programguide.ProgramGuide
|
import eu.wewox.programguide.ProgramGuide
|
||||||
import eu.wewox.programguide.ProgramGuideDimensions
|
import eu.wewox.programguide.ProgramGuideDimensions
|
||||||
import eu.wewox.programguide.ProgramGuideItem
|
import eu.wewox.programguide.ProgramGuideItem
|
||||||
|
|
@ -103,7 +111,6 @@ fun TvGrid(
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val state = rememberSaveableProgramGuideState()
|
val state = rememberSaveableProgramGuideState()
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// val timeline = 0..<24
|
|
||||||
var focusedProgramIndex by rememberInt(0)
|
var focusedProgramIndex by rememberInt(0)
|
||||||
var focusedChannelIndex by rememberInt(0)
|
var focusedChannelIndex by rememberInt(0)
|
||||||
|
|
||||||
|
|
@ -116,7 +123,23 @@ fun TvGrid(
|
||||||
currentTimeWidth = 2.dp,
|
currentTimeWidth = 2.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 }
|
val programsBeforeChannel =
|
||||||
|
remember {
|
||||||
|
CacheBuilder
|
||||||
|
.newBuilder()
|
||||||
|
.maximumSize(200)
|
||||||
|
.build<Int, Int>(
|
||||||
|
object : CacheLoader<Int, Int>() {
|
||||||
|
override fun load(key: Int): Int =
|
||||||
|
channels
|
||||||
|
.subList(0, key)
|
||||||
|
.map { it.id }
|
||||||
|
.sumOf { programs[it]?.size ?: 0 }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 }
|
||||||
|
|
||||||
ProgramGuide(
|
ProgramGuide(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -132,7 +155,7 @@ fun TvGrid(
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
Key.DirectionRight -> {
|
Key.DirectionRight -> {
|
||||||
val nextProgramIndex = focusedProgramIndex + 1
|
val nextProgramIndex = focusedProgramIndex + 1
|
||||||
val programsBefore = programsBeforeChannel(focusedChannelIndex)
|
val programsBefore = programsBeforeChannel.get(focusedChannelIndex)
|
||||||
val relativePosition = nextProgramIndex - programsBefore
|
val relativePosition = nextProgramIndex - programsBefore
|
||||||
val channelPrograms =
|
val channelPrograms =
|
||||||
programs[channels[focusedChannelIndex].id].orEmpty()
|
programs[channels[focusedChannelIndex].id].orEmpty()
|
||||||
|
|
@ -147,7 +170,8 @@ fun TvGrid(
|
||||||
Key.DirectionLeft -> {
|
Key.DirectionLeft -> {
|
||||||
val nextProgramIndex = focusedProgramIndex - 1
|
val nextProgramIndex = focusedProgramIndex - 1
|
||||||
if (nextProgramIndex >= 0) {
|
if (nextProgramIndex >= 0) {
|
||||||
val programsBefore = programsBeforeChannel(focusedChannelIndex)
|
val programsBefore =
|
||||||
|
programsBeforeChannel.get(focusedChannelIndex)
|
||||||
val relativePosition = nextProgramIndex - programsBefore
|
val relativePosition = nextProgramIndex - programsBefore
|
||||||
// val channelPrograms =
|
// val channelPrograms =
|
||||||
// programs[channels[focusedChannel].id].orEmpty()
|
// programs[channels[focusedChannel].id].orEmpty()
|
||||||
|
|
@ -175,9 +199,9 @@ fun TvGrid(
|
||||||
programs[channelId]?.indexOfFirst { start in (it.startHours..<it.endHours) }
|
programs[channelId]?.indexOfFirst { start in (it.startHours..<it.endHours) }
|
||||||
?: -1
|
?: -1
|
||||||
if (pIndex >= 0) {
|
if (pIndex >= 0) {
|
||||||
programsBeforeChannel(focusedChannelIndex) + pIndex
|
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
||||||
} else {
|
} else {
|
||||||
programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size
|
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -191,9 +215,9 @@ fun TvGrid(
|
||||||
val pIndex =
|
val pIndex =
|
||||||
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
|
pro.indexOfFirst { start in (it.startHours..<it.endHours) }
|
||||||
if (pIndex >= 0) {
|
if (pIndex >= 0) {
|
||||||
programsBeforeChannel(focusedChannelIndex) + pIndex
|
programsBeforeChannel.get(focusedChannelIndex) + pIndex
|
||||||
} else {
|
} else {
|
||||||
programsBeforeChannel(focusedChannelIndex) + programs[channelId]!!.size
|
programsBeforeChannel.get(focusedChannelIndex) + programs[channelId]!!.size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,13 +233,13 @@ fun TvGrid(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newIndex != null) {
|
if (newIndex != null) {
|
||||||
val before = programsBeforeChannel(focusedChannelIndex)
|
val before = programsBeforeChannel.get(focusedChannelIndex)
|
||||||
val max =
|
val max =
|
||||||
before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1
|
before + channels[focusedChannelIndex].let { programs[it.id]!!.size } - 1
|
||||||
val index = newIndex.coerceIn(before, max)
|
val index = newIndex.coerceIn(before, max)
|
||||||
scope.launch(ExceptionHandler()) {
|
scope.launch(ExceptionHandler()) {
|
||||||
state.animateToProgram(index, Alignment.Center)
|
|
||||||
focusedProgramIndex = index
|
focusedProgramIndex = index
|
||||||
|
state.animateToProgram(index, Alignment.Center)
|
||||||
}
|
}
|
||||||
return@onPreviewKeyEvent true
|
return@onPreviewKeyEvent true
|
||||||
}
|
}
|
||||||
|
|
@ -223,6 +247,20 @@ fun TvGrid(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
guideStartHour = 0f
|
guideStartHour = 0f
|
||||||
|
currentTime(
|
||||||
|
layoutInfo = {
|
||||||
|
ProgramGuideItem.CurrentTime(
|
||||||
|
hoursBetween(start, LocalDateTime.now()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
colors = SurfaceDefaults.colors(MaterialTheme.colorScheme.tertiary),
|
||||||
|
modifier = Modifier,
|
||||||
|
) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
}
|
||||||
timeline(
|
timeline(
|
||||||
count = MAX_HOURS.toInt(),
|
count = MAX_HOURS.toInt(),
|
||||||
layoutInfo = { index ->
|
layoutInfo = { index ->
|
||||||
|
|
@ -295,6 +333,33 @@ fun TvGrid(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (program.isSeriesRecording) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.background(Color.Red.copy(alpha = .5f), shape = CircleShape)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 4.dp, top = 4.dp, bottom = 4.dp, end = 10.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.background(Color.Red, shape = CircleShape)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
} else if (program.isRecording) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.background(Color.Red, shape = CircleShape)
|
||||||
|
.align(Alignment.BottomEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue