Migliora overlay basket Android, regia e catalogo sport senza timer.

Fix crash e race su +3, layout tabellone con squadre ai lati, safe area in diretta e rimozione overlay cronometro; backend con sync punteggio periodo, link diretta in regia e init score_state basket.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-09 23:31:12 +02:00
parent 34c97f4447
commit 2b7e8b9e36
23 changed files with 341 additions and 167 deletions

View File

@@ -220,11 +220,24 @@ data class ScoreStateDto(
timeoutHome = timeoutHome ?: false,
timeoutAway = timeoutAway ?: false,
boardType = boardType ?: "volley",
period = currentSet ?: 1,
period = parsePeriodNumber(period, currentSet),
periodLabel = period,
clockSecs = clockSecs ?: 0,
clockRunning = clockRunning ?: false,
)
private fun parsePeriodNumber(label: String?, currentSet: Int?): Int {
label?.let { text ->
Regex("""Q(\d+)""", RegexOption.IGNORE_CASE).find(text)?.groupValues?.get(1)?.toIntOrNull()
?.let { return it }
Regex("""(\d+)\s*°""").find(text)?.groupValues?.get(1)?.toIntOrNull()
?.let { return it }
Regex("""OT(\d+)""", RegexOption.IGNORE_CASE).find(text)?.groupValues?.get(1)?.toIntOrNull()
?.let { return it }
text.toIntOrNull()?.takeIf { it > 0 }?.let { return it }
}
return currentSet?.coerceAtLeast(1) ?: 1
}
}
data class ScoreActionRequest(
@@ -238,6 +251,11 @@ data class ScoreSyncRequest(
@Json(name = "away_points") val awayPoints: Int,
@Json(name = "current_set") val currentSet: Int,
@Json(name = "set_partials") val setPartials: List<SetPartialDto>,
@Json(name = "clock_secs") val clockSecs: Int? = null,
@Json(name = "clock_running") val clockRunning: Boolean? = null,
val period: Int? = null,
@Json(name = "home_score") val homeScore: Int? = null,
@Json(name = "away_score") val awayScore: Int? = null,
)
data class CreateSessionRequest(

View File

@@ -10,6 +10,7 @@ class ScoreRepository(
private val api: MatchLiveApi,
) {
suspend fun syncScore(sessionId: String, score: ScoreState): ScoreState? {
val periodBoard = score.boardType in PERIOD_BOARDS
val response = api.syncScore(
sessionId,
ScoreSyncRequest(
@@ -21,6 +22,11 @@ class ScoreRepository(
setPartials = score.setPartials.map {
SetPartialDto(set = it.set, home = it.home, away = it.away)
},
clockSecs = score.clockSecs.takeIf { periodBoard },
clockRunning = score.clockRunning.takeIf { periodBoard },
period = score.period.takeIf { periodBoard },
homeScore = score.homePoints.takeIf { periodBoard },
awayScore = score.awayPoints.takeIf { periodBoard },
),
)
return response.score?.toDomain()
@@ -30,4 +36,8 @@ class ScoreRepository(
val response = api.applyScoreAction(sessionId, ScoreActionRequest(action))
return response.score?.toDomain()
}
companion object {
private val PERIOD_BOARDS = setOf("basket", "timed")
}
}

View File

@@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -75,9 +74,26 @@ class ScoreController(
}
fun applyAction(action: String) {
applyOptimistic(action)
scheduleAction(action)
}
private fun applyOptimistic(action: String) {
_score.update { current ->
when (action) {
"home_point" -> current.copy(homePoints = current.homePoints + 1)
"away_point" -> current.copy(awayPoints = current.awayPoints + 1)
"home_point_2" -> current.copy(homePoints = current.homePoints + 2)
"away_point_2" -> current.copy(awayPoints = current.awayPoints + 2)
"home_point_3" -> current.copy(homePoints = current.homePoints + 3)
"away_point_3" -> current.copy(awayPoints = current.awayPoints + 3)
"home_undo" -> current.copy(homePoints = (current.homePoints - 1).coerceAtLeast(0))
"away_undo" -> current.copy(awayPoints = (current.awayPoints - 1).coerceAtLeast(0))
else -> current
}
}
}
private fun scheduleAction(action: String) {
syncJob?.cancel()
syncJob = scope.launch {

View File

@@ -6,14 +6,17 @@ import android.graphics.Paint
import android.graphics.RectF
import android.graphics.Typeface
import android.text.TextPaint
import android.text.TextUtils
import kotlin.math.roundToInt
/** Tabellone compatto per basket, timed e sport a punteggio semplice. */
/** Tabellone compatto basket/timed: squadre ai lati, punteggio al centro. */
class CompactScoreboardElement : OverlayElement {
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.argb(210, 12, 12, 12)
}
private val accentBarPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val logoPaint = Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG)
private val labelPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
@@ -21,6 +24,7 @@ class CompactScoreboardElement : OverlayElement {
private val metaPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#AAAAAA")
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
textAlign = Paint.Align.CENTER
}
private val scorePaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
@@ -33,29 +37,116 @@ class CompactScoreboardElement : OverlayElement {
val kind = state.overlayKind
if (kind !in setOf(OverlayKind.BASKET, OverlayKind.TIMED, OverlayKind.RACKET, OverlayKind.VOLLEY)) return
val w = (layout.canvasWidth * 0.42f).roundToInt().coerceAtLeast(220)
val h = (layout.canvasHeight * 0.11f).roundToInt().coerceAtLeast(72)
val scale = layout.canvasHeight / 720f
val pad = (10f * scale).roundToInt().coerceAtLeast(8)
val barW = (4f * scale).roundToInt().coerceAtLeast(3)
val logoSize = (28f * scale).roundToInt().coerceAtLeast(20)
val gap = (6f * scale).roundToInt().coerceAtLeast(4)
val totalW = (layout.canvasWidth * 0.62f).roundToInt().coerceAtLeast(320)
val mainH = (layout.canvasHeight * 0.09f).roundToInt().coerceAtLeast(56)
val metaH = (layout.canvasHeight * 0.028f).roundToInt().coerceAtLeast(16)
val totalH = mainH + metaH + pad
val left = layout.marginPx.toFloat()
val top = layout.marginPx.toFloat()
val rect = RectF(left, top, left + w, top + h)
canvas.drawRoundRect(rect, 12f, 12f, backgroundPaint)
val rect = RectF(left, top, left + totalW, top + totalH)
canvas.drawRoundRect(rect, 10f * scale, 10f * scale, backgroundPaint)
labelPaint.textSize = (h * 0.18f).coerceAtLeast(14f)
scorePaint.textSize = (h * 0.34f).coerceAtLeast(22f)
metaPaint.textSize = (h * 0.16f).coerceAtLeast(12f)
val centerX = rect.centerX()
val rowCenterY = top + pad + mainH * 0.58f
val sideMaxW = totalW * 0.34f
val scoreZoneHalf = totalW * 0.11f
labelPaint.textSize = (mainH * 0.28f).coerceAtLeast(13f)
scorePaint.textSize = (mainH * 0.46f).coerceAtLeast(24f)
drawTeamSide(
canvas = canvas,
compact = compact,
isHome = true,
rectLeft = rect.left + pad,
rectRight = centerX - scoreZoneHalf,
centerY = rowCenterY,
barW = barW,
logoSize = logoSize,
gap = gap,
maxWidth = sideMaxW,
)
drawTeamSide(
canvas = canvas,
compact = compact,
isHome = false,
rectLeft = centerX + scoreZoneHalf,
rectRight = rect.right - pad,
centerY = rowCenterY,
barW = barW,
logoSize = logoSize,
gap = gap,
maxWidth = sideMaxW,
)
val midY = rect.centerY()
canvas.drawText(compact.homeTeamName, rect.left + 16f, midY - 6f, labelPaint)
canvas.drawText(compact.awayTeamName, rect.left + 16f, midY + labelPaint.textSize + 4f, labelPaint)
canvas.drawText(
"${compact.homeScore} - ${compact.awayScore}",
rect.right - 56f,
midY + scorePaint.textSize * 0.35f,
centerX,
rowCenterY + scorePaint.textSize * 0.32f,
scorePaint,
)
val meta = listOfNotNull(compact.periodLabel, compact.clockText).joinToString(" · ")
if (meta.isNotBlank()) {
canvas.drawText(meta, rect.left + 16f, rect.bottom - 10f, metaPaint)
compact.periodLabel?.takeIf { it.isNotBlank() }?.let { period ->
metaPaint.textSize = (metaH * 0.72f).coerceAtLeast(11f)
canvas.drawText(period, centerX, rect.bottom - pad * 0.45f, metaPaint)
}
}
private fun drawTeamSide(
canvas: Canvas,
compact: CompactScoreboardState,
isHome: Boolean,
rectLeft: Float,
rectRight: Float,
centerY: Float,
barW: Int,
logoSize: Int,
gap: Int,
maxWidth: Float,
) {
val accentColor = if (isHome) compact.homeAccentColor else compact.awayAccentColor
val logoUrl = if (isHome) compact.homeLogoUrl else compact.awayLogoUrl
val logo = OverlayLogoCache.get(logoUrl)
val rawName = if (isHome) compact.homeTeamName else compact.awayTeamName
val name = ellipsize(rawName.trim(), labelPaint, maxWidth * 0.55f)
if (isHome) {
var x = rectLeft
accentBarPaint.color = accentColor
canvas.drawRect(x, centerY - logoSize * 0.45f, x + barW, centerY + logoSize * 0.45f, accentBarPaint)
x += barW + gap
if (logo != null) {
val dest = RectF(x, centerY - logoSize / 2f, x + logoSize, centerY + logoSize / 2f)
canvas.drawBitmap(logo, null, dest, logoPaint)
x += logoSize + gap
}
canvas.drawText(name, x, centerY + labelPaint.textSize * 0.35f, labelPaint)
} else {
var x = rectRight
accentBarPaint.color = accentColor
canvas.drawRect(x - barW, centerY - logoSize * 0.45f, x, centerY + logoSize * 0.45f, accentBarPaint)
x -= barW + gap
labelPaint.textAlign = Paint.Align.RIGHT
canvas.drawText(name, x, centerY + labelPaint.textSize * 0.35f, labelPaint)
labelPaint.textAlign = Paint.Align.LEFT
x -= gap
if (logo != null) {
x -= logoSize
val dest = RectF(x, centerY - logoSize / 2f, x + logoSize, centerY + logoSize / 2f)
canvas.drawBitmap(logo, null, dest, logoPaint)
}
}
}
private fun ellipsize(text: String, paint: TextPaint, maxWidth: Float): String {
val safeMax = maxWidth.coerceAtLeast(48f)
return TextUtils.ellipsize(text, paint, safeMax, TextUtils.TruncateAt.END)?.toString() ?: text
}
}

View File

@@ -16,7 +16,6 @@ class OverlayCanvasRenderer(context: Context) {
WatermarkElement(context),
ScoreboardElement(),
CompactScoreboardElement(),
TimerElement(),
SponsorElement(),
)

View File

@@ -1,6 +1,7 @@
package com.matchlivetv.match_live_tv.streaming.overlay
import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import com.pedro.encoder.input.gl.render.filters.`object`.ImageObjectFilterRender
import com.pedro.encoder.utils.gl.TranslateTo
@@ -21,6 +22,7 @@ class OverlayRenderer(context: Context) {
private var attached = false
private var lastFingerprint = Int.MIN_VALUE
private var pendingState: OverlayState? = null
private var uploadedBitmap: Bitmap? = null
fun attach(gl: GlInterface, width: Int, height: Int, isPortrait: Boolean = false) {
if (attached && gl === glInterface && width == streamWidth && height == streamHeight) {
@@ -72,6 +74,7 @@ class OverlayRenderer(context: Context) {
glInterface = null
attached = false
lastFingerprint = Int.MIN_VALUE
recycleUploadedBitmap()
canvasRenderer.release()
}
@@ -80,14 +83,24 @@ class OverlayRenderer(context: Context) {
if (fp == lastFingerprint) return
lastFingerprint = fp
val bmp = canvasRenderer.render(state, isPortraitContent)
val rendered = canvasRenderer.render(state, isPortraitContent)
val upload = rendered.copy(rendered.config ?: Bitmap.Config.ARGB_8888, false)
filter?.let { overlayFilter ->
overlayFilter.setDefaultScale(streamWidth, streamHeight)
overlayFilter.setPosition(TranslateTo.TOP_LEFT)
overlayFilter.setImage(bmp)
overlayFilter.setImage(upload)
recycleUploadedBitmap()
uploadedBitmap = upload
} ?: run {
upload.recycle()
}
}
private fun recycleUploadedBitmap() {
uploadedBitmap?.recycle()
uploadedBitmap = null
}
companion object {
private const val TAG = "OverlayRenderer"
}

View File

@@ -27,14 +27,14 @@ data class CompactScoreboardState(
val homeScore: Int,
val awayScore: Int,
val periodLabel: String? = null,
val clockText: String? = null,
val homeAccentColor: Int = 0xFFFF2D2D.toInt(),
val awayAccentColor: Int = 0xFF1E3A8A.toInt(),
val homeLogoUrl: String? = null,
val awayLogoUrl: String? = null,
)
enum class OverlayKind {
NONE,
TIMER,
VOLLEY,
BASKET,
TIMED,
@@ -43,8 +43,7 @@ enum class OverlayKind {
companion object {
fun fromApi(value: String?): OverlayKind = when (value?.lowercase()) {
"none" -> NONE
"timer" -> TIMER
"none", "timer" -> NONE
"basket" -> BASKET
"timed" -> TIMED
"racket" -> RACKET
@@ -66,14 +65,12 @@ data class OverlayState(
val compactScoreboard: CompactScoreboardState? = null,
val watermarkVisible: Boolean = true,
val broadcastStatus: BroadcastOverlayStatus = BroadcastOverlayStatus.LIVE,
val timerText: String? = null,
val sponsorText: String? = null,
) {
fun fingerprint(): Int {
var result = overlayKind.hashCode()
result = 31 * result + watermarkVisible.hashCode()
result = 31 * result + broadcastStatus.hashCode()
result = 31 * result + (timerText?.hashCode() ?: 0)
result = 31 * result + (sponsorText?.hashCode() ?: 0)
result = 31 * result + (scoreboard?.fingerprint() ?: 0)
result = 31 * result + (compactScoreboard?.fingerprint() ?: 0)
@@ -104,8 +101,11 @@ private fun CompactScoreboardState.fingerprint(): Int {
result = 31 * result + awayTeamName.hashCode()
result = 31 * result + homeScore
result = 31 * result + awayScore
result = 31 * result + homeAccentColor
result = 31 * result + awayAccentColor
result = 31 * result + (periodLabel?.hashCode() ?: 0)
result = 31 * result + (clockText?.hashCode() ?: 0)
result = 31 * result + logoLoadedKey(homeLogoUrl)
result = 31 * result + logoLoadedKey(awayLogoUrl)
return result
}

View File

@@ -1,24 +0,0 @@
package com.matchlivetv.match_live_tv.streaming.overlay
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
/** Placeholder per cronometro / timeout (MVP: disegna solo se [OverlayState.timerText] è valorizzato). */
class TimerElement : OverlayElement {
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
}
override fun draw(canvas: Canvas, state: OverlayState, layout: OverlayLayout) {
if (state.overlayKind != OverlayKind.TIMER && state.timerText.isNullOrBlank()) return
val text = state.timerText?.takeIf { it.isNotBlank() } ?: return
paint.textSize = layout.canvasHeight * 0.05f
val x = layout.marginPx.toFloat()
val y = layout.canvasHeight - layout.marginPx.toFloat()
canvas.drawText(text, x, y, paint)
}
}

View File

@@ -16,10 +16,9 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -28,7 +27,6 @@ import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.Timer
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.Visibility
@@ -65,7 +63,6 @@ import com.matchlivetv.match_live_tv.core.resolveMediaUrl
import com.matchlivetv.match_live_tv.core.DeviceHealthSnapshot
import com.matchlivetv.match_live_tv.core.ThermalLevel
import com.matchlivetv.match_live_tv.domain.ScoreState
import com.matchlivetv.match_live_tv.streaming.overlay.formatOverlayClock
import com.matchlivetv.match_live_tv.ui.components.MatchStatusBadge
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
@@ -100,7 +97,6 @@ fun BroadcastControlsOverlay(
onPoint3Away: (() -> Unit)? = null,
onCloseSet: (() -> Unit)? = null,
onAdvancePeriod: (() -> Unit)? = null,
onClockToggle: (() -> Unit)? = null,
onPauseOrResume: () -> Unit,
onTerminate: () -> Unit,
onShareLive: () -> Unit,
@@ -139,21 +135,23 @@ fun BroadcastControlsOverlay(
)
}
Box(modifier.fillMaxSize()) {
Box(
modifier
.fillMaxSize()
.windowInsetsPadding(WindowInsets.safeDrawing),
) {
MatchStatusBadge(
text = statusText,
textColor = statusColor,
backgroundColor = MatchColors.Background.copy(alpha = 0.78f),
modifier = Modifier
.align(Alignment.TopStart)
.windowInsetsPadding(WindowInsets.statusBars)
.padding(start = 8.dp, top = 4.dp),
)
Column(
Modifier
.align(Alignment.TopEnd)
.windowInsetsPadding(WindowInsets.statusBars)
.padding(top = 4.dp, end = 8.dp),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(6.dp),
@@ -205,14 +203,6 @@ fun BroadcastControlsOverlay(
onClick = onAdvancePeriod,
)
}
if (onClockToggle != null) {
SideIconButton(
icon = Icons.Default.Timer,
contentDescription = if (score.clockRunning) "Ferma cronometro" else "Avvia cronometro",
onClick = onClockToggle,
highlighted = score.clockRunning,
)
}
}
Column(
@@ -238,7 +228,6 @@ fun BroadcastControlsOverlay(
Row(
Modifier
.align(Alignment.BottomCenter)
.windowInsetsPadding(WindowInsets.navigationBars)
.padding(start = SideToolbarWidth, end = SideToolbarWidth, bottom = 8.dp),
verticalAlignment = Alignment.Bottom,
) {
@@ -403,18 +392,6 @@ private fun ScoreCenterPanel(
style = MaterialTheme.typography.labelSmall,
color = MatchColors.TextSecondary,
)
Text(
formatOverlayClock(score.clockSecs),
style = MaterialTheme.typography.labelMedium,
color = if (score.clockRunning) MatchColors.SuccessGreen else MatchColors.TextSecondary,
)
}
"timer" -> {
Text(
formatOverlayClock(score.clockSecs, countUp = true),
style = MaterialTheme.typography.labelMedium,
color = if (score.clockRunning) MatchColors.SuccessGreen else MatchColors.TextSecondary,
)
}
"generic" -> Unit
else -> {

View File

@@ -43,7 +43,6 @@ import com.matchlivetv.match_live_tv.streaming.overlay.CompactScoreboardState
import com.matchlivetv.match_live_tv.streaming.overlay.OverlayKind
import com.matchlivetv.match_live_tv.streaming.overlay.OverlayLogoCache
import com.matchlivetv.match_live_tv.streaming.overlay.OverlayState
import com.matchlivetv.match_live_tv.streaming.overlay.formatOverlayClock
import com.matchlivetv.match_live_tv.streaming.overlay.toOverlayStatus
import com.matchlivetv.match_live_tv.streaming.overlay.toScoreboardState
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
@@ -72,6 +71,7 @@ fun BroadcastScreen(
var pauseInFlight by remember { mutableStateOf(false) }
var resumeInFlight by remember { mutableStateOf(false) }
var controlsVisible by remember { mutableStateOf(true) }
var logoReady by remember { mutableStateOf(0) }
var deviceHealth by remember { mutableStateOf(DeviceTelemetry.snapshot(context)) }
val metrics by container.broadcastCoordinator.metrics.collectAsState()
val score by container.scoreController.score.collectAsState()
@@ -216,9 +216,10 @@ fun BroadcastScreen(
),
bearerToken = authToken,
)
logoReady++
}
LaunchedEffect(score, match, metrics.phase) {
LaunchedEffect(score, match, metrics.phase, logoReady) {
val currentMatch = match ?: return@LaunchedEffect
val homeLogoUrl = resolveMediaUrl(currentMatch.homeLogoUrl)
val awayLogoUrl = resolveMediaUrl(currentMatch.opponentLogoUrl)
@@ -231,15 +232,6 @@ fun BroadcastScreen(
watermarkVisible = false,
broadcastStatus = metrics.phase.toOverlayStatus(),
)
OverlayKind.TIMER -> OverlayState(
overlayKind = overlayKind,
timerText = formatOverlayClock(
if (score.boardType == "timer") score.clockSecs else score.homePoints,
countUp = true,
),
watermarkVisible = true,
broadcastStatus = metrics.phase.toOverlayStatus(),
)
OverlayKind.BASKET, OverlayKind.TIMED -> OverlayState(
overlayKind = overlayKind,
compactScoreboard = CompactScoreboardState(
@@ -249,11 +241,12 @@ fun BroadcastScreen(
awayScore = score.awayPoints,
periodLabel = score.periodLabel ?: when (overlayKind) {
OverlayKind.BASKET -> "Q${score.period}"
else -> "${score.period}°"
else -> "${score.period}° tempo"
},
clockText = formatOverlayClock(score.clockSecs),
homeAccentColor = homeColor,
awayAccentColor = awayColor,
homeLogoUrl = homeLogoUrl,
awayLogoUrl = awayLogoUrl,
),
watermarkVisible = true,
broadcastStatus = metrics.phase.toOverlayStatus(),
@@ -293,16 +286,6 @@ fun BroadcastScreen(
}
}
LaunchedEffect(sessionId) {
while (true) {
delay(1_000)
val current = container.scoreController.score.value
if (current.clockRunning && current.boardType in setOf("basket", "timed", "timer")) {
container.scoreController.applyAction("clock_tick")
}
}
}
LaunchedEffect(sessionId) {
while (true) {
delay(4_000)
@@ -364,7 +347,7 @@ fun BroadcastScreen(
val currentSession = session
val currentMatch = match
val boardType = currentMatch?.boardType?.ifBlank { score.boardType } ?: score.boardType
val usesActionScoring = boardType in setOf("basket", "timed", "timer")
val usesActionScoring = boardType in setOf("basket", "timed")
fun applyScoreAction(action: String) {
container.scoreController.applyAction(action)
@@ -427,7 +410,12 @@ fun BroadcastScreen(
}
if (!loading && permissions.granted && currentMatch != null && scoreActions != null) {
key(score.progressKey()) {
val controlsKey = if (boardType in setOf("basket", "timed")) {
currentMatch.id
} else {
score.progressKey()
}
key(controlsKey) {
BroadcastControlsOverlay(
controlsVisible = controlsVisible,
onToggleControls = { controlsVisible = !controlsVisible },
@@ -508,11 +496,6 @@ fun BroadcastScreen(
} else {
null
},
onClockToggle = if (boardType in setOf("basket", "timed", "timer")) {
{ applyScoreAction("clock_toggle") }
} else {
null
},
onPauseOrResume = {
scope.launch {
if (isPaused) resumeStream() else pauseStream()

View File

@@ -11,8 +11,16 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Sports
import androidx.compose.material.icons.filled.SportsBasketball
import androidx.compose.material.icons.filled.SportsSoccer
import androidx.compose.material.icons.filled.SportsTennis
import androidx.compose.material.icons.filled.SportsVolleyball
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -20,6 +28,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
@@ -56,6 +65,30 @@ fun WizardStepIndicator(currentStep: Int, modifier: Modifier = Modifier) {
fun wizardStepTitle(step: Int): String =
stepTitles[(step.coerceIn(1, 3) - 1)]
private fun sportWizardIcon(sportKey: String, boardType: String): ImageVector = when {
sportKey.contains("volley") || boardType == "volley" || boardType == "racket" ->
Icons.Default.SportsVolleyball
sportKey == "basket" || boardType == "basket" -> Icons.Default.SportsBasketball
sportKey.contains("calcio") || boardType == "timed" -> Icons.Default.SportsSoccer
sportKey.contains("tennis") || sportKey.contains("padel") -> Icons.Default.SportsTennis
else -> Icons.Default.Sports
}
@Composable
fun SportWizardBadge(
sportKey: String,
boardType: String,
sportLabel: String?,
modifier: Modifier = Modifier,
) {
Icon(
imageVector = sportWizardIcon(sportKey, boardType),
contentDescription = sportLabel ?: sportKey,
tint = MatchColors.TextSecondary,
modifier = modifier.size(18.dp),
)
}
@Composable
fun WizardChoiceButton(
label: String,