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:
@@ -12,7 +12,7 @@ android {
|
||||
applicationId = "com.matchlivetv.match_live_tv"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 17
|
||||
versionCode = 21
|
||||
versionName = "2.0.0-native"
|
||||
|
||||
val apiBaseUrl = project.findProperty("API_BASE_URL") as String?
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ class OverlayCanvasRenderer(context: Context) {
|
||||
WatermarkElement(context),
|
||||
ScoreboardElement(),
|
||||
CompactScoreboardElement(),
|
||||
TimerElement(),
|
||||
SponsorElement(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user