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:
2026-06-09 20:11:21 +02:00
parent 53f9a6a884
commit e727069d43
56 changed files with 1783 additions and 223 deletions

View File

@@ -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,

View File

@@ -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>

View File

@@ -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(

View File

@@ -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,
)
}
}
}

View File

@@ -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,

View File

@@ -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)
}
}
}

View File

@@ -15,6 +15,7 @@ class OverlayCanvasRenderer(context: Context) {
private val elements: List<OverlayElement> = listOf(
WatermarkElement(context),
ScoreboardElement(),
CompactScoreboardElement(),
TimerElement(),
SponsorElement(),
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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 ->