Add in-play channel switcher overlay for live TV.
Press up during playback to open a glass-style side panel with the current category channels; switch zenders without closing the menu until back is pressed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e061d23569
commit
8cfae01e4b
3 changed files with 307 additions and 21 deletions
|
|
@ -12,8 +12,8 @@ android {
|
||||||
applicationId = "nl.vonas.mediacluster.tv"
|
applicationId = "nl.vonas.mediacluster.tv"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 35
|
versionCode = 36
|
||||||
versionName = "0.11.3"
|
versionName = "0.11.4"
|
||||||
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
buildConfigField("String", "DEFAULT_API_BASE", "\"https://master.vonas.nl\"")
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
|
||||||
|
|
|
||||||
|
|
@ -4,22 +4,36 @@ import android.view.ViewGroup
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
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.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
|
@ -46,11 +60,14 @@ import androidx.media3.common.PlaybackException
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.ui.AspectRatioFrameLayout
|
import androidx.media3.ui.AspectRatioFrameLayout
|
||||||
import androidx.media3.ui.PlayerView
|
import androidx.media3.ui.PlayerView
|
||||||
|
import androidx.tv.material3.ClickableSurfaceDefaults
|
||||||
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
import androidx.tv.material3.ExperimentalTvMaterial3Api
|
||||||
|
import androidx.tv.material3.Surface
|
||||||
import androidx.tv.material3.Text
|
import androidx.tv.material3.Text
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import nl.vonas.mediacluster.tv.data.ApiClient
|
import nl.vonas.mediacluster.tv.data.ApiClient
|
||||||
|
import nl.vonas.mediacluster.tv.data.IptvChannel
|
||||||
import nl.vonas.mediacluster.tv.data.IptvEpg
|
import nl.vonas.mediacluster.tv.data.IptvEpg
|
||||||
import nl.vonas.mediacluster.tv.playback.buildLiveTvPlayer
|
import nl.vonas.mediacluster.tv.playback.buildLiveTvPlayer
|
||||||
import nl.vonas.mediacluster.tv.playback.ensureAudioSelected
|
import nl.vonas.mediacluster.tv.playback.ensureAudioSelected
|
||||||
|
|
@ -62,38 +79,47 @@ import nl.vonas.mediacluster.tv.ui.theme.McColors
|
||||||
fun LiveTvPlayerScreen(
|
fun LiveTvPlayerScreen(
|
||||||
api: ApiClient,
|
api: ApiClient,
|
||||||
streamId: Int,
|
streamId: Int,
|
||||||
|
categoryId: String?,
|
||||||
|
onStreamChange: (Int) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val player = remember { buildLiveTvPlayer(context) }
|
val player = remember { buildLiveTvPlayer(context) }
|
||||||
val focus = remember { FocusRequester() }
|
val playerFocus = remember { FocusRequester() }
|
||||||
|
|
||||||
|
var currentStreamId by remember { mutableIntStateOf(streamId) }
|
||||||
var loading by remember { mutableStateOf(true) }
|
var loading by remember { mutableStateOf(true) }
|
||||||
|
var switching by remember { mutableStateOf(false) }
|
||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
var channelName by remember { mutableStateOf("") }
|
var channelName by remember { mutableStateOf("") }
|
||||||
var logoUrl by remember { mutableStateOf<String?>(null) }
|
var logoUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var epg by remember { mutableStateOf<IptvEpg?>(null) }
|
var epg by remember { mutableStateOf<IptvEpg?>(null) }
|
||||||
var overlay by remember { mutableStateOf(true) }
|
var infoOverlay by remember { mutableStateOf(true) }
|
||||||
|
var channelMenuOpen by remember { mutableStateOf(false) }
|
||||||
var fallbackUrl by remember { mutableStateOf<String?>(null) }
|
var fallbackUrl by remember { mutableStateOf<String?>(null) }
|
||||||
var fallbackFormat by remember { mutableStateOf<String?>(null) }
|
var fallbackFormat by remember { mutableStateOf<String?>(null) }
|
||||||
var triedFallback by remember { mutableStateOf(false) }
|
var triedFallback by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var menuChannels by remember { mutableStateOf<List<IptvChannel>>(emptyList()) }
|
||||||
|
var menuCategoryName by remember { mutableStateOf("Zenders") }
|
||||||
|
var menuLoading by remember { mutableStateOf(false) }
|
||||||
|
val menuListState = rememberLazyListState()
|
||||||
|
val menuChannelFocus = remember { mutableStateMapOf<Int, FocusRequester>() }
|
||||||
|
|
||||||
fun startStream(url: String, format: String) {
|
fun startStream(url: String, format: String) {
|
||||||
player.setMediaItem(liveMediaItem(url, format))
|
player.setMediaItem(liveMediaItem(url, format))
|
||||||
player.prepare()
|
player.prepare()
|
||||||
player.playWhenReady = true
|
player.playWhenReady = true
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(streamId) {
|
suspend fun loadAndPlay(streamIdToPlay: Int, isSwitch: Boolean) {
|
||||||
loading = true
|
if (isSwitch) switching = true else loading = true
|
||||||
error = null
|
error = null
|
||||||
triedFallback = false
|
triedFallback = false
|
||||||
epg = null
|
|
||||||
try {
|
try {
|
||||||
val play = api.iptvPlayUrl(streamId)
|
val play = api.iptvPlayUrl(streamIdToPlay)
|
||||||
channelName = play.name
|
channelName = play.name
|
||||||
logoUrl = play.logoUrl
|
logoUrl = play.logoUrl
|
||||||
// API levert classic TS eerst; lokaal nog TS forceren als fallback in response zit
|
|
||||||
val tsUrl = when {
|
val tsUrl = when {
|
||||||
play.format.equals("ts", ignoreCase = true) -> play.streamUrl
|
play.format.equals("ts", ignoreCase = true) -> play.streamUrl
|
||||||
play.fallbackFormat?.equals("ts", ignoreCase = true) == true -> play.fallbackStreamUrl
|
play.fallbackFormat?.equals("ts", ignoreCase = true) == true -> play.fallbackStreamUrl
|
||||||
|
|
@ -115,22 +141,59 @@ fun LiveTvPlayerScreen(
|
||||||
fallbackFormat = play.fallbackFormat
|
fallbackFormat = play.fallbackFormat
|
||||||
startStream(play.streamUrl, play.format)
|
startStream(play.streamUrl, play.format)
|
||||||
}
|
}
|
||||||
|
onStreamChange(streamIdToPlay)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
error = e.message ?: "Kon zender niet starten"
|
error = e.message ?: "Kon zender niet starten"
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
|
switching = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(streamId) {
|
LaunchedEffect(streamId) {
|
||||||
delay(2_500)
|
currentStreamId = streamId
|
||||||
runCatching { epg = api.iptvEpg(streamId) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(overlay) {
|
LaunchedEffect(currentStreamId) {
|
||||||
if (overlay) {
|
epg = null
|
||||||
|
loadAndPlay(currentStreamId, isSwitch = currentStreamId != streamId)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(currentStreamId) {
|
||||||
|
delay(2_500)
|
||||||
|
runCatching { epg = api.iptvEpg(currentStreamId) }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(channelMenuOpen, categoryId) {
|
||||||
|
if (!channelMenuOpen) return@LaunchedEffect
|
||||||
|
menuLoading = true
|
||||||
|
try {
|
||||||
|
val cats = runCatching { api.iptvCategories() }.getOrDefault(emptyList())
|
||||||
|
menuCategoryName = cats.firstOrNull { it.id == categoryId }?.name ?: "Zenders"
|
||||||
|
menuChannels = if (!categoryId.isNullOrBlank()) {
|
||||||
|
api.iptvChannels(categoryId)
|
||||||
|
} else {
|
||||||
|
api.iptvChannels()
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
menuChannels = emptyList()
|
||||||
|
} finally {
|
||||||
|
menuLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(channelMenuOpen, menuChannels, currentStreamId) {
|
||||||
|
if (!channelMenuOpen || menuChannels.isEmpty()) return@LaunchedEffect
|
||||||
|
val idx = menuChannels.indexOfFirst { it.streamId == currentStreamId }.coerceAtLeast(0)
|
||||||
|
menuListState.scrollToItem(idx)
|
||||||
|
delay(100)
|
||||||
|
menuChannelFocus[currentStreamId]?.let { runCatching { it.requestFocus() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(infoOverlay) {
|
||||||
|
if (infoOverlay && !channelMenuOpen) {
|
||||||
delay(5000)
|
delay(5000)
|
||||||
overlay = false
|
infoOverlay = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,27 +222,43 @@ fun LiveTvPlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler { onBack() }
|
BackHandler(enabled = channelMenuOpen) {
|
||||||
|
channelMenuOpen = false
|
||||||
|
}
|
||||||
|
BackHandler(enabled = !channelMenuOpen) {
|
||||||
|
onBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectChannel(newStreamId: Int) {
|
||||||
|
if (newStreamId == currentStreamId) return
|
||||||
|
currentStreamId = newStreamId
|
||||||
|
infoOverlay = true
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(Color.Black)
|
.background(Color.Black)
|
||||||
.focusRequester(focus)
|
.focusRequester(playerFocus)
|
||||||
.focusable()
|
.focusable()
|
||||||
.onPreviewKeyEvent { ev ->
|
.onPreviewKeyEvent { ev ->
|
||||||
|
if (channelMenuOpen) return@onPreviewKeyEvent false
|
||||||
if (ev.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
if (ev.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false
|
||||||
when (ev.key) {
|
when (ev.key) {
|
||||||
Key.Back, Key.Escape -> {
|
Key.Back, Key.Escape -> {
|
||||||
onBack()
|
onBack()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
Key.DirectionUp -> {
|
||||||
|
channelMenuOpen = true
|
||||||
|
true
|
||||||
|
}
|
||||||
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
|
Key.DirectionCenter, Key.Enter, Key.NumPadEnter -> {
|
||||||
overlay = !overlay
|
infoOverlay = !infoOverlay
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
overlay = true
|
infoOverlay = true
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,11 +282,18 @@ fun LiveTvPlayerScreen(
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
delay(200)
|
delay(200)
|
||||||
runCatching { focus.requestFocus() }
|
runCatching { playerFocus.requestFocus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(channelMenuOpen) {
|
||||||
|
if (!channelMenuOpen) {
|
||||||
|
delay(80)
|
||||||
|
runCatching { playerFocus.requestFocus() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = overlay && error == null,
|
visible = infoOverlay && !channelMenuOpen && error == null,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
) {
|
) {
|
||||||
|
|
@ -281,7 +367,32 @@ fun LiveTvPlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
AnimatedVisibility(
|
||||||
|
visible = channelMenuOpen,
|
||||||
|
enter = fadeIn(tween(180)) + slideInHorizontally(tween(220)) { it },
|
||||||
|
exit = fadeOut(tween(150)) + slideOutHorizontally(tween(200)) { it },
|
||||||
|
) {
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(alpha = 0.4f)),
|
||||||
|
)
|
||||||
|
LiveChannelSwitcher(
|
||||||
|
categoryName = menuCategoryName,
|
||||||
|
channels = menuChannels,
|
||||||
|
currentStreamId = currentStreamId,
|
||||||
|
loading = menuLoading,
|
||||||
|
switching = switching,
|
||||||
|
listState = menuListState,
|
||||||
|
channelFocus = menuChannelFocus,
|
||||||
|
onSelect = { selectChannel(it) },
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loading && !channelMenuOpen) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
Text("Zender laden…", color = Color.White, fontSize = 18.sp)
|
Text("Zender laden…", color = Color.White, fontSize = 18.sp)
|
||||||
}
|
}
|
||||||
|
|
@ -303,3 +414,176 @@ fun LiveTvPlayerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalTvMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun LiveChannelSwitcher(
|
||||||
|
categoryName: String,
|
||||||
|
channels: List<IptvChannel>,
|
||||||
|
currentStreamId: Int,
|
||||||
|
loading: Boolean,
|
||||||
|
switching: Boolean,
|
||||||
|
listState: androidx.compose.foundation.lazy.LazyListState,
|
||||||
|
channelFocus: MutableMap<Int, FocusRequester>,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val glassShape = RoundedCornerShape(topStart = 16.dp, bottomStart = 16.dp)
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.padding(top = 28.dp, bottom = 28.dp, end = 0.dp)
|
||||||
|
.width(340.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clip(glassShape)
|
||||||
|
.background(
|
||||||
|
Brush.horizontalGradient(
|
||||||
|
listOf(Color(0xF0181820), Color(0xE8101018)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
listOf(Color.White.copy(alpha = 0.18f), Color.White.copy(alpha = 0.06f)),
|
||||||
|
),
|
||||||
|
shape = glassShape,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Column(Modifier.fillMaxSize()) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 18.dp, vertical = 16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Zenders",
|
||||||
|
color = McColors.TextDim,
|
||||||
|
fontSize = 11.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
letterSpacing = 1.sp,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
categoryName,
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 17.sp,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
if (switching) {
|
||||||
|
Text("Wisselen…", color = McColors.Brand, fontSize = 12.sp, modifier = Modifier.padding(top = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(1.dp)
|
||||||
|
.background(Color.White.copy(alpha = 0.08f)),
|
||||||
|
)
|
||||||
|
when {
|
||||||
|
loading -> {
|
||||||
|
Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Laden…", color = McColors.TextDim, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channels.isEmpty() -> {
|
||||||
|
Box(Modifier.weight(1f), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Geen zenders", color = McColors.TextDim, fontSize = 14.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
) {
|
||||||
|
items(channels, key = { it.streamId }) { ch ->
|
||||||
|
val isCurrent = ch.streamId == currentStreamId
|
||||||
|
val fr = channelFocus.getOrPut(ch.streamId) { FocusRequester() }
|
||||||
|
Surface(
|
||||||
|
onClick = { onSelect(ch.streamId) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
.focusRequester(fr),
|
||||||
|
shape = ClickableSurfaceDefaults.shape(shape = RoundedCornerShape(8.dp)),
|
||||||
|
scale = ClickableSurfaceDefaults.scale(focusedScale = 1f),
|
||||||
|
colors = ClickableSurfaceDefaults.colors(
|
||||||
|
containerColor = if (isCurrent) Color(0x33E50914) else Color.Transparent,
|
||||||
|
focusedContainerColor = Color(0x28FFFFFF),
|
||||||
|
contentColor = Color.White,
|
||||||
|
focusedContentColor = Color.White,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(horizontal = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.width(3.dp)
|
||||||
|
.height(24.dp)
|
||||||
|
.clip(RoundedCornerShape(2.dp))
|
||||||
|
.background(if (isCurrent) McColors.Brand else Color.Transparent),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.size(32.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(Color(0xFF0A0A0A)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (!ch.logoUrl.isNullOrBlank()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = ch.logoUrl,
|
||||||
|
contentDescription = ch.name,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(3.dp),
|
||||||
|
contentScale = ContentScale.Fit,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
ch.number?.let { num ->
|
||||||
|
Text(
|
||||||
|
"$num",
|
||||||
|
color = McColors.TextDim,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = Modifier.width(32.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
ch.name,
|
||||||
|
color = if (isCurrent) Color.White else Color(0xFFDDDDDD),
|
||||||
|
fontSize = 14.sp,
|
||||||
|
fontWeight = if (isCurrent) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(14.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"Terug om te sluiten",
|
||||||
|
color = Color(0xFF666666),
|
||||||
|
fontSize = 11.sp,
|
||||||
|
modifier = Modifier.align(Alignment.Center),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -225,6 +225,8 @@ fun McNav() {
|
||||||
LiveTvPlayerScreen(
|
LiveTvPlayerScreen(
|
||||||
api = api,
|
api = api,
|
||||||
streamId = streamId,
|
streamId = streamId,
|
||||||
|
categoryId = tvCategoryId,
|
||||||
|
onStreamChange = { tvLastStreamId = it },
|
||||||
onBack = { nav.popBackStack() },
|
onBack = { nav.popBackStack() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue