Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a0fa37e55 |
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "dev.castarr.tv"
|
applicationId = "dev.castarr.tv"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 32
|
versionCode = 33
|
||||||
versionName = "0.10.6"
|
versionName = "0.11.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Release signing from environment (see ~/.keys/castarr-release.env on the
|
// Release signing from environment (see ~/.keys/castarr-release.env on the
|
||||||
|
|||||||
@@ -170,22 +170,31 @@ class AppState(
|
|||||||
SourceMode.DISPATCHARR -> dispatcharr.upcoming(channel, windowMs)
|
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
|
* Channels showing the viewer's club within the scanned window, paired
|
||||||
* with the programme that matched — earliest kick-off first, so whatever
|
* with the programme that matched — earliest kick-off first, so whatever
|
||||||
* is running right now sits on top.
|
* 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 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()
|
return activeChannels()
|
||||||
.filter { dev.castarr.tv.data.TeamFilters.scansGroup(it.group) }
|
.filter { dev.castarr.tv.data.TeamFilters.scansGroup(it.group) }
|
||||||
.mapNotNull { channel ->
|
.mapNotNull { channel ->
|
||||||
upcoming(channel, window)
|
// A channel can carry several of the club's broadcasts in one
|
||||||
.firstOrNull { !isEpgPlaceholder(it.title) && filter.matches(it.title) }
|
// evening; the row names the next and counts the rest.
|
||||||
?.let { channel to it }
|
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() {
|
fun refreshActive() {
|
||||||
|
|||||||
@@ -29,8 +29,25 @@ data class TeamFilter(
|
|||||||
|
|
||||||
object TeamFilters {
|
object TeamFilters {
|
||||||
|
|
||||||
/** Window scanned ahead of now. */
|
/** Minimum window scanned ahead of now. */
|
||||||
const val WINDOW_MS = 3 * 60 * 60 * 1000L
|
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. */
|
/** At most this many club groups sit in the rail at once. */
|
||||||
const val MAX_ACTIVE = 3
|
const val MAX_ACTIVE = 3
|
||||||
|
|||||||
@@ -246,7 +246,7 @@ fun LiveScreen(state: AppState) {
|
|||||||
Text(
|
Text(
|
||||||
when {
|
when {
|
||||||
activeTeam != null ->
|
activeTeam != null ->
|
||||||
"In den nächsten 3 Stunden läuft nichts mit ${activeTeam.fullName}."
|
"Heute läuft nichts mehr mit ${activeTeam.fullName}."
|
||||||
state.favoritesOnly ->
|
state.favoritesOnly ->
|
||||||
"Noch keine Favoriten — halte OK auf einem Sender gedrückt."
|
"Noch keine Favoriten — halte OK auf einem Sender gedrückt."
|
||||||
else -> "Diese Gruppe ist leer."
|
else -> "Diese Gruppe ist leer."
|
||||||
@@ -351,7 +351,7 @@ private fun ChannelRow(
|
|||||||
number: Int,
|
number: Int,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
nowNext: NowNext,
|
nowNext: NowNext,
|
||||||
highlight: dev.castarr.tv.data.Programme? = null,
|
highlight: AppState.TeamHit? = null,
|
||||||
playing: Boolean,
|
playing: Boolean,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
epgStamp: Long,
|
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
|
@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
|
val running = System.currentTimeMillis() in programme.start until programme.stop
|
||||||
Column(modifier) {
|
Column(modifier) {
|
||||||
Row(verticalAlignment = Alignment.Bottom) {
|
Row(verticalAlignment = Alignment.Bottom) {
|
||||||
@@ -536,7 +537,8 @@ private fun HighlightCell(programme: dev.castarr.tv.data.Programme, modifier: Mo
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(6.dp))
|
Spacer(Modifier.height(6.dp))
|
||||||
Text(
|
Text(
|
||||||
"${formatClock(programme.start)}–${formatClock(programme.stop)}",
|
"${formatClock(programme.start)}–${formatClock(programme.stop)}" +
|
||||||
|
if (hit.further > 0) " · +${hit.further} weitere" else "",
|
||||||
color = CastarrColors.faint,
|
color = CastarrColors.faint,
|
||||||
fontFamily = AppFont,
|
fontFamily = AppFont,
|
||||||
fontSize = 12.sp,
|
fontSize = 12.sp,
|
||||||
|
|||||||
@@ -178,8 +178,8 @@ fun SettingsScreen(state: AppState) {
|
|||||||
|
|
||||||
SettingsCard("Vereinsmenüs") {
|
SettingsCard("Vereinsmenüs") {
|
||||||
Text(
|
Text(
|
||||||
"Eigene Gruppe mit allen Sendern, auf denen der Verein in den " +
|
"Eigene Gruppe mit allen Sendern, auf denen der Verein " +
|
||||||
"nächsten 3 Stunden läuft. Bis zu " +
|
"heute noch läuft. Bis zu " +
|
||||||
"${dev.castarr.tv.data.TeamFilters.MAX_ACTIVE} Vereine.",
|
"${dev.castarr.tv.data.TeamFilters.MAX_ACTIVE} Vereine.",
|
||||||
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 11.sp,
|
color = CastarrColors.faint, fontFamily = AppFont, fontSize = 11.sp,
|
||||||
modifier = Modifier.padding(start = 14.dp, end = 14.dp, bottom = 6.dp),
|
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() } })
|
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
|
@Test
|
||||||
fun `covers all three divisions`() {
|
fun `covers all three divisions`() {
|
||||||
assertTrue(TeamFilters.all.size > 50)
|
assertTrue(TeamFilters.all.size > 50)
|
||||||
|
|||||||
Reference in New Issue
Block a user