Introduce architettura multi-sport con catalogo, engine scoring e overlay.
Catalogo sport in YAML, board implicito, engine per volley/basket/timed/racket/timer/generic, regia e app Android allineate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package com.matchlivetv.match_live_tv.data.api
|
||||
import com.matchlivetv.match_live_tv.domain.AuthTokens
|
||||
import com.matchlivetv.match_live_tv.domain.Match
|
||||
import com.matchlivetv.match_live_tv.domain.ScoringRules
|
||||
import com.matchlivetv.match_live_tv.domain.SportOption
|
||||
import com.matchlivetv.match_live_tv.domain.StreamSession
|
||||
import com.matchlivetv.match_live_tv.domain.Team
|
||||
import com.matchlivetv.match_live_tv.domain.User
|
||||
@@ -30,10 +31,31 @@ data class UserDto(
|
||||
fun toDomain() = User(id, email, name, role ?: "coach")
|
||||
}
|
||||
|
||||
data class SportDto(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val board: String,
|
||||
val overlay: String,
|
||||
@Json(name = "allowed_overlays") val allowedOverlays: List<String> = emptyList(),
|
||||
@Json(name = "default_rules") val defaultRules: Map<String, Int> = emptyMap(),
|
||||
) {
|
||||
fun toDomain() = SportOption(
|
||||
key = key,
|
||||
label = label,
|
||||
board = board,
|
||||
overlay = overlay,
|
||||
allowedOverlays = allowedOverlays,
|
||||
defaultRules = defaultRules,
|
||||
)
|
||||
}
|
||||
|
||||
data class TeamDto(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val sport: String? = null,
|
||||
@Json(name = "sport_key") val sportKey: String? = null,
|
||||
@Json(name = "board_type") val boardType: String? = null,
|
||||
@Json(name = "sport_label") val sportLabel: String? = null,
|
||||
@Json(name = "can_stream") val canStream: Boolean? = true,
|
||||
@Json(name = "club_name") val clubName: String? = null,
|
||||
@Json(name = "youtube_enabled") val youtubeEnabled: Boolean? = false,
|
||||
@@ -54,7 +76,9 @@ data class TeamDto(
|
||||
fun toDomain() = Team(
|
||||
id = id,
|
||||
name = name,
|
||||
sport = sport ?: "volleyball",
|
||||
sport = sportKey ?: sport ?: "pallavolo",
|
||||
sportKey = sportKey ?: sport ?: "pallavolo",
|
||||
boardType = boardType ?: "volley",
|
||||
canStream = canStream ?: true,
|
||||
clubName = clubName,
|
||||
logoUrl = logoUrl,
|
||||
@@ -82,6 +106,12 @@ data class MatchDto(
|
||||
val location: String? = null,
|
||||
@Json(name = "scheduled_at") val scheduledAt: String? = null,
|
||||
@Json(name = "sets_to_win") val setsToWin: Int? = null,
|
||||
@Json(name = "sport_key") val sportKey: String? = null,
|
||||
val sport: String? = null,
|
||||
@Json(name = "sport_label") val sportLabel: String? = null,
|
||||
@Json(name = "board_type") val boardType: String? = null,
|
||||
@Json(name = "overlay_kind") val overlayKind: String? = null,
|
||||
@Json(name = "effective_overlay_kind") val effectiveOverlayKind: String? = null,
|
||||
/** Campionato / descrizione torneo (facoltativo). */
|
||||
val category: String? = null,
|
||||
@Json(name = "scoring_rules") val scoringRules: ScoringRulesBody? = null,
|
||||
@@ -103,6 +133,11 @@ data class MatchDto(
|
||||
location = location,
|
||||
scheduledAt = scheduledAt,
|
||||
setsToWin = setsToWin ?: 3,
|
||||
sportKey = sportKey ?: sport ?: "pallavolo",
|
||||
sportLabel = sportLabel,
|
||||
boardType = boardType ?: "volley",
|
||||
overlayKind = overlayKind,
|
||||
effectiveOverlayKind = effectiveOverlayKind ?: overlayKind ?: "volley",
|
||||
category = category,
|
||||
scoringRules = scoringRules?.toDomain(),
|
||||
activeSessionId = activeSessionId,
|
||||
@@ -212,34 +247,51 @@ data class UpdateMatchBody(
|
||||
data class UpdateMatchRequest(val match: UpdateMatchBody)
|
||||
|
||||
data class ScoringRulesBody(
|
||||
@Json(name = "points_per_set") val pointsPerSet: Int,
|
||||
@Json(name = "points_deciding_set") val pointsDecidingSet: Int,
|
||||
@Json(name = "min_point_lead") val minPointLead: Int,
|
||||
@Json(name = "sets_to_win") val setsToWin: Int? = null,
|
||||
@Json(name = "points_per_set") val pointsPerSet: Int? = null,
|
||||
@Json(name = "points_deciding_set") val pointsDecidingSet: Int? = null,
|
||||
@Json(name = "min_point_lead") val minPointLead: Int? = null,
|
||||
val periods: Int? = null,
|
||||
@Json(name = "period_duration_secs") val periodDurationSecs: Int? = null,
|
||||
@Json(name = "overtime_duration_secs") val overtimeDurationSecs: Int? = null,
|
||||
) {
|
||||
fun toDomain() = ScoringRules(
|
||||
pointsPerSet = pointsPerSet,
|
||||
pointsDecidingSet = pointsDecidingSet,
|
||||
minPointLead = minPointLead,
|
||||
pointsPerSet = pointsPerSet ?: 25,
|
||||
pointsDecidingSet = pointsDecidingSet ?: 15,
|
||||
minPointLead = minPointLead ?: 2,
|
||||
periods = periods ?: 4,
|
||||
periodDurationSecs = periodDurationSecs ?: 600,
|
||||
overtimeDurationSecs = overtimeDurationSecs ?: 300,
|
||||
)
|
||||
}
|
||||
|
||||
fun ScoringRules.toBody() = ScoringRulesBody(
|
||||
fun ScoringRules.toBody(setsToWin: Int? = null) = ScoringRulesBody(
|
||||
setsToWin = setsToWin,
|
||||
pointsPerSet = pointsPerSet,
|
||||
pointsDecidingSet = pointsDecidingSet,
|
||||
minPointLead = minPointLead,
|
||||
periods = periods,
|
||||
periodDurationSecs = periodDurationSecs,
|
||||
overtimeDurationSecs = overtimeDurationSecs,
|
||||
)
|
||||
|
||||
fun ScoringRulesBody.toMap(): Map<String, Int> = mapOf(
|
||||
"points_per_set" to pointsPerSet,
|
||||
"points_deciding_set" to pointsDecidingSet,
|
||||
"min_point_lead" to minPointLead,
|
||||
)
|
||||
fun ScoringRulesBody.toMap(): Map<String, Int> = buildMap {
|
||||
setsToWin?.let { put("sets_to_win", it) }
|
||||
pointsPerSet?.let { put("points_per_set", it) }
|
||||
pointsDecidingSet?.let { put("points_deciding_set", it) }
|
||||
minPointLead?.let { put("min_point_lead", it) }
|
||||
periods?.let { put("periods", it) }
|
||||
periodDurationSecs?.let { put("period_duration_secs", it) }
|
||||
overtimeDurationSecs?.let { put("overtime_duration_secs", it) }
|
||||
}
|
||||
|
||||
data class UpdateMatchBodyFull(
|
||||
@Json(name = "opponent_name") val opponentName: String,
|
||||
val location: String? = null,
|
||||
@Json(name = "scheduled_at") val scheduledAt: String? = null,
|
||||
@Json(name = "sets_to_win") val setsToWin: Int,
|
||||
@Json(name = "sport_key") val sportKey: String? = null,
|
||||
@Json(name = "overlay_kind") val overlayKind: String? = null,
|
||||
val category: String? = null,
|
||||
@Json(name = "opponent_primary_color") val opponentPrimaryColor: String? = null,
|
||||
@Json(name = "scoring_rules") val scoringRules: Map<String, Int>? = null,
|
||||
|
||||
@@ -23,6 +23,9 @@ interface MatchLiveApi {
|
||||
@POST("auth/logout")
|
||||
suspend fun logout()
|
||||
|
||||
@GET("sports")
|
||||
suspend fun sports(): List<SportDto>
|
||||
|
||||
@GET("teams")
|
||||
suspend fun teams(): List<TeamDto>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.matchlivetv.match_live_tv.data.api.UpdateMatchRequestFull
|
||||
import com.matchlivetv.match_live_tv.data.api.UpdateTeamBody
|
||||
import com.matchlivetv.match_live_tv.data.api.UpdateTeamRequest
|
||||
import com.matchlivetv.match_live_tv.domain.Match
|
||||
import com.matchlivetv.match_live_tv.domain.SportOption
|
||||
import com.matchlivetv.match_live_tv.domain.Team
|
||||
import com.matchlivetv.match_live_tv.domain.coachHubVisible
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -25,6 +26,9 @@ class MatchRepository(
|
||||
private val tokenStore: TokenStore,
|
||||
private val appContext: Context,
|
||||
) {
|
||||
suspend fun fetchSports(): List<SportOption> =
|
||||
api.sports().map { it.toDomain() }
|
||||
|
||||
suspend fun fetchTeams(): List<Team> =
|
||||
api.teams().map { it.toDomain() }
|
||||
|
||||
@@ -115,6 +119,8 @@ class MatchRepository(
|
||||
category: String?,
|
||||
opponentPrimaryColor: String?,
|
||||
scoringRules: ScoringRulesBody?,
|
||||
sportKey: String? = null,
|
||||
overlayKind: String? = null,
|
||||
opponentLogoUri: Uri? = null,
|
||||
): Match {
|
||||
val body = UpdateMatchBodyFull(
|
||||
@@ -122,9 +128,11 @@ class MatchRepository(
|
||||
location = location?.takeIf { it.isNotBlank() },
|
||||
scheduledAt = scheduledAt,
|
||||
setsToWin = setsToWin,
|
||||
sportKey = sportKey,
|
||||
overlayKind = overlayKind,
|
||||
category = category?.takeIf { it.isNotBlank() },
|
||||
opponentPrimaryColor = opponentPrimaryColor,
|
||||
scoringRules = scoringRules?.toMap() ?: emptyMap(),
|
||||
scoringRules = scoringRules?.toMap().takeIf { !it.isNullOrEmpty() },
|
||||
)
|
||||
if (opponentLogoUri != null) {
|
||||
return api.updateMatchMultipart(
|
||||
|
||||
@@ -10,6 +10,10 @@ data class MatchScoringRules(
|
||||
val pointsPerSet: Int = 25,
|
||||
val pointsDecidingSet: Int = 15,
|
||||
val minPointLead: Int = 2,
|
||||
val periods: Int = 4,
|
||||
val periodDurationSecs: Int = 600,
|
||||
val overtimeDurationSecs: Int = 300,
|
||||
val boardType: String = "volley",
|
||||
) {
|
||||
val maxSets: Int get() = (setsToWin * 2) - 1
|
||||
|
||||
@@ -35,6 +39,18 @@ data class MatchScoringRules(
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromMatch(match: Match) = MatchScoringRules(setsToWin = match.setsToWin)
|
||||
fun fromMatch(match: Match): MatchScoringRules {
|
||||
val rules = match.scoringRules
|
||||
return MatchScoringRules(
|
||||
setsToWin = match.setsToWin,
|
||||
pointsPerSet = rules?.pointsPerSet ?: 25,
|
||||
pointsDecidingSet = rules?.pointsDecidingSet ?: 15,
|
||||
minPointLead = rules?.minPointLead ?: 2,
|
||||
periods = rules?.periods ?: 4,
|
||||
periodDurationSecs = rules?.periodDurationSecs ?: 600,
|
||||
overtimeDurationSecs = rules?.overtimeDurationSecs ?: 300,
|
||||
boardType = match.boardType,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,21 @@ data class AuthTokens(
|
||||
val refreshToken: String,
|
||||
)
|
||||
|
||||
data class SportOption(
|
||||
val key: String,
|
||||
val label: String,
|
||||
val board: String,
|
||||
val overlay: String,
|
||||
val allowedOverlays: List<String>,
|
||||
val defaultRules: Map<String, Int> = emptyMap(),
|
||||
)
|
||||
|
||||
data class Team(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val sport: String,
|
||||
val sportKey: String = sport,
|
||||
val boardType: String = "volley",
|
||||
val clubName: String? = null,
|
||||
val logoUrl: String? = null,
|
||||
val primaryColor: String? = null,
|
||||
@@ -59,6 +70,9 @@ data class ScoringRules(
|
||||
val pointsPerSet: Int = 25,
|
||||
val pointsDecidingSet: Int = 15,
|
||||
val minPointLead: Int = 2,
|
||||
val periods: Int = 4,
|
||||
val periodDurationSecs: Int = 600,
|
||||
val overtimeDurationSecs: Int = 300,
|
||||
)
|
||||
|
||||
data class Match(
|
||||
@@ -68,6 +82,11 @@ data class Match(
|
||||
val opponentName: String,
|
||||
val location: String? = null,
|
||||
val scheduledAt: String? = null,
|
||||
val sportKey: String = "pallavolo",
|
||||
val sportLabel: String? = null,
|
||||
val boardType: String = "volley",
|
||||
val overlayKind: String? = null,
|
||||
val effectiveOverlayKind: String = "volley",
|
||||
val setsToWin: Int = 3,
|
||||
/** Campionato / descrizione torneo (facoltativo). */
|
||||
val category: String? = null,
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.matchlivetv.match_live_tv.streaming.overlay
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/** Tabellone compatto per basket, timed e sport a punteggio semplice. */
|
||||
class CompactScoreboardElement : OverlayElement {
|
||||
|
||||
private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.argb(210, 12, 12, 12)
|
||||
}
|
||||
private val labelPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
}
|
||||
private val metaPaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.parseColor("#AAAAAA")
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL)
|
||||
}
|
||||
private val scorePaint = TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.WHITE
|
||||
typeface = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD)
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, state: OverlayState, layout: OverlayLayout) {
|
||||
val compact = state.compactScoreboard ?: return
|
||||
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 left = layout.marginPx.toFloat()
|
||||
val top = layout.marginPx.toFloat()
|
||||
val rect = RectF(left, top, left + w, top + h)
|
||||
canvas.drawRoundRect(rect, 12f, 12f, backgroundPaint)
|
||||
|
||||
labelPaint.textSize = (h * 0.18f).coerceAtLeast(14f)
|
||||
scorePaint.textSize = (h * 0.34f).coerceAtLeast(22f)
|
||||
metaPaint.textSize = (h * 0.16f).coerceAtLeast(12f)
|
||||
|
||||
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,
|
||||
scorePaint,
|
||||
)
|
||||
val meta = listOfNotNull(compact.periodLabel, compact.clockText).joinToString(" · ")
|
||||
if (meta.isNotBlank()) {
|
||||
canvas.drawText(meta, rect.left + 16f, rect.bottom - 10f, metaPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class OverlayCanvasRenderer(context: Context) {
|
||||
private val elements: List<OverlayElement> = listOf(
|
||||
WatermarkElement(context),
|
||||
ScoreboardElement(),
|
||||
CompactScoreboardElement(),
|
||||
TimerElement(),
|
||||
SponsorElement(),
|
||||
)
|
||||
|
||||
@@ -42,6 +42,13 @@ fun ScoreState.toScoreboardState(
|
||||
)
|
||||
}
|
||||
|
||||
fun formatOverlayClock(secs: Int, countUp: Boolean = false): String {
|
||||
if (secs <= 0 && !countUp) return "0:00"
|
||||
val m = secs / 60
|
||||
val s = secs % 60
|
||||
return "%d:%02d".format(m, s)
|
||||
}
|
||||
|
||||
fun BroadcastPhase.toOverlayStatus(): BroadcastOverlayStatus = when (this) {
|
||||
BroadcastPhase.LIVE -> BroadcastOverlayStatus.LIVE
|
||||
BroadcastPhase.PAUSED -> BroadcastOverlayStatus.PAUSED
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.matchlivetv.match_live_tv.streaming.overlay
|
||||
|
||||
/**
|
||||
* Stato grafico dell'overlay video. Indipendente dalla logica sportiva:
|
||||
* il tabellone è descritto da [ScoreboardState], non da regole di gioco.
|
||||
* il tabellone è descritto da [ScoreboardState] / [CompactScoreboardState].
|
||||
*/
|
||||
data class ScoreboardSetColumn(
|
||||
val setNumber: Int,
|
||||
@@ -21,6 +21,38 @@ data class ScoreboardState(
|
||||
val awayLogoUrl: String? = null,
|
||||
)
|
||||
|
||||
data class CompactScoreboardState(
|
||||
val homeTeamName: String,
|
||||
val awayTeamName: String,
|
||||
val homeScore: Int,
|
||||
val awayScore: Int,
|
||||
val periodLabel: String? = null,
|
||||
val clockText: String? = null,
|
||||
val homeAccentColor: Int = 0xFFFF2D2D.toInt(),
|
||||
val awayAccentColor: Int = 0xFF1E3A8A.toInt(),
|
||||
)
|
||||
|
||||
enum class OverlayKind {
|
||||
NONE,
|
||||
TIMER,
|
||||
VOLLEY,
|
||||
BASKET,
|
||||
TIMED,
|
||||
RACKET,
|
||||
;
|
||||
|
||||
companion object {
|
||||
fun fromApi(value: String?): OverlayKind = when (value?.lowercase()) {
|
||||
"none" -> NONE
|
||||
"timer" -> TIMER
|
||||
"basket" -> BASKET
|
||||
"timed" -> TIMED
|
||||
"racket" -> RACKET
|
||||
else -> VOLLEY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BroadcastOverlayStatus {
|
||||
PREVIEW,
|
||||
CONNECTING,
|
||||
@@ -29,18 +61,22 @@ enum class BroadcastOverlayStatus {
|
||||
}
|
||||
|
||||
data class OverlayState(
|
||||
val overlayKind: OverlayKind = OverlayKind.VOLLEY,
|
||||
val scoreboard: ScoreboardState? = null,
|
||||
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 = watermarkVisible.hashCode()
|
||||
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)
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -63,4 +99,14 @@ private fun ScoreboardState.fingerprint(): Int {
|
||||
return result
|
||||
}
|
||||
|
||||
private fun CompactScoreboardState.fingerprint(): Int {
|
||||
var result = homeTeamName.hashCode()
|
||||
result = 31 * result + awayTeamName.hashCode()
|
||||
result = 31 * result + homeScore
|
||||
result = 31 * result + awayScore
|
||||
result = 31 * result + (periodLabel?.hashCode() ?: 0)
|
||||
result = 31 * result + (clockText?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun logoLoadedKey(url: String?): Int = if (OverlayLogoCache.get(url) != null) 1 else 0
|
||||
|
||||
@@ -62,6 +62,7 @@ class ScoreboardElement : OverlayElement {
|
||||
private val liveRect = RectF()
|
||||
|
||||
override fun draw(canvas: Canvas, state: OverlayState, layout: OverlayLayout) {
|
||||
if (state.overlayKind !in setOf(OverlayKind.VOLLEY, OverlayKind.RACKET)) return
|
||||
val board = state.scoreboard ?: return
|
||||
if (board.columns.isEmpty()) return
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class TimerElement : OverlayElement {
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -16,6 +16,7 @@ class WatermarkElement(context: Context) : OverlayElement {
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas, state: OverlayState, layout: OverlayLayout) {
|
||||
if (state.overlayKind == OverlayKind.NONE) return
|
||||
if (!state.watermarkVisible) return
|
||||
|
||||
val targetWidth = (layout.canvasWidth * LOGO_WIDTH_RATIO).roundToInt().coerceIn(28, 72)
|
||||
|
||||
@@ -39,8 +39,11 @@ import com.matchlivetv.match_live_tv.domain.StreamSession
|
||||
import com.matchlivetv.match_live_tv.streaming.BroadcastConfig
|
||||
import com.matchlivetv.match_live_tv.streaming.BroadcastPhase
|
||||
import com.matchlivetv.match_live_tv.streaming.LivePreviewView
|
||||
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
|
||||
@@ -219,23 +222,50 @@ fun BroadcastScreen(
|
||||
val currentMatch = match ?: return@LaunchedEffect
|
||||
val homeLogoUrl = resolveMediaUrl(currentMatch.homeLogoUrl)
|
||||
val awayLogoUrl = resolveMediaUrl(currentMatch.opponentLogoUrl)
|
||||
container.broadcastCoordinator.updateOverlay(
|
||||
OverlayState(
|
||||
val overlayKind = OverlayKind.fromApi(currentMatch.effectiveOverlayKind)
|
||||
val homeColor = parseColorHex(currentMatch.homePrimaryColor, 0xFFFF2D2D.toInt())
|
||||
val awayColor = parseColorHex(currentMatch.opponentPrimaryColor, 0xFF1E3A8A.toInt())
|
||||
val overlayState = when (overlayKind) {
|
||||
OverlayKind.NONE -> OverlayState(
|
||||
overlayKind = overlayKind,
|
||||
watermarkVisible = false,
|
||||
broadcastStatus = metrics.phase.toOverlayStatus(),
|
||||
)
|
||||
OverlayKind.TIMER -> OverlayState(
|
||||
overlayKind = overlayKind,
|
||||
timerText = formatOverlayClock(score.homePoints, countUp = true),
|
||||
watermarkVisible = true,
|
||||
broadcastStatus = metrics.phase.toOverlayStatus(),
|
||||
)
|
||||
OverlayKind.BASKET, OverlayKind.TIMED -> OverlayState(
|
||||
overlayKind = overlayKind,
|
||||
compactScoreboard = CompactScoreboardState(
|
||||
homeTeamName = currentMatch.teamName,
|
||||
awayTeamName = currentMatch.opponentName,
|
||||
homeScore = score.homePoints,
|
||||
awayScore = score.awayPoints,
|
||||
periodLabel = "${score.currentSet}°",
|
||||
homeAccentColor = homeColor,
|
||||
awayAccentColor = awayColor,
|
||||
),
|
||||
watermarkVisible = true,
|
||||
broadcastStatus = metrics.phase.toOverlayStatus(),
|
||||
)
|
||||
else -> OverlayState(
|
||||
overlayKind = overlayKind,
|
||||
scoreboard = score.toScoreboardState(
|
||||
homeTeamName = currentMatch.teamName,
|
||||
awayTeamName = currentMatch.opponentName,
|
||||
homeAccentColor = parseColorHex(currentMatch.homePrimaryColor, 0xFFFF2D2D.toInt()),
|
||||
awayAccentColor = parseColorHex(
|
||||
currentMatch.opponentPrimaryColor,
|
||||
0xFF1E3A8A.toInt(),
|
||||
),
|
||||
homeAccentColor = homeColor,
|
||||
awayAccentColor = awayColor,
|
||||
homeLogoUrl = homeLogoUrl,
|
||||
awayLogoUrl = awayLogoUrl,
|
||||
),
|
||||
watermarkVisible = true,
|
||||
broadcastStatus = metrics.phase.toOverlayStatus(),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
container.broadcastCoordinator.updateOverlay(overlayState)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
@@ -31,7 +31,9 @@ import com.matchlivetv.match_live_tv.core.normalizeHexColor
|
||||
import com.matchlivetv.match_live_tv.data.AppContainer
|
||||
import com.matchlivetv.match_live_tv.data.api.ScoringRulesBody
|
||||
import com.matchlivetv.match_live_tv.domain.Match
|
||||
import com.matchlivetv.match_live_tv.domain.SportOption
|
||||
import com.matchlivetv.match_live_tv.domain.Team
|
||||
import com.matchlivetv.match_live_tv.streaming.overlay.OverlayKind
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.matches.MatchOutlinedField
|
||||
@@ -111,6 +113,19 @@ fun StepMatchScreen(
|
||||
var pointsPerSetText by remember { mutableStateOf(pointsPerSet.toString()) }
|
||||
var pointsDecidingSetText by remember { mutableStateOf(pointsDecidingSet.toString()) }
|
||||
var saving by remember { mutableStateOf(false) }
|
||||
var sports by remember { mutableStateOf<List<SportOption>>(emptyList()) }
|
||||
var selectedSportKey by remember { mutableStateOf(match.sportKey) }
|
||||
var selectedOverlay by remember { mutableStateOf(match.overlayKind ?: match.effectiveOverlayKind) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
runCatching { container.matchRepository.fetchSports() }
|
||||
.onSuccess { sports = it }
|
||||
}
|
||||
|
||||
val currentSport = sports.find { it.key == selectedSportKey }
|
||||
?: sports.find { it.key == homeTeam?.sportKey }
|
||||
val allowedOverlays = currentSport?.allowedOverlays.orEmpty()
|
||||
val boardType = currentSport?.board ?: homeTeam?.boardType ?: match.boardType
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
@@ -174,6 +189,35 @@ fun StepMatchScreen(
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
WizardReadOnlyField(
|
||||
label = "Sport",
|
||||
value = currentSport?.label ?: match.sportLabel ?: selectedSportKey,
|
||||
)
|
||||
if (allowedOverlays.isNotEmpty()) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Text("Overlay video", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
allowedOverlays.forEach { overlayKey ->
|
||||
val selected = selectedOverlay == overlayKey
|
||||
val label = OverlayKind.fromApi(overlayKey).name.lowercase()
|
||||
.replaceFirstChar { it.uppercase() }
|
||||
if (selected) {
|
||||
MatchPrimaryButton(
|
||||
label = label,
|
||||
onClick = { selectedOverlay = overlayKey },
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp),
|
||||
)
|
||||
} else {
|
||||
MatchSecondaryButton(
|
||||
label = label,
|
||||
onClick = { selectedOverlay = overlayKey },
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(20.dp))
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
@@ -195,7 +239,7 @@ fun StepMatchScreen(
|
||||
Switch(checked = customRules, onCheckedChange = { customRules = it })
|
||||
}
|
||||
|
||||
if (customRules) {
|
||||
if (customRules && boardType in setOf("volley", "racket")) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Set da vincere la partita", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
@@ -262,8 +306,9 @@ fun StepMatchScreen(
|
||||
}
|
||||
|
||||
val resolvedSets = if (customRules) setsToWin else DEFAULT_SETS_TO_WIN
|
||||
val resolvedRules: ScoringRulesBody? = if (customRules) {
|
||||
val resolvedRules: ScoringRulesBody? = if (customRules && boardType in setOf("volley", "racket")) {
|
||||
ScoringRulesBody(
|
||||
setsToWin = resolvedSets,
|
||||
pointsPerSet = pointsPerSet,
|
||||
pointsDecidingSet = pointsDecidingSet,
|
||||
minPointLead = existingRules?.minPointLead ?: DEFAULT_MIN_POINT_LEAD,
|
||||
@@ -298,6 +343,10 @@ fun StepMatchScreen(
|
||||
category = campionato.trim(),
|
||||
opponentPrimaryColor = opponentPrimaryColor,
|
||||
scoringRules = resolvedRules,
|
||||
sportKey = selectedSportKey,
|
||||
overlayKind = selectedOverlay.takeIf {
|
||||
it != currentSport?.overlay
|
||||
},
|
||||
opponentLogoUri = opponentLogoUri,
|
||||
)
|
||||
}.onSuccess { updated ->
|
||||
|
||||
Reference in New Issue
Block a user