Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a0fa37e55 |
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "dev.castarr.tv"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 32
|
||||
versionName = "0.10.6"
|
||||
versionCode = 33
|
||||
versionName = "0.11.0"
|
||||
}
|
||||
|
||||
// Release signing from environment (see ~/.keys/castarr-release.env on the
|
||||
|
||||
@@ -170,22 +170,31 @@ class AppState(
|
||||
SourceMode.DISPATCHARR -> dispatcharr.upcoming(channel, windowMs)
|
||||
}
|
||||
|
||||
/** One channel's club broadcasts: the next one plus how many follow. */
|
||||
data class TeamHit(
|
||||
val programme: dev.castarr.tv.data.Programme,
|
||||
val further: Int,
|
||||
)
|
||||
|
||||
/**
|
||||
* Channels showing the viewer's club within the scanned window, paired
|
||||
* with the programme that matched — earliest kick-off first, so whatever
|
||||
* is running right now sits on top.
|
||||
*/
|
||||
fun teamMatches(key: String): List<Pair<Channel, dev.castarr.tv.data.Programme>> {
|
||||
fun teamMatches(key: String): List<Pair<Channel, TeamHit>> {
|
||||
val filter = dev.castarr.tv.data.TeamFilters.byKey(key) ?: return emptyList()
|
||||
val window = dev.castarr.tv.data.TeamFilters.WINDOW_MS
|
||||
val now = System.currentTimeMillis()
|
||||
val window = dev.castarr.tv.data.TeamFilters.windowEnd(now) - now
|
||||
return activeChannels()
|
||||
.filter { dev.castarr.tv.data.TeamFilters.scansGroup(it.group) }
|
||||
.mapNotNull { channel ->
|
||||
upcoming(channel, window)
|
||||
.firstOrNull { !isEpgPlaceholder(it.title) && filter.matches(it.title) }
|
||||
?.let { channel to it }
|
||||
// A channel can carry several of the club's broadcasts in one
|
||||
// evening; the row names the next and counts the rest.
|
||||
val hits = upcoming(channel, window)
|
||||
.filter { !isEpgPlaceholder(it.title) && filter.matches(it.title) }
|
||||
hits.firstOrNull()?.let { channel to TeamHit(it, hits.size - 1) }
|
||||
}
|
||||
.sortedBy { it.second.start }
|
||||
.sortedBy { it.second.programme.start }
|
||||
}
|
||||
|
||||
fun refreshActive() {
|
||||
|
||||
@@ -29,8 +29,25 @@ data class TeamFilter(
|
||||
|
||||
object TeamFilters {
|
||||
|
||||
/** Window scanned ahead of now. */
|
||||
const val WINDOW_MS = 3 * 60 * 60 * 1000L
|
||||
/** Minimum window scanned ahead of now. */
|
||||
const val MIN_WINDOW_MS = 3 * 60 * 60 * 1000L
|
||||
|
||||
/**
|
||||
* Scans until midnight, but never less than [MIN_WINDOW_MS] — asking in
|
||||
* the morning should already answer "does my club play tonight", and
|
||||
* asking late at night must not cut the evening short.
|
||||
*/
|
||||
fun windowEnd(now: Long): Long {
|
||||
val midnight = java.util.Calendar.getInstance().apply {
|
||||
timeInMillis = now
|
||||
add(java.util.Calendar.DAY_OF_YEAR, 1)
|
||||
set(java.util.Calendar.HOUR_OF_DAY, 0)
|
||||
set(java.util.Calendar.MINUTE, 0)
|
||||
set(java.util.Calendar.SECOND, 0)
|
||||
set(java.util.Calendar.MILLISECOND, 0)
|
||||
}.timeInMillis
|
||||
return maxOf(midnight, now + MIN_WINDOW_MS)
|
||||
}
|
||||
|
||||
/** At most this many club groups sit in the rail at once. */
|
||||
const val MAX_ACTIVE = 3
|
||||
|
||||
@@ -246,7 +246,7 @@ fun LiveScreen(state: AppState) {
|
||||
Text(
|
||||
when {
|
||||
activeTeam != null ->
|
||||
"In den nächsten 3 Stunden läuft nichts mit ${activeTeam.fullName}."
|
||||
"Heute läuft nichts mehr mit ${activeTeam.fullName}."
|
||||
state.favoritesOnly ->
|
||||
"Noch keine Favoriten — halte OK auf einem Sender gedrückt."
|
||||
else -> "Diese Gruppe ist leer."
|
||||
@@ -351,7 +351,7 @@ private fun ChannelRow(
|
||||
number: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
nowNext: NowNext,
|
||||
highlight: dev.castarr.tv.data.Programme? = null,
|
||||
highlight: AppState.TeamHit? = null,
|
||||
playing: Boolean,
|
||||
favorite: Boolean,
|
||||
epgStamp: Long,
|
||||
@@ -510,9 +510,10 @@ private fun LogoInitials(initials: String) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The programme that matched the club filter, with its start time. */
|
||||
/** The next club broadcast on this channel, and how many follow it. */
|
||||
@Composable
|
||||
private fun HighlightCell(programme: dev.castarr.tv.data.Programme, modifier: Modifier = Modifier) {
|
||||
private fun HighlightCell(hit: AppState.TeamHit, modifier: Modifier = Modifier) {
|
||||
val programme = hit.programme
|
||||
val running = System.currentTimeMillis() in programme.start until programme.stop
|
||||
Column(modifier) {
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
@@ -536,7 +537,8 @@ private fun HighlightCell(programme: dev.castarr.tv.data.Programme, modifier: Mo
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
"${formatClock(programme.start)}–${formatClock(programme.stop)}",
|
||||
"${formatClock(programme.start)}–${formatClock(programme.stop)}" +
|
||||
if (hit.further > 0) " · +${hit.further} weitere" else "",
|
||||
color = CastarrColors.faint,
|
||||
fontFamily = AppFont,
|
||||
fontSize = 12.sp,
|
||||
|
||||
@@ -178,8 +178,8 @@ fun SettingsScreen(state: AppState) {
|
||||
|
||||
SettingsCard("Vereinsmenüs") {
|
||||
Text(
|
||||
"Eigene Gruppe mit allen Sendern, auf denen der Verein in den " +
|
||||
"nächsten 3 Stunden läuft. Bis zu " +
|
||||
"Eigene Gruppe mit allen Sendern, auf denen der Verein " +
|
||||
"heute noch läuft. Bis zu " +
|
||||
"${dev.castarr.tv.data.TeamFilters.MAX_ACTIVE} Vereine.",
|
||||
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 11.sp,
|
||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 6.dp),
|
||||
|
||||
@@ -51,6 +51,25 @@ class TeamFiltersTest {
|
||||
assertTrue(TeamFilters.all.all { c -> c.needles.all { it == it.lowercase() } })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `window reaches midnight but never falls below three hours`() {
|
||||
val cal = java.util.Calendar.getInstance()
|
||||
// Morning: the window must reach into the evening.
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 9)
|
||||
cal.set(java.util.Calendar.MINUTE, 0)
|
||||
val morning = cal.timeInMillis
|
||||
val eveningKickoff = morning + 12 * 60 * 60 * 1000L
|
||||
assertTrue(TeamFilters.windowEnd(morning) > eveningKickoff)
|
||||
|
||||
// Late at night: still at least three hours ahead.
|
||||
cal.set(java.util.Calendar.HOUR_OF_DAY, 23)
|
||||
cal.set(java.util.Calendar.MINUTE, 30)
|
||||
val lateNight = cal.timeInMillis
|
||||
assertTrue(
|
||||
TeamFilters.windowEnd(lateNight) >= lateNight + TeamFilters.MIN_WINDOW_MS
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `covers all three divisions`() {
|
||||
assertTrue(TeamFilters.all.size > 50)
|
||||
|
||||
Reference in New Issue
Block a user