Test implementation using programguide

This commit is contained in:
Damontecres 2025-10-20 11:57:12 -04:00
parent 5eb534ba77
commit 24763d6d8c
No known key found for this signature in database
3 changed files with 181 additions and 17 deletions

View file

@ -228,6 +228,7 @@ dependencies {
implementation(libs.timber) implementation(libs.timber)
implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.core)
implementation(libs.aboutlibraries.compose.m3) implementation(libs.aboutlibraries.compose.m3)
implementation(libs.programguide)
androidTestImplementation(platform(libs.androidx.compose.bom)) androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.compose.ui.test.junit4) androidTestImplementation(libs.androidx.compose.ui.test.junit4)

View file

@ -19,7 +19,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusProperties
@ -30,6 +32,7 @@ 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
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type import androidx.compose.ui.input.key.type
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times import androidx.compose.ui.unit.times
@ -38,16 +41,162 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.ui.ifElse
import com.github.damontecres.wholphin.ui.playback.isDown import com.github.damontecres.wholphin.ui.playback.isDown
import com.github.damontecres.wholphin.ui.playback.isUp import com.github.damontecres.wholphin.ui.playback.isUp
import com.github.damontecres.wholphin.ui.rememberInt
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.ExceptionHandler
import eu.wewox.programguide.ProgramGuide
import eu.wewox.programguide.ProgramGuideDimensions
import eu.wewox.programguide.ProgramGuideItem
import eu.wewox.programguide.rememberSaveableProgramGuideState
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.UUID import java.util.UUID
import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.minutes
fun translate(
channelIndex: Int,
programIndex: Int,
) {
val before =
channels.subList(0, channelIndex + 1).map { it.id }.sumOf { programs[it]?.size ?: 0 }
programIndex - before
}
fun programsBeforeChannel(channelIndex: Int): Int = channels.subList(0, channelIndex).map { it.id }.sumOf { programs[it]?.size ?: 0 }
@Composable @Composable
fun TvGrid(modifier: Modifier = Modifier) = TvGridBox(channels, programs, modifier) fun TvGrid(modifier: Modifier = Modifier) {
val state = rememberSaveableProgramGuideState()
val scope = rememberCoroutineScope()
val startHours = 6
val timeline = 16..24
var focusedProgram by rememberInt(0)
var focusedChannel by rememberInt(0)
val dimensions =
ProgramGuideDimensions(
timelineHourWidth = 240.dp,
timelineHeight = 32.dp,
channelWidth = 64.dp,
channelHeight = 64.dp,
currentTimeWidth = 2.dp,
)
ProgramGuide(
state = state,
dimensions = dimensions,
modifier =
modifier
.focusable()
.onPreviewKeyEvent {
if (it.type == KeyEventType.KeyUp) {
return@onPreviewKeyEvent false
}
val newIndex =
when (it.key) {
Key.DirectionRight -> focusedProgram + 1
Key.DirectionLeft -> focusedProgram - 1
Key.DirectionUp -> {
val start = programList[focusedProgram].start.hours
focusedChannel = (focusedChannel - 1).coerceAtLeast(0)
val channelId = channels[focusedChannel].id
val pIndex =
programs[channelId]?.indexOfFirst { start in (it.start.hours..<it.end.hours) }
?: -1
if (pIndex >= 0) {
programsBeforeChannel(focusedChannel) + pIndex
} else {
programsBeforeChannel(focusedChannel) + programs[channelId]!!.size
}
}
Key.DirectionDown -> {
val start = programList[focusedProgram].start.hours
focusedChannel =
(focusedChannel + 1).coerceAtMost(channels.size - 1)
val channelId = channels[focusedChannel].id
val pro = programs[channelId]!!
val pIndex =
pro.indexOfFirst { start in (it.start.hours..<it.end.hours) }
?: -1
if (pIndex >= 0) {
programsBeforeChannel(focusedChannel) + pIndex
} else {
programsBeforeChannel(focusedChannel) + programs[channelId]!!.size
}
}
else -> {
null
}
}
if (newIndex != null) {
val before = programsBeforeChannel(focusedChannel)
val max =
before + channels[focusedChannel].let { programs[it.id]!!.size } - 1
val index = newIndex.coerceIn(before, max)
scope.launch(ExceptionHandler()) {
state.animateToProgram(index, Alignment.Center)
focusedProgram = index
}
return@onPreviewKeyEvent true
}
return@onPreviewKeyEvent false
},
) {
guideStartHour = timeline.first.toFloat()
timeline(
count = timeline.count(),
layoutInfo = {
val start = timeline.toList()[it].toFloat()
ProgramGuideItem.Timeline(
startHour = start,
endHour = start + 1f,
)
},
) { index ->
val start = timeline.toList()[index].toFloat()
Text(
text = "$start o'clock",
)
}
programs(
count = programList.size,
layoutInfo = { programIndex ->
val program = programList[programIndex]
val channelIndex = channels.indexOfFirst { it.id == program.channelId }
ProgramGuideItem.Program(channelIndex, program.start.hours, program.end.hours)
},
) { programIndex ->
val program = programList[programIndex]
Text(
text = program.name ?: program.id.toString(),
modifier =
Modifier.ifElse(
programIndex == focusedProgram,
Modifier.background(MaterialTheme.colorScheme.error),
),
)
}
channels(
count = channels.size,
layoutInfo = { channelIndex ->
ProgramGuideItem.Channel(channelIndex)
},
) { channelIndex ->
Text(
text = channels[channelIndex].name ?: channelIndex.toString(),
modifier = Modifier.background(MaterialTheme.colorScheme.background),
)
}
}
}
@Composable @Composable
fun TvGridBox( fun TvGridBox(
@ -294,7 +443,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0), end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #1", name = "C1 Program #1",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -304,7 +453,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0), start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0), end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #2", name = "C1 Program #2",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -314,7 +463,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0), start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #3", name = "C1 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -324,7 +473,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 20, 0, 0, 0), start = LocalDateTime.of(2025, 10, 16, 20, 0, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 21, 0, 0), end = LocalDateTime.of(2025, 10, 16, 21, 0, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #3", name = "C1 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -334,7 +483,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0), start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 22, 0, 0), end = LocalDateTime.of(2025, 10, 16, 22, 0, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #3", name = "C1 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -344,7 +493,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 22, 0, 0, 0), start = LocalDateTime.of(2025, 10, 16, 22, 0, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 23, 0, 0), end = LocalDateTime.of(2025, 10, 16, 23, 0, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #3", name = "C1 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -357,7 +506,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 18, 30, 0), end = LocalDateTime.of(2025, 10, 16, 18, 30, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #1", name = "C2 Program #1",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -367,7 +516,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 30, 0), start = LocalDateTime.of(2025, 10, 16, 18, 30, 0),
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0), end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #2", name = "C2 Program #2",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -377,7 +526,17 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0), start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #3", name = "C2 Program #3",
subtitle = null,
seasonEpisode = null,
),
TvProgram(
id = UUID.randomUUID(),
channelId = channel2Id,
start = LocalDateTime.of(2025, 10, 16, 21, 0, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 22, 0, 0),
duration = 60.minutes,
name = "C2 Program #4",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -390,7 +549,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 18, 15, 0), end = LocalDateTime.of(2025, 10, 16, 18, 15, 0),
duration = 15.minutes, duration = 15.minutes,
name = "Program #1", name = "C3 Program #1",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -400,7 +559,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 15, 0), start = LocalDateTime.of(2025, 10, 16, 18, 15, 0),
end = LocalDateTime.of(2025, 10, 16, 18, 45, 0), end = LocalDateTime.of(2025, 10, 16, 18, 45, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #2", name = "C3 Program #2",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -410,7 +569,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 45, 0), start = LocalDateTime.of(2025, 10, 16, 18, 45, 0),
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0), end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
duration = 15.minutes, duration = 15.minutes,
name = "Program #3", name = "C3 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -420,7 +579,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0), start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #3", name = "C3 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -433,7 +592,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 18, 0, 0), start = LocalDateTime.of(2025, 10, 16, 18, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 19, 0, 0), end = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
duration = 60.minutes, duration = 60.minutes,
name = "Program #1", name = "C4 Program #1",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -443,7 +602,7 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 19, 0, 0), start = LocalDateTime.of(2025, 10, 16, 19, 0, 0),
end = LocalDateTime.of(2025, 10, 16, 19, 30, 0), end = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #2", name = "C4 Program #2",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
@ -453,9 +612,11 @@ val programs =
start = LocalDateTime.of(2025, 10, 16, 19, 30, 0), start = LocalDateTime.of(2025, 10, 16, 19, 30, 0),
end = LocalDateTime.of(2025, 10, 16, 20, 0, 0), end = LocalDateTime.of(2025, 10, 16, 20, 0, 0),
duration = 30.minutes, duration = 30.minutes,
name = "Program #3", name = "C4 Program #3",
subtitle = null, subtitle = null,
seasonEpisode = null, seasonEpisode = null,
), ),
), ),
) )
val programList = programs.values.flatten()

View file

@ -9,6 +9,7 @@ coreKtx = "1.17.0"
appcompat = "1.7.1" appcompat = "1.7.1"
composeBom = "2025.10.00" composeBom = "2025.10.00"
compose-runtime = "1.9.3" compose-runtime = "1.9.3"
programguide = "1.6.0"
timber = "5.0.1" timber = "5.0.1"
tvFoundation = "1.0.0-alpha12" tvFoundation = "1.0.0-alpha12"
tvMaterial = "1.0.1" tvMaterial = "1.0.1"
@ -56,6 +57,7 @@ androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "d
desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" } hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" }
programguide = { module = "io.github.oleksandrbalan:programguide", version.ref = "programguide" }
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-javalite" } protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf-javalite" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines-android" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" } kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }