Completa multi-sport su web, API score_action e controlli Android board-aware.
Allinea i form società/squadra al catalogo sport, espone score_action per basket/timed e corregge bug namespace Sports e Result nel period engine; l'app nativa gestisce overlay e punteggio per board con wizard regole esteso. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@ module Api
|
||||
module V1
|
||||
class SportsController < BaseController
|
||||
def index
|
||||
render json: Sports::Catalog.as_api_list
|
||||
render json: ::Sports::Catalog.as_api_list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,6 +39,20 @@ module Api
|
||||
render json: session_json(@session, detail: true)
|
||||
end
|
||||
|
||||
def score_action
|
||||
action = params[:score_action].presence
|
||||
raise ArgumentError, "Azione mancante" if action.blank?
|
||||
|
||||
result = Scoring::ApplyAction.new(session: @session, action: action).call
|
||||
render json: session_json(@session, detail: true).merge(
|
||||
set_won: result.set_won,
|
||||
match_won: result.match_won,
|
||||
winner: result.winner&.to_s
|
||||
)
|
||||
rescue ArgumentError => e
|
||||
render json: { error: e.message }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def events
|
||||
events = @session.stream_events.recent.limit(100)
|
||||
render json: events.map { |e| event_json(e) }
|
||||
|
||||
@@ -21,7 +21,7 @@ module Api
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
sport_key = Sports::Catalog.normalize_key(team_params[:sport_key] || team_params[:sport] || "pallavolo")
|
||||
sport_key = ::Sports::Catalog.normalize_key(team_params[:sport_key] || team_params[:sport] || "pallavolo")
|
||||
club = Club.create!(name: team_params[:name], sport: sport_key,
|
||||
primary_color: "#e53935", secondary_color: "#ffffff")
|
||||
ClubMembership.create!(user: current_user, club: club, role: "owner")
|
||||
|
||||
@@ -190,6 +190,7 @@ module Public
|
||||
)
|
||||
p[:primary_color] = normalize_hex_color(p[:primary_color], "#e53935")
|
||||
p[:secondary_color] = normalize_hex_color(p[:secondary_color], "#ffffff")
|
||||
p[:sport] = Sports::Catalog.normalize_key(p[:sport]) if p[:sport].present?
|
||||
p
|
||||
end
|
||||
|
||||
|
||||
@@ -165,7 +165,12 @@ module Public
|
||||
end
|
||||
|
||||
def team_params
|
||||
p = params.require(:team).permit(:name, :sport, :description, :logo_url, :primary_color, :secondary_color)
|
||||
p = params.require(:team).permit(:name, :sport, :sport_key, :description, :logo_url, :primary_color, :secondary_color)
|
||||
if p[:sport].present? && p[:sport_key].blank?
|
||||
p[:sport_key] = p.delete(:sport)
|
||||
elsif p[:sport_key].present?
|
||||
p.delete(:sport)
|
||||
end
|
||||
p[:primary_color] = p[:primary_color].presence
|
||||
p[:secondary_color] = p[:secondary_color].presence
|
||||
if p[:primary_color].present?
|
||||
|
||||
@@ -16,4 +16,9 @@ module ApplicationHelper
|
||||
value = date_or_time.respond_to?(:in_time_zone) ? date_or_time.in_time_zone : date_or_time
|
||||
I18n.with_locale(:it) { I18n.l(value, format: format) }
|
||||
end
|
||||
|
||||
def sport_catalog_options(selected = nil)
|
||||
options = Sports::Catalog.as_api_list.map { |entry| [entry[:label], entry[:key]] }
|
||||
options_for_select(options, Sports::Catalog.normalize_key(selected))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
module Scoring
|
||||
module Engines
|
||||
module PeriodEngineMixin
|
||||
Result = Scoring::ApplyAction::Result
|
||||
|
||||
private
|
||||
|
||||
def period_rules
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<%= label_tag "club[name]", "Nome società" %>
|
||||
<%= text_field_tag "club[name]", @club.name, required: true %>
|
||||
<%= label_tag "club[sport]", "Sport principale" %>
|
||||
<%= select_tag "club[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], @club.sport) %>
|
||||
<%= select_tag "club[sport]", sport_catalog_options(@club.sport) %>
|
||||
<%= render "shared/branding_fields", record: @club, legend: "Logo e colori societari" %>
|
||||
<%= render "shared/billing_profile_fields", record: @club %>
|
||||
<%= submit_tag "Salva", class: "btn btn-primary" %>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<%= label_tag "club[name]", "Nome società / club" %>
|
||||
<%= text_field_tag "club[name]", params.dig(:club, :name), required: true, placeholder: "es. Crazy Volley Rozzano" %>
|
||||
<%= label_tag "club[sport]", "Sport principale" %>
|
||||
<%= select_tag "club[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], params.dig(:club, :sport) || "volleyball") %>
|
||||
<%= select_tag "club[sport]", sport_catalog_options(params.dig(:club, :sport) || "pallavolo") %>
|
||||
<%= render "shared/branding_fields", record: Club.new(primary_color: "#e53935", secondary_color: "#ffffff"), legend: "Logo e colori societari" %>
|
||||
<%= render "shared/billing_profile_fields", record: Club.new(billing_country: "IT") %>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<%= label_tag "team[name]", "Nome squadra" %>
|
||||
<%= text_field_tag "team[name]", @team.name, required: true %>
|
||||
<%= label_tag "team[sport]", "Sport" %>
|
||||
<%= select_tag "team[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], @team.sport) %>
|
||||
<%= select_tag "team[sport]", sport_catalog_options(@team.sport_key) %>
|
||||
|
||||
<%= label_tag "team[description]", "Descrizione squadra" %>
|
||||
<%= text_area_tag "team[description]", @team.description, rows: 5, placeholder: "Presentazione della squadra, palmarès, obiettivi della stagione…" %>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<%= label_tag "team[name]", "Nome squadra" %>
|
||||
<%= text_field_tag "team[name]", nil, required: true, placeholder: "es. Under 13, Serie C" %>
|
||||
<%= label_tag "team[sport]", "Sport" %>
|
||||
<%= select_tag "team[sport]", options_for_select([["Pallavolo", "volleyball"], ["Calcio", "football"], ["Basket", "basketball"]], @club.sport) %>
|
||||
<%= select_tag "team[sport]", sport_catalog_options(@club.sport) %>
|
||||
<%= render "shared/branding_fields", record: Team.new(club: @club), show_inherit_hint: true, legend: "Override branding (opzionale)" %>
|
||||
<%= submit_tag "Aggiungi squadra", class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
|
||||
@@ -43,6 +43,7 @@ Rails.application.routes.draw do
|
||||
patch :pause
|
||||
patch :resume
|
||||
patch :score
|
||||
post :score_action
|
||||
get :events
|
||||
post :telemetry
|
||||
post :pairing_token
|
||||
|
||||
10
backend/db/schema.rb
generated
10
backend/db/schema.rb
generated
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_06_09_100000) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "pgcrypto"
|
||||
enable_extension "plpgsql"
|
||||
@@ -147,7 +147,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
|
||||
t.string "opponent_name", null: false
|
||||
t.string "location"
|
||||
t.datetime "scheduled_at"
|
||||
t.string "sport", default: "volleyball"
|
||||
t.string "sport_key", default: "pallavolo"
|
||||
t.integer "sets_to_win", default: 3
|
||||
t.jsonb "roster_numbers", default: []
|
||||
t.string "category"
|
||||
@@ -155,6 +155,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.jsonb "scoring_rules", default: {}, null: false
|
||||
t.string "opponent_primary_color"
|
||||
t.string "overlay_kind"
|
||||
t.index ["team_id"], name: "index_matches_on_team_id"
|
||||
end
|
||||
|
||||
@@ -217,6 +219,8 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.jsonb "set_partials", default: [], null: false
|
||||
t.string "board_type", default: "volley", null: false
|
||||
t.jsonb "data", default: {}, null: false
|
||||
t.index ["stream_session_id"], name: "index_score_states_on_stream_session_id", unique: true
|
||||
end
|
||||
|
||||
@@ -319,7 +323,7 @@ ActiveRecord::Schema[7.2].define(version: 2026_06_03_121000) do
|
||||
|
||||
create_table "teams", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.string "sport", default: "volleyball"
|
||||
t.string "sport_key", default: "pallavolo"
|
||||
t.string "logo_url"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
|
||||
@@ -8,7 +8,7 @@ RSpec.describe Sports::Catalog do
|
||||
entry = described_class.find("pallavolo")
|
||||
expect(entry[:board]).to eq("volley")
|
||||
expect(entry[:overlay]).to eq("volley")
|
||||
expect(entry[:default_rules]["sets_to_win"]).to eq(3)
|
||||
expect(entry[:default_rules][:sets_to_win]).to eq(3)
|
||||
end
|
||||
|
||||
it "mappa legacy volleyball" do
|
||||
|
||||
@@ -12,7 +12,7 @@ android {
|
||||
applicationId = "com.matchlivetv.match_live_tv"
|
||||
minSdk = 24
|
||||
targetSdk = 35
|
||||
versionCode = 16
|
||||
versionCode = 17
|
||||
versionName = "2.0.0-native"
|
||||
|
||||
val apiBaseUrl = project.findProperty("API_BASE_URL") as String?
|
||||
|
||||
@@ -195,6 +195,7 @@ data class SetPartialDto(
|
||||
)
|
||||
|
||||
data class ScoreStateDto(
|
||||
@Json(name = "board_type") val boardType: String? = null,
|
||||
@Json(name = "home_sets") val homeSets: Int? = null,
|
||||
@Json(name = "away_sets") val awaySets: Int? = null,
|
||||
@Json(name = "home_points") val homePoints: Int? = null,
|
||||
@@ -203,6 +204,9 @@ data class ScoreStateDto(
|
||||
@Json(name = "set_partials") val setPartials: List<SetPartialDto>? = null,
|
||||
@Json(name = "timeout_home") val timeoutHome: Boolean? = null,
|
||||
@Json(name = "timeout_away") val timeoutAway: Boolean? = null,
|
||||
val period: String? = null,
|
||||
@Json(name = "clock_secs") val clockSecs: Int? = null,
|
||||
@Json(name = "clock_running") val clockRunning: Boolean? = null,
|
||||
) {
|
||||
fun toDomain() = com.matchlivetv.match_live_tv.domain.ScoreState(
|
||||
homeSets = homeSets ?: 0,
|
||||
@@ -215,9 +219,18 @@ data class ScoreStateDto(
|
||||
},
|
||||
timeoutHome = timeoutHome ?: false,
|
||||
timeoutAway = timeoutAway ?: false,
|
||||
boardType = boardType ?: "volley",
|
||||
period = currentSet ?: 1,
|
||||
periodLabel = period,
|
||||
clockSecs = clockSecs ?: 0,
|
||||
clockRunning = clockRunning ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
data class ScoreActionRequest(
|
||||
@Json(name = "score_action") val scoreAction: String,
|
||||
)
|
||||
|
||||
data class ScoreSyncRequest(
|
||||
@Json(name = "home_sets") val homeSets: Int,
|
||||
@Json(name = "away_sets") val awaySets: Int,
|
||||
|
||||
@@ -120,6 +120,12 @@ interface MatchLiveApi {
|
||||
@Body body: ScoreSyncRequest,
|
||||
): StreamSessionDto
|
||||
|
||||
@POST("sessions/{sessionId}/score_action")
|
||||
suspend fun applyScoreAction(
|
||||
@Path("sessionId") sessionId: String,
|
||||
@Body body: ScoreActionRequest,
|
||||
): StreamSessionDto
|
||||
|
||||
@POST("sessions/{sessionId}/telemetry")
|
||||
suspend fun postTelemetry(
|
||||
@Path("sessionId") sessionId: String,
|
||||
|
||||
@@ -126,35 +126,7 @@ class SessionCableService(
|
||||
return nested as? Map<String, Any?> ?: raw
|
||||
}
|
||||
|
||||
private fun parseScore(data: Map<String, Any?>): ScoreState {
|
||||
val partialsRaw = data["set_partials"] as? List<*> ?: data["setPartials"] as? List<*> ?: emptyList<Any>()
|
||||
return ScoreState(
|
||||
homeSets = intField(data, "home_sets", "homeSets"),
|
||||
awaySets = intField(data, "away_sets", "awaySets"),
|
||||
homePoints = intField(data, "home_points", "homePoints"),
|
||||
awayPoints = intField(data, "away_points", "awayPoints"),
|
||||
currentSet = intField(data, "current_set", "currentSet").coerceAtLeast(1),
|
||||
setPartials = partialsRaw.mapNotNull { item ->
|
||||
val row = item as? Map<*, *> ?: return@mapNotNull null
|
||||
com.matchlivetv.match_live_tv.domain.SetPartial(
|
||||
set = (row["set"] as? Number)?.toInt() ?: 1,
|
||||
home = (row["home"] as? Number)?.toInt() ?: 0,
|
||||
away = (row["away"] as? Number)?.toInt() ?: 0,
|
||||
)
|
||||
},
|
||||
timeoutHome = data["timeout_home"] as? Boolean ?: false,
|
||||
timeoutAway = data["timeout_away"] as? Boolean ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun intField(data: Map<String, Any?>, snake: String, camel: String): Int {
|
||||
val raw = data[snake] ?: data[camel]
|
||||
return when (raw) {
|
||||
is Number -> raw.toInt()
|
||||
is String -> raw.toIntOrNull() ?: 0
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
private fun parseScore(data: Map<String, Any?>): ScoreState = ScoreState.fromCablePayload(data)
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SessionCableService"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.matchlivetv.match_live_tv.data.repository
|
||||
|
||||
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
|
||||
import com.matchlivetv.match_live_tv.data.api.ScoreActionRequest
|
||||
import com.matchlivetv.match_live_tv.data.api.ScoreSyncRequest
|
||||
import com.matchlivetv.match_live_tv.data.api.SetPartialDto
|
||||
import com.matchlivetv.match_live_tv.domain.ScoreState
|
||||
@@ -24,4 +25,9 @@ class ScoreRepository(
|
||||
)
|
||||
return response.score?.toDomain()
|
||||
}
|
||||
|
||||
suspend fun applyScoreAction(sessionId: String, action: String): ScoreState? {
|
||||
val response = api.applyScoreAction(sessionId, ScoreActionRequest(action))
|
||||
return response.score?.toDomain()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,27 @@ class ScoreController(
|
||||
schedulePush()
|
||||
}
|
||||
|
||||
fun applyAction(action: String) {
|
||||
scheduleAction(action)
|
||||
}
|
||||
|
||||
private fun scheduleAction(action: String) {
|
||||
syncJob?.cancel()
|
||||
syncJob = scope.launch {
|
||||
syncMutex.withLock {
|
||||
val id = sessionId ?: return@withLock
|
||||
runCatching { scoreRepository.applyScoreAction(id, action) }
|
||||
.onSuccess { remote ->
|
||||
suppressRemoteUntilMs = System.currentTimeMillis() + 500
|
||||
if (remote != null) {
|
||||
_score.value = remote
|
||||
sessionCable.sendScoreUpdate(remote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun closeSet() {
|
||||
val current = _score.value
|
||||
val homeWon = current.homePoints > current.awayPoints
|
||||
|
||||
@@ -15,9 +15,17 @@ data class ScoreState(
|
||||
val setPartials: List<SetPartial> = emptyList(),
|
||||
val timeoutHome: Boolean = false,
|
||||
val timeoutAway: Boolean = false,
|
||||
val boardType: String = "volley",
|
||||
val period: Int = 1,
|
||||
val periodLabel: String? = null,
|
||||
val clockSecs: Int = 0,
|
||||
val clockRunning: Boolean = false,
|
||||
val overtime: Boolean = false,
|
||||
) {
|
||||
fun cablePayload(): Map<String, Any?> = mapOf(
|
||||
fun cablePayload(): Map<String, Any?> {
|
||||
val payload = mutableMapOf<String, Any?>(
|
||||
"type" to "score_update",
|
||||
"board_type" to boardType,
|
||||
"home_sets" to homeSets,
|
||||
"away_sets" to awaySets,
|
||||
"home_points" to homePoints,
|
||||
@@ -26,12 +34,88 @@ data class ScoreState(
|
||||
"set_partials" to setPartials.map {
|
||||
mapOf("set" to it.set, "home" to it.home, "away" to it.away)
|
||||
},
|
||||
"timeout_home" to timeoutHome,
|
||||
"timeout_away" to timeoutAway,
|
||||
)
|
||||
if (boardType in PERIOD_BOARDS) {
|
||||
payload["period"] = periodLabel ?: period.toString()
|
||||
payload["clock_secs"] = clockSecs
|
||||
payload["clock_running"] = clockRunning
|
||||
payload["home_score"] = homePoints
|
||||
payload["away_score"] = awayPoints
|
||||
payload["overtime"] = overtime
|
||||
}
|
||||
if (boardType == "timer") {
|
||||
payload["clock_secs"] = clockSecs
|
||||
payload["clock_running"] = clockRunning
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
fun progressKey(): Long =
|
||||
currentSet * 10_000_000_000L +
|
||||
homeSets * 1_000_000L +
|
||||
awaySets * 100_000L +
|
||||
homePoints * 1_000L +
|
||||
awayPoints
|
||||
awayPoints +
|
||||
clockSecs
|
||||
|
||||
companion object {
|
||||
private val PERIOD_BOARDS = setOf("basket", "timed")
|
||||
|
||||
fun fromCablePayload(data: Map<String, Any?>): ScoreState {
|
||||
val boardType = data["board_type"] as? String ?: "volley"
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val nested = data["data"] as? Map<String, Any?>
|
||||
val partialsRaw = data["set_partials"] as? List<*> ?: data["setPartials"] as? List<*> ?: emptyList<Any>()
|
||||
val periodNumber = intFrom(nested, "period")
|
||||
.takeIf { it > 0 }
|
||||
?: intField(data, "current_set", "currentSet").coerceAtLeast(1)
|
||||
return ScoreState(
|
||||
homeSets = intField(data, "home_sets", "homeSets"),
|
||||
awaySets = intField(data, "away_sets", "awaySets"),
|
||||
homePoints = intField(data, "home_points", "homePoints"),
|
||||
awayPoints = intField(data, "away_points", "awayPoints"),
|
||||
currentSet = intField(data, "current_set", "currentSet").coerceAtLeast(1),
|
||||
setPartials = partialsRaw.mapNotNull { item ->
|
||||
val row = item as? Map<*, *> ?: return@mapNotNull null
|
||||
SetPartial(
|
||||
set = (row["set"] as? Number)?.toInt() ?: 1,
|
||||
home = (row["home"] as? Number)?.toInt() ?: 0,
|
||||
away = (row["away"] as? Number)?.toInt() ?: 0,
|
||||
)
|
||||
},
|
||||
timeoutHome = data["timeout_home"] as? Boolean ?: false,
|
||||
timeoutAway = data["timeout_away"] as? Boolean ?: false,
|
||||
boardType = boardType,
|
||||
period = periodNumber,
|
||||
periodLabel = data["period"] as? String,
|
||||
clockSecs = intField(data, "clock_secs", "clockSecs")
|
||||
.takeIf { it > 0 || data.containsKey("clock_secs") || data.containsKey("clockSecs") }
|
||||
?: intFrom(nested, "clock_secs"),
|
||||
clockRunning = data["clock_running"] as? Boolean
|
||||
?: nested?.get("clock_running") as? Boolean
|
||||
?: false,
|
||||
overtime = nested?.get("overtime") as? Boolean ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
private fun intField(data: Map<String, Any?>, snake: String, camel: String): Int {
|
||||
val raw = data[snake] ?: data[camel]
|
||||
return when (raw) {
|
||||
is Number -> raw.toInt()
|
||||
is String -> raw.toIntOrNull() ?: 0
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
private fun intFrom(data: Map<String, Any?>?, key: String): Int {
|
||||
if (data == null) return 0
|
||||
return when (val raw = data[key]) {
|
||||
is Number -> raw.toInt()
|
||||
is String -> raw.toIntOrNull() ?: 0
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import androidx.compose.material.icons.Icons
|
||||
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
|
||||
@@ -63,6 +65,7 @@ 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
|
||||
|
||||
@@ -85,12 +88,19 @@ fun BroadcastControlsOverlay(
|
||||
homeLogoUrl: String?,
|
||||
awayLogoUrl: String?,
|
||||
score: ScoreState,
|
||||
boardType: String,
|
||||
pointsTarget: Int,
|
||||
onPointHome: () -> Unit,
|
||||
onPointAway: () -> Unit,
|
||||
onMinusHome: () -> Unit,
|
||||
onMinusAway: () -> Unit,
|
||||
onCloseSet: () -> Unit,
|
||||
onPoint2Home: (() -> Unit)? = null,
|
||||
onPoint3Home: (() -> Unit)? = null,
|
||||
onPoint2Away: (() -> Unit)? = null,
|
||||
onPoint3Away: (() -> Unit)? = null,
|
||||
onCloseSet: (() -> Unit)? = null,
|
||||
onAdvancePeriod: (() -> Unit)? = null,
|
||||
onClockToggle: (() -> Unit)? = null,
|
||||
onPauseOrResume: () -> Unit,
|
||||
onTerminate: () -> Unit,
|
||||
onShareLive: () -> Unit,
|
||||
@@ -181,12 +191,29 @@ fun BroadcastControlsOverlay(
|
||||
contentDescription = "Condividi link regia",
|
||||
onClick = onShareRegia,
|
||||
)
|
||||
if (onCloseSet != null) {
|
||||
SideIconButton(
|
||||
icon = Icons.Default.Check,
|
||||
contentDescription = "Chiudi set",
|
||||
onClick = onCloseSet,
|
||||
)
|
||||
}
|
||||
if (onAdvancePeriod != null) {
|
||||
SideIconButton(
|
||||
icon = Icons.Default.SkipNext,
|
||||
contentDescription = "Periodo successivo",
|
||||
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(
|
||||
Modifier
|
||||
@@ -223,34 +250,17 @@ fun BroadcastControlsOverlay(
|
||||
points = score.homePoints,
|
||||
onPlus = onPointHome,
|
||||
onMinus = onMinusHome,
|
||||
onPlus2 = onPoint2Home,
|
||||
onPlus3 = onPoint3Home,
|
||||
showBasketButtons = boardType == "basket",
|
||||
alignEnd = false,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
"${score.homePoints} - ${score.awayPoints}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
ScoreCenterPanel(
|
||||
score = score,
|
||||
boardType = boardType,
|
||||
pointsTarget = pointsTarget,
|
||||
)
|
||||
Text(
|
||||
"Set ${score.currentSet} · $pointsTarget pt",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MatchColors.TextSecondary,
|
||||
)
|
||||
if (score.homeSets > 0 || score.awaySets > 0) {
|
||||
Text(
|
||||
"Set vinti ${score.homeSets}-${score.awaySets}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
TeamScoreColumn(
|
||||
teamLabel = "OSPITE",
|
||||
teamName = awayName,
|
||||
@@ -259,6 +269,9 @@ fun BroadcastControlsOverlay(
|
||||
points = score.awayPoints,
|
||||
onPlus = onPointAway,
|
||||
onMinus = onMinusAway,
|
||||
onPlus2 = onPoint2Away,
|
||||
onPlus3 = onPoint3Away,
|
||||
showBasketButtons = boardType == "basket",
|
||||
alignEnd = true,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
@@ -365,6 +378,63 @@ private fun formatBitrateKbps(kbps: Long): String {
|
||||
return "$kbps kbps"
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ScoreCenterPanel(
|
||||
score: ScoreState,
|
||||
boardType: String,
|
||||
pointsTarget: Int,
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.padding(bottom = 8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(
|
||||
"${score.homePoints} - ${score.awayPoints}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = Color.White,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
when (boardType) {
|
||||
"basket", "timed" -> {
|
||||
Text(
|
||||
score.periodLabel ?: if (boardType == "basket") "Q${score.period}" else "${score.period}° tempo",
|
||||
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 -> {
|
||||
Text(
|
||||
"Set ${score.currentSet} · $pointsTarget pt",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MatchColors.TextSecondary,
|
||||
)
|
||||
if (score.homeSets > 0 || score.awaySets > 0) {
|
||||
Text(
|
||||
"Set vinti ${score.homeSets}-${score.awaySets}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MatchColors.TextSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TeamScoreColumn(
|
||||
teamLabel: String,
|
||||
@@ -374,6 +444,9 @@ private fun TeamScoreColumn(
|
||||
points: Int,
|
||||
onPlus: () -> Unit,
|
||||
onMinus: () -> Unit,
|
||||
onPlus2: (() -> Unit)? = null,
|
||||
onPlus3: (() -> Unit)? = null,
|
||||
showBasketButtons: Boolean = false,
|
||||
alignEnd: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -407,11 +480,19 @@ private fun TeamScoreColumn(
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
if (alignEnd) {
|
||||
if (showBasketButtons) {
|
||||
onPlus3?.let {
|
||||
ScoreIconButton(label = "+3", tooltip = "+3 $teamSide", onClick = it, primary = true)
|
||||
}
|
||||
onPlus2?.let {
|
||||
ScoreIconButton(label = "+2", tooltip = "+2 $teamSide", onClick = it, primary = true)
|
||||
}
|
||||
}
|
||||
ScoreIconButton(
|
||||
label = "+1",
|
||||
tooltip = "Aggiungi punto $teamSide",
|
||||
onClick = onPlus,
|
||||
primary = true,
|
||||
primary = !showBasketButtons,
|
||||
)
|
||||
ScoreIconButton(label = "−", tooltip = "Togli punto $teamSide", onClick = onMinus)
|
||||
} else {
|
||||
@@ -420,8 +501,16 @@ private fun TeamScoreColumn(
|
||||
label = "+1",
|
||||
tooltip = "Aggiungi punto $teamSide",
|
||||
onClick = onPlus,
|
||||
primary = true,
|
||||
primary = !showBasketButtons,
|
||||
)
|
||||
if (showBasketButtons) {
|
||||
onPlus2?.let {
|
||||
ScoreIconButton(label = "+2", tooltip = "+2 $teamSide", onClick = it, primary = true)
|
||||
}
|
||||
onPlus3?.let {
|
||||
ScoreIconButton(label = "+3", tooltip = "+3 $teamSide", onClick = it, primary = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,10 @@ fun BroadcastScreen(
|
||||
)
|
||||
OverlayKind.TIMER -> OverlayState(
|
||||
overlayKind = overlayKind,
|
||||
timerText = formatOverlayClock(score.homePoints, countUp = true),
|
||||
timerText = formatOverlayClock(
|
||||
if (score.boardType == "timer") score.clockSecs else score.homePoints,
|
||||
countUp = true,
|
||||
),
|
||||
watermarkVisible = true,
|
||||
broadcastStatus = metrics.phase.toOverlayStatus(),
|
||||
)
|
||||
@@ -244,7 +247,11 @@ fun BroadcastScreen(
|
||||
awayTeamName = currentMatch.opponentName,
|
||||
homeScore = score.homePoints,
|
||||
awayScore = score.awayPoints,
|
||||
periodLabel = "${score.currentSet}°",
|
||||
periodLabel = score.periodLabel ?: when (overlayKind) {
|
||||
OverlayKind.BASKET -> "Q${score.period}"
|
||||
else -> "${score.period}°"
|
||||
},
|
||||
clockText = formatOverlayClock(score.clockSecs),
|
||||
homeAccentColor = homeColor,
|
||||
awayAccentColor = awayColor,
|
||||
),
|
||||
@@ -286,6 +293,16 @@ 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)
|
||||
@@ -346,6 +363,12 @@ fun BroadcastScreen(
|
||||
val pointsTarget = scoringRules?.pointsTarget(score.currentSet) ?: 25
|
||||
val currentSession = session
|
||||
val currentMatch = match
|
||||
val boardType = currentMatch?.boardType?.ifBlank { score.boardType } ?: score.boardType
|
||||
val usesActionScoring = boardType in setOf("basket", "timed", "timer")
|
||||
|
||||
fun applyScoreAction(action: String) {
|
||||
container.scoreController.applyAction(action)
|
||||
}
|
||||
val shareUrl = currentSession?.watchShareUrl()
|
||||
val shareSubject = currentMatch?.let { "${it.teamName} vs ${it.opponentName}" } ?: "Diretta Match Live TV"
|
||||
|
||||
@@ -419,29 +442,76 @@ fun BroadcastScreen(
|
||||
homeLogoUrl = currentMatch.homeLogoUrl,
|
||||
awayLogoUrl = currentMatch.opponentLogoUrl,
|
||||
score = score,
|
||||
boardType = boardType,
|
||||
pointsTarget = pointsTarget,
|
||||
onPointHome = {
|
||||
scope.launch {
|
||||
if (usesActionScoring) {
|
||||
applyScoreAction("home_point")
|
||||
} else {
|
||||
container.scoreController.incrementHome()
|
||||
scoreActions.afterPointChange(container.scoreController.score.value) {
|
||||
stopStreamPermanently()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onPointAway = {
|
||||
scope.launch {
|
||||
if (usesActionScoring) {
|
||||
applyScoreAction("away_point")
|
||||
} else {
|
||||
container.scoreController.incrementAway()
|
||||
scoreActions.afterPointChange(container.scoreController.score.value) {
|
||||
stopStreamPermanently()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onMinusHome = { container.scoreController.decrementHome() },
|
||||
onMinusAway = { container.scoreController.decrementAway() },
|
||||
onCloseSet = {
|
||||
onMinusHome = {
|
||||
if (usesActionScoring) applyScoreAction("home_undo") else container.scoreController.decrementHome()
|
||||
},
|
||||
onMinusAway = {
|
||||
if (usesActionScoring) applyScoreAction("away_undo") else container.scoreController.decrementAway()
|
||||
},
|
||||
onPoint2Home = if (boardType == "basket") {
|
||||
{ applyScoreAction("home_point_2") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onPoint3Home = if (boardType == "basket") {
|
||||
{ applyScoreAction("home_point_3") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onPoint2Away = if (boardType == "basket") {
|
||||
{ applyScoreAction("away_point_2") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onPoint3Away = if (boardType == "basket") {
|
||||
{ applyScoreAction("away_point_3") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onCloseSet = if (boardType in setOf("volley", "racket")) {
|
||||
{
|
||||
scope.launch {
|
||||
scoreActions.requestCloseSet { stopStreamPermanently() }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onAdvancePeriod = if (boardType in setOf("basket", "timed")) {
|
||||
{ applyScoreAction("advance_period") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClockToggle = if (boardType in setOf("basket", "timed", "timer")) {
|
||||
{ applyScoreAction("clock_toggle") }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onPauseOrResume = {
|
||||
scope.launch {
|
||||
|
||||
@@ -98,10 +98,15 @@ class LiveScoreActions(
|
||||
private val scoreController: ScoreController,
|
||||
private val dialogHost: LiveScoreDialogHost,
|
||||
) {
|
||||
private val usesSetLogic: Boolean
|
||||
get() = rules.boardType in setOf("volley", "racket")
|
||||
|
||||
suspend fun afterPointChange(
|
||||
score: ScoreState,
|
||||
onStopStream: suspend () -> Unit = {},
|
||||
) {
|
||||
if (!usesSetLogic) return
|
||||
|
||||
val winner = rules.setWinnerFromPoints(
|
||||
homePoints = score.homePoints,
|
||||
awayPoints = score.awayPoints,
|
||||
@@ -123,6 +128,8 @@ class LiveScoreActions(
|
||||
}
|
||||
|
||||
suspend fun requestCloseSet(onStopStream: suspend () -> Unit) {
|
||||
if (!usesSetLogic) return
|
||||
|
||||
val score = scoreController.score.value
|
||||
val winner = rules.setWinnerFromPoints(
|
||||
homePoints = score.homePoints,
|
||||
|
||||
@@ -45,15 +45,27 @@ private const val DEFAULT_SETS_TO_WIN = 3
|
||||
private const val DEFAULT_POINTS_PER_SET = 25
|
||||
private const val DEFAULT_POINTS_DECIDING_SET = 15
|
||||
private const val DEFAULT_MIN_POINT_LEAD = 2
|
||||
private const val DEFAULT_PERIODS = 4
|
||||
private const val DEFAULT_PERIOD_DURATION_SECS = 600
|
||||
private const val DEFAULT_OVERTIME_DURATION_SECS = 300
|
||||
private const val DEFAULT_OPPONENT_COLOR = "#1E3A8A"
|
||||
|
||||
/** True solo se la partita ha regole non standard (non i default FIPAV). */
|
||||
private fun matchHasCustomScoring(match: Match): Boolean {
|
||||
if (match.setsToWin != DEFAULT_SETS_TO_WIN) return true
|
||||
/** True se la partita ha regole non standard rispetto ai default del board. */
|
||||
private fun matchHasCustomScoring(match: Match, boardType: String): Boolean {
|
||||
val rules = match.scoringRules ?: return false
|
||||
return rules.pointsPerSet != DEFAULT_POINTS_PER_SET ||
|
||||
return when (boardType) {
|
||||
"basket", "timed" -> {
|
||||
rules.periods != DEFAULT_PERIODS ||
|
||||
rules.periodDurationSecs != DEFAULT_PERIOD_DURATION_SECS ||
|
||||
rules.overtimeDurationSecs != DEFAULT_OVERTIME_DURATION_SECS
|
||||
}
|
||||
else -> {
|
||||
if (match.setsToWin != DEFAULT_SETS_TO_WIN) return true
|
||||
rules.pointsPerSet != DEFAULT_POINTS_PER_SET ||
|
||||
rules.pointsDecidingSet != DEFAULT_POINTS_DECIDING_SET ||
|
||||
rules.minPointLead != DEFAULT_MIN_POINT_LEAD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -102,7 +114,7 @@ fun StepMatchScreen(
|
||||
) { uri -> opponentLogoUri = uri }
|
||||
|
||||
val existingRules = match.scoringRules
|
||||
var customRules by remember { mutableStateOf(matchHasCustomScoring(match)) }
|
||||
var customRules by remember { mutableStateOf(false) }
|
||||
var setsToWin by remember { mutableIntStateOf(match.setsToWin.coerceIn(2, 3)) }
|
||||
var pointsPerSet by remember {
|
||||
mutableIntStateOf(existingRules?.pointsPerSet ?: DEFAULT_POINTS_PER_SET)
|
||||
@@ -112,6 +124,15 @@ fun StepMatchScreen(
|
||||
}
|
||||
var pointsPerSetText by remember { mutableStateOf(pointsPerSet.toString()) }
|
||||
var pointsDecidingSetText by remember { mutableStateOf(pointsDecidingSet.toString()) }
|
||||
var periods by remember { mutableIntStateOf(existingRules?.periods ?: DEFAULT_PERIODS) }
|
||||
var periodDurationMins by remember {
|
||||
mutableIntStateOf((existingRules?.periodDurationSecs ?: DEFAULT_PERIOD_DURATION_SECS) / 60)
|
||||
}
|
||||
var overtimeDurationMins by remember {
|
||||
mutableIntStateOf((existingRules?.overtimeDurationSecs ?: DEFAULT_OVERTIME_DURATION_SECS) / 60)
|
||||
}
|
||||
var periodDurationText by remember { mutableStateOf(periodDurationMins.toString()) }
|
||||
var overtimeDurationText by remember { mutableStateOf(overtimeDurationMins.toString()) }
|
||||
var saving by remember { mutableStateOf(false) }
|
||||
var sports by remember { mutableStateOf<List<SportOption>>(emptyList()) }
|
||||
var selectedSportKey by remember { mutableStateOf(match.sportKey) }
|
||||
@@ -127,6 +148,10 @@ fun StepMatchScreen(
|
||||
val allowedOverlays = currentSport?.allowedOverlays.orEmpty()
|
||||
val boardType = currentSport?.board ?: homeTeam?.boardType ?: match.boardType
|
||||
|
||||
LaunchedEffect(boardType, match.id) {
|
||||
customRules = matchHasCustomScoring(match, boardType)
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
@@ -227,9 +252,14 @@ fun StepMatchScreen(
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text("Regole punteggio personalizzate")
|
||||
Text(
|
||||
if (customRules) {
|
||||
when {
|
||||
!customRules && boardType in setOf("basket", "timed") ->
|
||||
"Regole standard dello sport selezionato."
|
||||
customRules && boardType in setOf("basket", "timed") ->
|
||||
"Torneo non standard: tempi e periodi personalizzati."
|
||||
customRules ->
|
||||
"Torneo non standard: imposta set e punteggi."
|
||||
} else {
|
||||
else ->
|
||||
"Standard FIPAV: 3 set per vincere, set a 25, tie-break a 15."
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@@ -239,6 +269,51 @@ fun StepMatchScreen(
|
||||
Switch(checked = customRules, onCheckedChange = { customRules = it })
|
||||
}
|
||||
|
||||
if (customRules && boardType in setOf("basket", "timed")) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
if (boardType == "basket") "Quarti" else "Tempi",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf(2, 4).forEach { value ->
|
||||
val selected = periods == value
|
||||
if (selected) {
|
||||
MatchPrimaryButton(
|
||||
label = value.toString(),
|
||||
onClick = { periods = value },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
MatchSecondaryButton(
|
||||
label = value.toString(),
|
||||
onClick = { periods = value },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
MatchOutlinedField(
|
||||
value = periodDurationText,
|
||||
onValueChange = { text ->
|
||||
periodDurationText = text.filter { it.isDigit() }.take(3)
|
||||
periodDurationText.toIntOrNull()?.let { periodDurationMins = it.coerceIn(1, 120) }
|
||||
},
|
||||
label = if (boardType == "basket") "Minuti per quarto" else "Minuti per tempo",
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
MatchOutlinedField(
|
||||
value = overtimeDurationText,
|
||||
onValueChange = { text ->
|
||||
overtimeDurationText = text.filter { it.isDigit() }.take(3)
|
||||
overtimeDurationText.toIntOrNull()?.let { overtimeDurationMins = it.coerceIn(1, 60) }
|
||||
},
|
||||
label = "Minuti supplementari",
|
||||
)
|
||||
}
|
||||
|
||||
if (customRules && boardType in setOf("volley", "racket")) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text("Set da vincere la partita", style = MaterialTheme.typography.bodyMedium)
|
||||
@@ -290,7 +365,7 @@ fun StepMatchScreen(
|
||||
onError("Inserisci il nome avversario")
|
||||
return@MatchPrimaryButton
|
||||
}
|
||||
if (customRules) {
|
||||
if (customRules && boardType in setOf("volley", "racket")) {
|
||||
val perSet = pointsPerSetText.toIntOrNull()
|
||||
val deciding = pointsDecidingSetText.toIntOrNull()
|
||||
if (perSet == null || perSet < 1) {
|
||||
@@ -304,17 +379,39 @@ fun StepMatchScreen(
|
||||
pointsPerSet = perSet
|
||||
pointsDecidingSet = deciding
|
||||
}
|
||||
if (customRules && boardType in setOf("basket", "timed")) {
|
||||
val periodMins = periodDurationText.toIntOrNull()
|
||||
val overtimeMins = overtimeDurationText.toIntOrNull()
|
||||
if (periodMins == null || periodMins < 1) {
|
||||
onError("Inserisci la durata del periodo in minuti")
|
||||
return@MatchPrimaryButton
|
||||
}
|
||||
if (overtimeMins == null || overtimeMins < 1) {
|
||||
onError("Inserisci la durata dei supplementari in minuti")
|
||||
return@MatchPrimaryButton
|
||||
}
|
||||
periodDurationMins = periodMins
|
||||
overtimeDurationMins = overtimeMins
|
||||
}
|
||||
|
||||
val resolvedSets = if (customRules) setsToWin else DEFAULT_SETS_TO_WIN
|
||||
val resolvedRules: ScoringRulesBody? = if (customRules && boardType in setOf("volley", "racket")) {
|
||||
ScoringRulesBody(
|
||||
val resolvedSets = if (customRules && boardType in setOf("volley", "racket")) {
|
||||
setsToWin
|
||||
} else {
|
||||
DEFAULT_SETS_TO_WIN
|
||||
}
|
||||
val resolvedRules: ScoringRulesBody? = when {
|
||||
customRules && boardType in setOf("volley", "racket") -> ScoringRulesBody(
|
||||
setsToWin = resolvedSets,
|
||||
pointsPerSet = pointsPerSet,
|
||||
pointsDecidingSet = pointsDecidingSet,
|
||||
minPointLead = existingRules?.minPointLead ?: DEFAULT_MIN_POINT_LEAD,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
customRules && boardType in setOf("basket", "timed") -> ScoringRulesBody(
|
||||
periods = periods,
|
||||
periodDurationSecs = periodDurationMins * 60,
|
||||
overtimeDurationSecs = overtimeDurationMins * 60,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
|
||||
val initialHomeColor = normalizeHexColor(
|
||||
|
||||
Reference in New Issue
Block a user