|
|
|
@@ -13,7 +13,10 @@ import androidx.compose.ui.focus.FocusRequester
|
|
|
|
|
import androidx.compose.ui.focus.focusRequester
|
|
|
|
|
import androidx.compose.ui.focus.onFocusChanged
|
|
|
|
|
import androidx.compose.ui.graphics.Color
|
|
|
|
|
import androidx.compose.ui.graphics.Brush
|
|
|
|
|
import androidx.compose.ui.graphics.ColorFilter
|
|
|
|
|
import androidx.compose.ui.graphics.asImageBitmap
|
|
|
|
|
import androidx.compose.ui.layout.ContentScale
|
|
|
|
|
import androidx.compose.ui.res.painterResource
|
|
|
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
|
|
|
import androidx.compose.ui.text.style.TextOverflow
|
|
|
|
@@ -47,6 +50,11 @@ internal fun programmeProgress(startMs: Long, endMs: Long, nowMs: Long): Float =
|
|
|
|
|
if (endMs <= startMs) 0f else ((nowMs - startMs).toFloat() / (endMs - startMs)).coerceIn(0f, 1f)
|
|
|
|
|
|
|
|
|
|
private val HomeCorner = RoundedCornerShape(6.dp)
|
|
|
|
|
private const val HOME_SHUFFLE_MS = 30 * 60_000L
|
|
|
|
|
|
|
|
|
|
/** A few other channels, a different mix each half hour, so the choice doesn't shuffle under the cursor. */
|
|
|
|
|
internal fun homeOnNow(channels: List<SampleChannel>, tunedNumber: Int, count: Int, seed: Long): List<SampleChannel> =
|
|
|
|
|
channels.filter { it.number != tunedNumber }.shuffled(java.util.Random(seed)).take(count)
|
|
|
|
|
|
|
|
|
|
@Composable
|
|
|
|
|
internal fun HomeCard(modifier: Modifier = Modifier, focusRequester: FocusRequester? = null, onClick: () -> Unit, content: @Composable (Boolean) -> Unit) {
|
|
|
|
@@ -64,9 +72,60 @@ private fun ProgressBar(fraction: Float, modifier: Modifier = Modifier, focused:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** What's on, written over the live picture so the picture itself never has to move or restart. */
|
|
|
|
|
@Composable
|
|
|
|
|
private fun WhatsOn(tuned: SampleChannel, now: Long, modifier: Modifier) {
|
|
|
|
|
val show = tuned.at(now)
|
|
|
|
|
val next = tuned.at(show.endMs)
|
|
|
|
|
val branding = ChannelBrands.forNumber(tuned.number)
|
|
|
|
|
val (name, detail) = guideTitleParts(show.media.item.title)
|
|
|
|
|
Row(modifier.background(Brush.verticalGradient(listOf(Color.Transparent, Color(0xE603081A), Color(0xF003081A)))).padding(start = 10.dp, end = 10.dp, top = 22.dp, bottom = 8.dp),
|
|
|
|
|
horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.Bottom) {
|
|
|
|
|
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.width(84.dp).height(28.dp))
|
|
|
|
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
|
|
|
|
Text("${tuned.number} • NOW ${clock(show.startMs)} to ${clock(show.endMs)}", color = Color(0xFFD5DCEA), fontSize = 11.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
|
|
|
|
Text(name, color = Color.White, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
if (detail != null) Text(detail, color = Color.White, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
ProgressBar(programmeProgress(show.startMs, show.endMs, now), Modifier.fillMaxWidth().padding(vertical = 2.dp))
|
|
|
|
|
Text("NEXT ${clock(next.startMs)} ${guideTitleParts(next.media.item.title).first}", color = Color(0xFFD5DCEA), fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The ticket, then the next bill over its own artwork from the library, then what follows. */
|
|
|
|
|
@Composable
|
|
|
|
|
private fun DriveInCard(session: EmbySampleSession, plan: ChannelPlan?, now: Long, modifier: Modifier, onClick: () -> Unit) {
|
|
|
|
|
val minute = now / 60_000
|
|
|
|
|
val coming = remember(plan, minute) { plan?.let { driveInBills(it).filter { bill -> bill.endMs > now } }.orEmpty() }
|
|
|
|
|
val first = coming.firstOrNull()
|
|
|
|
|
val picture = rememberFilmPicture(session, first?.entries?.first()?.media?.item?.id)
|
|
|
|
|
HomeCard(modifier, onClick = onClick) { focused ->
|
|
|
|
|
Column(Modifier.fillMaxSize().padding(3.dp), verticalArrangement = Arrangement.spacedBy(5.dp)) {
|
|
|
|
|
Image(painterResource(R.drawable.orbit_drive_in_ticket), "Orbit Drive-In", Modifier.fillMaxWidth().aspectRatio(500f / 155f), contentScale = ContentScale.FillBounds)
|
|
|
|
|
Box(Modifier.fillMaxWidth().weight(1f).clip(HomeCorner).background(Navy)) {
|
|
|
|
|
picture?.let { Image(it.asImageBitmap(), null, Modifier.matchParentSize(), contentScale = ContentScale.Crop) }
|
|
|
|
|
Column(Modifier.align(Alignment.BottomStart).fillMaxWidth().background(Brush.verticalGradient(listOf(Color.Transparent, Color(0xE603081A), Color(0xF003081A)))).padding(start = 10.dp, end = 10.dp, top = 24.dp, bottom = 8.dp),
|
|
|
|
|
verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
|
|
|
|
if (first != null) {
|
|
|
|
|
Text("${driveInDay(first, now).uppercase(java.util.Locale.UK)}${if (first.startMs > now) " • ${clock(first.startMs)}" else ""}", color = Color(0xFFF2C14E), fontSize = 11.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 1.sp, maxLines = 1)
|
|
|
|
|
Text(first.title, color = Color.White, fontSize = 17.sp, fontWeight = FontWeight.SemiBold, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
Text(first.kind.label, color = Color(0xFFD5DCEA), fontSize = 11.sp, maxLines = 1)
|
|
|
|
|
} else Text("Tonight's bill is on its way", color = Color.White, fontSize = 15.sp, fontWeight = FontWeight.SemiBold)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
coming.drop(1).take(2).forEach { bill ->
|
|
|
|
|
Row(Modifier.fillMaxWidth().padding(horizontal = 7.dp), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
|
|
|
Text("${driveInDay(bill, now).take(3)} ${clock(bill.startMs)}", color = if (focused) Navy else Color(0xFFF2C14E), fontSize = 11.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
|
|
|
|
Text(bill.title, color = if (focused) Navy else Color.White, fontSize = 11.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Home opens onto the tuned channel playing, one press from anything else. */
|
|
|
|
|
@Composable
|
|
|
|
|
internal fun HomeScreen(channels: List<SampleChannel>, tuned: SampleChannel, modifier: Modifier = Modifier,
|
|
|
|
|
internal fun HomeScreen(session: EmbySampleSession, channels: List<SampleChannel>, tuned: SampleChannel, modifier: Modifier = Modifier,
|
|
|
|
|
picture: @Composable () -> Unit, onCategory: (ChannelCategory) -> Unit, onWatch: (Int) -> Unit, onDriveIn: () -> Unit, driveInPlan: ChannelPlan? = null) {
|
|
|
|
|
var now by remember { mutableLongStateOf(System.currentTimeMillis()) }
|
|
|
|
|
LaunchedEffect(Unit) { while (true) { delay(1000); now = System.currentTimeMillis() } }
|
|
|
|
@@ -86,32 +145,21 @@ internal fun HomeScreen(channels: List<SampleChannel>, tuned: SampleChannel, mod
|
|
|
|
|
}
|
|
|
|
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
|
|
|
HomeCard(Modifier.fillMaxWidth().aspectRatio(16f / 9f), onClick = { onWatch(tuned.number) }) {
|
|
|
|
|
Box(Modifier.fillMaxSize().padding(3.dp)) { picture() }
|
|
|
|
|
}
|
|
|
|
|
val show = tuned.at(now)
|
|
|
|
|
val next = tuned.at(show.endMs)
|
|
|
|
|
val branding = ChannelBrands.forNumber(tuned.number)
|
|
|
|
|
val (name, detail) = guideTitleParts(show.media.item.title)
|
|
|
|
|
Row(Modifier.fillMaxWidth().weight(1f).clip(HomeCorner).background(Panel).padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.CenterVertically) {
|
|
|
|
|
if (branding != null) Image(painterResource(branding.logo), branding.displayName, Modifier.width(92.dp).height(34.dp))
|
|
|
|
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
|
|
|
|
Text("NOW ${clock(show.startMs)} to ${clock(show.endMs)}", color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
|
|
|
|
Text(name, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
if (detail != null) Text(detail, color = Color.White, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
ProgressBar(programmeProgress(show.startMs, show.endMs, now), Modifier.fillMaxWidth().padding(vertical = 2.dp))
|
|
|
|
|
Text("NEXT ${clock(next.startMs)} ${guideTitleParts(next.media.item.title).first}", color = Muted, fontSize = 12.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
Box(Modifier.fillMaxSize().padding(3.dp)) {
|
|
|
|
|
picture()
|
|
|
|
|
WhatsOn(tuned, now, Modifier.align(Alignment.BottomStart).fillMaxWidth())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Column(Modifier.width(250.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
|
|
|
|
Text("On now", color = Color.White, fontSize = 14.sp, fontWeight = FontWeight.SemiBold)
|
|
|
|
|
channels.take(3).forEach { line ->
|
|
|
|
|
val seed = now / HOME_SHUFFLE_MS
|
|
|
|
|
val others = remember(seed, tuned.number, channels.size) { homeOnNow(channels, tuned.number, 3, seed) }
|
|
|
|
|
others.forEach { line ->
|
|
|
|
|
val show = line.at(now)
|
|
|
|
|
val mark = ChannelBrands.forNumber(line.number)
|
|
|
|
|
HomeCard(Modifier.fillMaxWidth().height(50.dp), onClick = { onWatch(line.number) }) { focused ->
|
|
|
|
|
HomeCard(Modifier.fillMaxWidth().height(46.dp), onClick = { onWatch(line.number) }) { focused ->
|
|
|
|
|
Row(Modifier.fillMaxSize().padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.width(72.dp).height(28.dp))
|
|
|
|
|
else Text(ChannelBrands.displayName(line), Modifier.width(72.dp), color = if (focused) Navy else Color.White, fontSize = 12.sp, maxLines = 1)
|
|
|
|
|
if (mark != null) Image(painterResource(mark.logo), mark.displayName, Modifier.width(84.dp).height(28.dp))
|
|
|
|
|
else Text(ChannelBrands.displayName(line), Modifier.width(84.dp), color = if (focused) Navy else Color.White, fontSize = 12.sp, maxLines = 1)
|
|
|
|
|
Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
|
|
|
Text(guideTitleParts(show.media.item.title).first, color = if (focused) Navy else Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
ProgressBar(programmeProgress(show.startMs, show.endMs, now), Modifier.fillMaxWidth(), focused)
|
|
|
|
@@ -119,21 +167,8 @@ internal fun HomeScreen(channels: List<SampleChannel>, tuned: SampleChannel, mod
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
val nowMs = System.currentTimeMillis()
|
|
|
|
|
val preview = driveInPlan?.let { plan -> driveInBills(plan).firstOrNull { it.endMs > nowMs } }
|
|
|
|
|
HomeCard(Modifier.fillMaxWidth().weight(1f), onClick = onDriveIn) { focused ->
|
|
|
|
|
Column {
|
|
|
|
|
Row(Modifier.fillMaxWidth().background(if (focused) Yellow else Color(0xFF7A5A12)).padding(horizontal = 10.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
|
|
|
Image(painterResource(R.drawable.orbit_icon_office), null, Modifier.size(20.dp), colorFilter = if (focused) ColorFilter.tint(Navy) else null)
|
|
|
|
|
Text("Orbit Drive-In", color = if (focused) Navy else Color.White, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, maxLines = 1)
|
|
|
|
|
}
|
|
|
|
|
Column(Modifier.padding(horizontal = 10.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
|
|
|
Text(preview?.title ?: "Tonight's bill is on its way", color = if (focused) Navy else Color.White, fontSize = 18.sp, fontWeight = FontWeight.SemiBold, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
|
|
|
|
if (preview != null) Text("${preview.kind.label}, ${driveInWhen(preview, nowMs)}", color = if (focused) Navy else Muted, fontSize = 12.sp, maxLines = 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
DriveInCard(session, driveInPlan, now, Modifier.width(250.dp).fillMaxHeight(), onDriveIn)
|
|
|
|
|
}
|
|
|
|
|
Text("▲▼ Move OK Select ► Picture and events Back Close", color = Muted, fontSize = 11.sp)
|
|
|
|
|
}
|
|
|
|
|