diff --git a/backend/app/helpers/public/live_helper.rb b/backend/app/helpers/public/live_helper.rb
index b55b830..e5e50e5 100644
--- a/backend/app/helpers/public/live_helper.rb
+++ b/backend/app/helpers/public/live_helper.rb
@@ -61,57 +61,8 @@ module Public
"#{match.team.name} #{score_state.home_points} - #{score_state.away_points} #{match.opponent_name}"
end
- def live_scorebug_columns(score_state)
- unless score_state
- return {
- labels: ["1"],
- home: ["0"],
- away: ["0"],
- live_index: 0
- }
- end
-
- partials = Array(score_state.set_partials)
- labels = partials.map { |p| (p["set"] || p[:set]).to_s }
- home = partials.map { |p| (p["home"] || p[:home]).to_s }
- away = partials.map { |p| (p["away"] || p[:away]).to_s }
- labels << score_state.current_set.to_s
- home << score_state.home_points.to_s
- away << score_state.away_points.to_s
- {
- labels: labels,
- home: home,
- away: away,
- live_index: labels.length - 1
- }
- end
-
- def live_scorebug_team_label(name, max: 16)
- n = name.to_s
- n.length <= max ? n : "#{n[0, max - 1]}…"
- end
-
- # Colore ospite sul tabellone: secondario società se leggibile su bianco, altrimenti blu visitatore.
- def live_scorebug_away_color(team)
- club = team.club
- candidate = club&.effective_secondary_color.presence || "#1565c0"
- return candidate unless light_hex_color?(candidate)
-
- "#1565c0"
- end
-
private
- def light_hex_color?(hex)
- h = hex.to_s.delete_prefix("#")
- return false unless h.match?(/\A\h{6}\z/i)
-
- r = h[0..1].to_i(16)
- g = h[2..3].to_i(16)
- b = h[4..5].to_i(16)
- (0.299 * r + 0.587 * g + 0.114 * b) > 200
- end
-
def format_set_partial_entry(partial)
data = partial.respond_to?(:with_indifferent_access) ? partial.with_indifferent_access : partial
set_no = data[:set] || data["set"]
diff --git a/backend/app/views/layouts/marketing_live.html.erb b/backend/app/views/layouts/marketing_live.html.erb
index 50e3372..37f4f5a 100644
--- a/backend/app/views/layouts/marketing_live.html.erb
+++ b/backend/app/views/layouts/marketing_live.html.erb
@@ -7,7 +7,7 @@
<%= render "shared/meta_tags" %>
-
+
<%= yield :head %>
data-ga-id="<%= MatchLiveTv.google_analytics_measurement_id %>"<% end %>>
diff --git a/backend/app/views/public/live/_player_overlays.html.erb b/backend/app/views/public/live/_player_overlays.html.erb
new file mode 100644
index 0000000..e427f10
--- /dev/null
+++ b/backend/app/views/public/live/_player_overlays.html.erb
@@ -0,0 +1,6 @@
+Il punteggio finale resta visibile sopra.
+ Il punteggio è visibile nel video se la diretta era ancora in corso.
<%= link_to "Tutte le dirette", public_live_index_path, class: "btn btn-secondary stream-ended-btn" %>
<% elsif @session.platform == "youtube" %>
@@ -41,7 +41,7 @@
<% else %>
- <%= render "public/live/scorebug", match: @match, session: @session %>
+ <%= render "public/live/player_overlays", match: @match, session: @session %>
@@ -58,68 +58,7 @@
const pausedMsg = document.getElementById("paused-msg");
const awaitingMsg = document.getElementById("awaiting-msg");
const playHint = document.getElementById("play-hint");
- const scorebug = document.getElementById("live-scorebug");
const streamBadge = document.getElementById("live-stream-badge");
- const homeName = "<%= j @match.team.name %>";
- const awayName = "<%= j @match.opponent_name %>";
-
- function abbrevName(name, max = 16) {
- const n = String(name || "");
- return n.length <= max ? n : `${n.slice(0, max - 1)}…`;
- }
-
- function scoreColumnsFromPayload(score) {
- if (!score) return { labels: ["1"], home: ["0"], away: ["0"], liveIndex: 0 };
- const partials = score.set_partials || score.setPartials || [];
- const labels = partials.map((p) => String(p.set ?? p["set"] ?? ""));
- const home = partials.map((p) => String(p.home ?? p["home"] ?? "0"));
- const away = partials.map((p) => String(p.away ?? p["away"] ?? "0"));
- const currentSet = score.current_set ?? score.currentSet ?? 1;
- labels.push(String(currentSet));
- home.push(String(score.home_points ?? score.homePoints ?? 0));
- away.push(String(score.away_points ?? score.awayPoints ?? 0));
- return { labels, home, away, liveIndex: labels.length - 1 };
- }
-
- function renderLiveScorebug(score) {
- if (!scorebug) return;
- const cols = scoreColumnsFromPayload(score);
- const homeLabel = abbrevName(scorebug.dataset.homeName || homeName);
- const awayLabel = abbrevName(scorebug.dataset.awayName || awayName);
- const headCells = cols.labels
- .map((label) => `
${label} | `)
- .join("");
- const homeCells = cols.home
- .map((val, i) => {
- const live = i === cols.liveIndex ? " live-scorebug__pts--live-home" : "";
- return `
${val} | `;
- })
- .join("");
- const awayCells = cols.away
- .map((val, i) => {
- const live = i === cols.liveIndex ? " live-scorebug__pts--live-away" : "";
- return `
${val} | `;
- })
- .join("");
- scorebug.innerHTML = `
-
- Punteggio live
-
- | Squadra | ${headCells}
-
-
-
- | ${homeLabel} | ${homeCells}
-
-
- | ${awayLabel} | ${awayCells}
-
-
-
-
- MATCH LIVE TV
-
`;
- }
function syncStreamBadge(data) {
if (!streamBadge) return;
@@ -329,7 +268,6 @@
return;
}
- renderLiveScorebug(data.score);
syncStreamBadge(data);
const onAir = !!data.on_air;
diff --git a/backend/public/live.css b/backend/public/live.css
index a9512b7..8e84d5f 100644
--- a/backend/public/live.css
+++ b/backend/public/live.css
@@ -48,166 +48,6 @@
left: auto;
}
-/* Tabellone broadcast: tabella per colonne allineate (set / punteggi). */
-.live-main .live-scorebug {
- position: absolute;
- top: 12px;
- left: 12px;
- width: max-content;
- max-width: min(92%, 320px);
- padding: 6px 8px 5px;
- border-radius: 6px;
- background: rgba(255, 255, 255, 0.94);
- color: #111;
- box-shadow: 0 2px 14px rgba(0, 0, 0, 0.45);
- font-size: 0.8rem;
- line-height: 1.2;
-}
-
-.live-main .live-scorebug__table {
- border-collapse: collapse;
- table-layout: fixed;
- width: auto;
-}
-
-.live-main .live-scorebug__col-team {
- width: 7.25rem;
-}
-
-.live-main .live-scorebug__col-set {
- width: 1.85rem;
-}
-
-.live-main .live-scorebug__corner {
- width: 7.25rem;
- padding: 0;
- border: none;
- font-size: 0;
- line-height: 0;
- visibility: hidden;
-}
-
-.live-main .live-scorebug thead th {
- padding: 0 0 4px;
- border-bottom: 1px solid rgba(0, 0, 0, 0.12);
- font-size: 0.68rem;
- font-weight: 700;
- color: #666;
- text-align: center;
- vertical-align: bottom;
-}
-
-.live-main .live-scorebug__col-h {
- letter-spacing: 0.02em;
-}
-
-.live-main .live-scorebug tbody th,
-.live-main .live-scorebug tbody td {
- padding: 3px 0;
- vertical-align: middle;
-}
-
-.live-main .live-scorebug tbody tr + tr th,
-.live-main .live-scorebug tbody tr + tr td {
- padding-top: 2px;
-}
-
-.live-main .live-scorebug__team {
- font-weight: 700;
- text-align: left;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 7.25rem;
- padding-right: 8px;
-}
-
-.live-main .live-scorebug__row--home .live-scorebug__team {
- color: var(--sb-home-primary, #111);
-}
-
-.live-main .live-scorebug__row--away .live-scorebug__team {
- color: var(--sb-away-primary, #1565c0);
-}
-
-.live-main .live-scorebug__pts {
- text-align: center;
- font-weight: 800;
- font-variant-numeric: tabular-nums;
- color: #111;
- padding-left: 0;
- padding-right: 0;
-}
-
-.live-main .live-scorebug__pts--live-home {
- background: color-mix(in srgb, var(--sb-home-primary, #e53935) 16%, #fff);
- border-radius: 3px;
- color: var(--sb-home-primary, #111);
- font-weight: 900;
-}
-
-.live-main .live-scorebug__pts--live-away {
- background: color-mix(in srgb, var(--sb-away-primary, #1565c0) 16%, #fff);
- border-radius: 3px;
- color: var(--sb-away-primary, #111);
- font-weight: 900;
-}
-
-.live-main .live-scorebug__brand {
- margin-top: 5px;
- padding: 4px 10px;
- border-radius: 4px;
- background: linear-gradient(135deg, #14141c 0%, #2a1214 55%, #1a1a24 100%);
- border: 1px solid rgba(229, 57, 53, 0.35);
- text-align: center;
- line-height: 1.1;
-}
-
-.live-main .live-scorebug__brand-text {
- font-size: 0.7rem;
- font-weight: 800;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- color: #fff;
-}
-
-.live-main .live-scorebug__brand-accent {
- color: #e53935;
-}
-
-.live-main .live-scorebug .visually-hidden {
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
-}
-
-@media (max-width: 520px) {
- .live-main .live-scorebug {
- font-size: 0.72rem;
- max-width: 88%;
- padding: 5px 6px 4px;
- }
-
- .live-main .live-scorebug__col-team,
- .live-main .live-scorebug__corner {
- width: 5.75rem;
- }
-
- .live-main .live-scorebug__team {
- max-width: 5.75rem;
- }
-
- .live-main .live-scorebug__col-set {
- width: 1.55rem;
- }
-}
-
.live-main .live-status-msg {
margin: 12px 0 0;
color: #f5f5f5;
diff --git a/docs/REPLAY_MODULE.md b/docs/REPLAY_MODULE.md
index 7b6f4cb..f25172f 100644
--- a/docs/REPLAY_MODULE.md
+++ b/docs/REPLAY_MODULE.md
@@ -25,7 +25,7 @@ MediaMTX registra **solo mentre il telefono pubblica** (RTMP connesso). In pausa
### 3. Archivio Replay
- Web: `/clubs/:id/replays` (gestione società)
- Pubblico: `/replay` con filtro società/squadra
-- App: schermata Replay
+- App mobile: **nessuna** UI replay (gestione solo da sito web)
### 4. Riproduzione
- Player MP4: `/replay/:id` + stream `/replay/:id/stream`
diff --git a/native/android/README.md b/native/android/README.md
index 5a80dc0..8813ccc 100644
--- a/native/android/README.md
+++ b/native/android/README.md
@@ -34,7 +34,7 @@ API default: `https://www.matchlivetv.it` (override con `-PAPI_BASE_URL=...`).
- [x] Login, lista partite, avvio diretta, preview + RTMP
- [ ] Wizard completo (rete, piattaforma YouTube, tabellone)
- [ ] WebSocket tabellone / regia
-- [ ] Archivio registrazioni
+- [x] Replay solo da sito web (non in app)
- [ ] **iOS nativo** — da sviluppare su Mac in `native/ios/` (SwiftUI)
## iOS (prossimo passo su Mac)
diff --git a/native/android/app/build.gradle.kts b/native/android/app/build.gradle.kts
index 77b4df1..f44fa99 100644
--- a/native/android/app/build.gradle.kts
+++ b/native/android/app/build.gradle.kts
@@ -12,7 +12,7 @@ android {
applicationId = "com.matchlivetv.match_live_tv"
minSdk = 24
targetSdk = 35
- versionCode = 15
+ versionCode = 16
versionName = "2.0.0-native"
val apiBaseUrl = project.findProperty("API_BASE_URL") as String?
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt
index 9454405..5753145 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/ApiDtos.kt
@@ -2,7 +2,6 @@ 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.Recording
import com.matchlivetv.match_live_tv.domain.ScoringRules
import com.matchlivetv.match_live_tv.domain.StreamSession
import com.matchlivetv.match_live_tv.domain.Team
@@ -230,6 +229,12 @@ fun ScoringRules.toBody() = ScoringRulesBody(
minPointLead = minPointLead,
)
+fun ScoringRulesBody.toMap(): Map
= mapOf(
+ "points_per_set" to pointsPerSet,
+ "points_deciding_set" to pointsDecidingSet,
+ "min_point_lead" to minPointLead,
+)
+
data class UpdateMatchBodyFull(
@Json(name = "opponent_name") val opponentName: String,
val location: String? = null,
@@ -237,7 +242,7 @@ data class UpdateMatchBodyFull(
@Json(name = "sets_to_win") val setsToWin: Int,
val category: String? = null,
@Json(name = "opponent_primary_color") val opponentPrimaryColor: String? = null,
- @Json(name = "scoring_rules") val scoringRules: ScoringRulesBody? = null,
+ @Json(name = "scoring_rules") val scoringRules: Map? = null,
)
data class UpdateTeamRequest(val team: UpdateTeamBody)
@@ -257,43 +262,6 @@ data class NetworkTestResponse(
@Json(name = "target_fps") val targetFps: Int? = null,
)
-data class RecordingDto(
- val id: String,
- val title: String? = null,
- @Json(name = "opponent_name") val opponentName: String? = null,
- @Json(name = "team_name") val teamName: String? = null,
- val status: String? = null,
- @Json(name = "status_label") val statusLabel: String? = null,
- @Json(name = "recorded_at") val recordedAt: String? = null,
- @Json(name = "ended_at") val endedAt: String? = null,
- @Json(name = "duration_label") val durationLabel: String? = null,
- @Json(name = "views_label") val viewsLabel: String? = null,
- @Json(name = "replay_url") val replayUrl: String? = null,
- @Json(name = "thumbnail_url") val thumbnailUrl: String? = null,
- @Json(name = "download_enabled") val downloadEnabled: Boolean? = null,
- @Json(name = "youtube_watch_url") val youtubeWatchUrl: String? = null,
- @Json(name = "expires_at") val expiresAt: String? = null,
- @Json(name = "title_or_default") val titleOrDefault: String? = null,
-) {
- fun toDomain() = Recording(
- id = id,
- title = title ?: titleOrDefault ?: "Replay",
- opponentName = opponentName.orEmpty(),
- teamName = teamName.orEmpty(),
- status = status ?: "processing",
- statusLabel = statusLabel ?: status.orEmpty(),
- recordedAt = recordedAt,
- endedAt = endedAt,
- durationLabel = durationLabel,
- viewsLabel = viewsLabel,
- replayUrl = replayUrl,
- thumbnailUrl = thumbnailUrl,
- downloadEnabled = downloadEnabled ?: false,
- youtubeWatchUrl = youtubeWatchUrl,
- expiresAt = expiresAt,
- )
-}
-
data class RegiaLinkResponse(
@Json(name = "regia_url") val regiaUrl: String,
)
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt
index 8a68079..dfaee82 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/api/MatchLiveApi.kt
@@ -44,9 +44,6 @@ interface MatchLiveApi {
@Part logoFile: MultipartBody.Part?,
): TeamDto
- @GET("teams/{teamId}/recordings")
- suspend fun recordings(@Path("teamId") teamId: String): List
-
@GET("teams/{teamId}/matches")
suspend fun matches(@Path("teamId") teamId: String): List
@@ -126,6 +123,4 @@ interface MatchLiveApi {
@Body body: TelemetryRequest,
)
- @GET("recordings/{recordingId}/download")
- suspend fun recordingDownload(@Path("recordingId") recordingId: String): Map
}
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/cable/SessionCableService.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/cable/SessionCableService.kt
index 6474ad3..90bd151 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/cable/SessionCableService.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/cable/SessionCableService.kt
@@ -127,13 +127,13 @@ class SessionCableService(
}
private fun parseScore(data: Map): ScoreState {
- val partialsRaw = data["set_partials"] as? List<*> ?: emptyList()
+ val partialsRaw = data["set_partials"] as? List<*> ?: data["setPartials"] as? List<*> ?: emptyList()
return ScoreState(
- homeSets = (data["home_sets"] as? Number)?.toInt() ?: 0,
- awaySets = (data["away_sets"] as? Number)?.toInt() ?: 0,
- homePoints = (data["home_points"] as? Number)?.toInt() ?: 0,
- awayPoints = (data["away_points"] as? Number)?.toInt() ?: 0,
- currentSet = (data["current_set"] as? Number)?.toInt() ?: 1,
+ 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(
@@ -147,6 +147,15 @@ class SessionCableService(
)
}
+ private fun intField(data: Map, 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
+ }
+ }
+
companion object {
private const val TAG = "SessionCableService"
}
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/repository/MatchRepository.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/repository/MatchRepository.kt
index 7542357..030305c 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/repository/MatchRepository.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/repository/MatchRepository.kt
@@ -9,12 +9,12 @@ import com.matchlivetv.match_live_tv.data.api.CreateMatchRequest
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
import com.matchlivetv.match_live_tv.data.api.MultipartBodies
import com.matchlivetv.match_live_tv.data.api.ScoringRulesBody
+import com.matchlivetv.match_live_tv.data.api.toMap
import com.matchlivetv.match_live_tv.data.api.UpdateMatchBodyFull
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.Recording
import com.matchlivetv.match_live_tv.domain.Team
import com.matchlivetv.match_live_tv.domain.coachHubVisible
import kotlinx.coroutines.flow.first
@@ -55,9 +55,6 @@ class MatchRepository(
return fetchMatchesForTeam(team.id)
}
- suspend fun fetchRecordings(teamId: String): List =
- api.recordings(teamId).map { it.toDomain() }
-
suspend fun selectTeam(teamId: String) {
tokenStore.saveActiveTeamId(teamId)
}
@@ -127,7 +124,7 @@ class MatchRepository(
setsToWin = setsToWin,
category = category?.takeIf { it.isNotBlank() },
opponentPrimaryColor = opponentPrimaryColor,
- scoringRules = scoringRules,
+ scoringRules = scoringRules?.toMap() ?: emptyMap(),
)
if (opponentLogoUri != null) {
return api.updateMatchMultipart(
@@ -154,11 +151,6 @@ class MatchRepository(
api.deleteMatch(matchId)
}
- suspend fun recordingDownloadUrl(recordingId: String): String {
- val body = api.recordingDownload(recordingId)
- return body["download_url"] ?: error("URL download mancante")
- }
-
companion object {
private val matchComparator = compareBy(nullsLast()) { match ->
parseApiInstant(match.scheduledAt)
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/scoring/ScoreController.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/scoring/ScoreController.kt
index 4ef9acb..875ac52 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/scoring/ScoreController.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/data/scoring/ScoreController.kt
@@ -6,6 +6,7 @@ import com.matchlivetv.match_live_tv.domain.ScoreState
import com.matchlivetv.match_live_tv.domain.SetPartial
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -35,6 +36,12 @@ class ScoreController(
}
fun applyRemote(remote: ScoreState) {
+ scope.launch {
+ applyRemoteOnMain(remote)
+ }
+ }
+
+ private fun applyRemoteOnMain(remote: ScoreState) {
if (remote == _score.value) return
val now = System.currentTimeMillis()
if (now < suppressRemoteUntilMs && remote.progressKey() <= _score.value.progressKey()) {
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/domain/Recording.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/domain/Recording.kt
deleted file mode 100644
index 7a09989..0000000
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/domain/Recording.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package com.matchlivetv.match_live_tv.domain
-
-data class Recording(
- val id: String,
- val title: String,
- val opponentName: String,
- val teamName: String,
- val status: String,
- val statusLabel: String,
- val recordedAt: String? = null,
- val endedAt: String? = null,
- val durationLabel: String? = null,
- val viewsLabel: String? = null,
- val replayUrl: String? = null,
- val thumbnailUrl: String? = null,
- val downloadEnabled: Boolean = false,
- val youtubeWatchUrl: String? = null,
- val expiresAt: String? = null,
-) {
- val isReady: Boolean get() = status == "ready"
- val isProcessing: Boolean get() = status == "processing"
-}
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/archive/ArchiveScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/archive/ArchiveScreen.kt
deleted file mode 100644
index 2e2b56f..0000000
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/archive/ArchiveScreen.kt
+++ /dev/null
@@ -1,178 +0,0 @@
-package com.matchlivetv.match_live_tv.ui.archive
-
-import android.content.Intent
-import android.net.Uri
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.ArrowBack
-import androidx.compose.material3.Card
-import androidx.compose.material3.CardDefaults
-import androidx.compose.material3.CircularProgressIndicator
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.material3.TopAppBar
-import androidx.compose.material3.TopAppBarDefaults
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.dp
-import com.matchlivetv.match_live_tv.data.AppContainer
-import com.matchlivetv.match_live_tv.domain.Recording
-import com.matchlivetv.match_live_tv.domain.Team
-import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
-import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
-import com.matchlivetv.match_live_tv.ui.theme.MatchColors
-import kotlinx.coroutines.launch
-
-@OptIn(ExperimentalMaterial3Api::class)
-@Composable
-fun ArchiveScreen(
- container: AppContainer,
- onBack: () -> Unit,
-) {
- val context = LocalContext.current
- val scope = rememberCoroutineScope()
- var loading by remember { mutableStateOf(true) }
- var team by remember { mutableStateOf(null) }
- var recordings by remember { mutableStateOf>(emptyList()) }
- var error by remember { mutableStateOf(null) }
-
- LaunchedEffect(Unit) {
- loading = true
- runCatching {
- val teams = container.matchRepository.fetchTeams()
- val active = container.matchRepository.resolveActiveTeam(teams)
- team = active
- recordings = active?.let { container.matchRepository.fetchRecordings(it.id) }.orEmpty()
- }.onFailure { error = it.message }
- loading = false
- }
-
- MatchScreenScaffold(
- topBar = {
- TopAppBar(
- title = { Text("Replay") },
- navigationIcon = {
- IconButton(onClick = onBack) {
- Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White)
- }
- },
- colors = TopAppBarDefaults.topAppBarColors(
- containerColor = MatchColors.Background,
- titleContentColor = Color.White,
- ),
- )
- },
- ) {
- when {
- loading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
- CircularProgressIndicator(color = MatchColors.PrimaryRed)
- }
- error != null -> Column(
- Modifier.fillMaxSize().padding(24.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(error!!, color = MatchColors.PrimaryRed, textAlign = TextAlign.Center)
- }
- team != null && !team!!.recordingsEnabled -> Column(
- Modifier.fillMaxSize().padding(24.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- Text(
- "Replay disponibili con Premium Light o Full",
- style = MaterialTheme.typography.headlineMedium,
- textAlign = TextAlign.Center,
- )
- Text(
- "Light: 30 giorni · Full: 90 giorni",
- style = MaterialTheme.typography.bodyMedium,
- textAlign = TextAlign.Center,
- modifier = Modifier.padding(top = 8.dp),
- )
- team?.billingUrl?.let { url ->
- MatchPrimaryButton(
- label = "SCOPRI PREMIUM",
- onClick = {
- context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
- },
- modifier = Modifier.padding(top = 16.dp),
- )
- }
- }
- recordings.isEmpty() -> Box(Modifier.fillMaxSize().padding(24.dp), contentAlignment = Alignment.Center) {
- Text(
- "Nessun replay ancora.\nAl termine delle dirette Premium le registrazioni compaiono qui.",
- textAlign = TextAlign.Center,
- style = MaterialTheme.typography.bodyMedium,
- )
- }
- else -> LazyColumn(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
- items(recordings, key = { it.id }) { recording ->
- Card(
- colors = CardDefaults.cardColors(containerColor = MatchColors.Surface),
- shape = RoundedCornerShape(12.dp),
- modifier = Modifier.fillMaxWidth(),
- onClick = {
- if (recording.isReady) {
- recording.replayUrl?.let { url ->
- context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
- }
- }
- },
- ) {
- Column(Modifier.padding(16.dp)) {
- Text(recording.title, style = MaterialTheme.typography.titleMedium)
- Text(
- listOfNotNull(
- recording.durationLabel,
- recording.viewsLabel,
- recording.statusLabel,
- ).joinToString(" · "),
- style = MaterialTheme.typography.bodyMedium,
- )
- if (recording.downloadEnabled && team?.phoneDownloadEnabled == true) {
- MatchPrimaryButton(
- label = "SCARICA MP4",
- onClick = {
- scope.launch {
- runCatching {
- container.matchRepository.recordingDownloadUrl(recording.id)
- }.onSuccess { url ->
- context.startActivity(
- Intent(Intent.ACTION_VIEW, Uri.parse(url)),
- )
- }
- }
- },
- modifier = Modifier.padding(top = 8.dp),
- )
- }
- }
- }
- }
- }
- }
- }
-}
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt
index 35f030b..c6086f4 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastControlsOverlay.kt
@@ -233,8 +233,8 @@ fun BroadcastControlsOverlay(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
- "${score.homeSets} - ${score.awaySets}",
- style = MaterialTheme.typography.titleMedium,
+ "${score.homePoints} - ${score.awayPoints}",
+ style = MaterialTheme.typography.titleLarge,
color = Color.White,
fontWeight = FontWeight.Bold,
)
@@ -243,6 +243,13 @@ fun BroadcastControlsOverlay(
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",
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt
index c6ccd50..572ba7a 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/broadcast/BroadcastScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.key
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -201,16 +202,23 @@ fun BroadcastScreen(
loading = false
}
- LaunchedEffect(score, match, metrics.phase) {
+ LaunchedEffect(match?.id) {
val currentMatch = match ?: return@LaunchedEffect
val authToken = container.authRepository.currentSession()?.accessToken
- val homeLogoUrl = resolveMediaUrl(currentMatch.homeLogoUrl)
- val awayLogoUrl = resolveMediaUrl(currentMatch.opponentLogoUrl)
OverlayLogoCache.preloadAll(
context,
- listOf(homeLogoUrl, awayLogoUrl),
+ listOf(
+ resolveMediaUrl(currentMatch.homeLogoUrl),
+ resolveMediaUrl(currentMatch.opponentLogoUrl),
+ ),
bearerToken = authToken,
)
+ }
+
+ LaunchedEffect(score, match, metrics.phase) {
+ val currentMatch = match ?: return@LaunchedEffect
+ val homeLogoUrl = resolveMediaUrl(currentMatch.homeLogoUrl)
+ val awayLogoUrl = resolveMediaUrl(currentMatch.opponentLogoUrl)
container.broadcastCoordinator.updateOverlay(
OverlayState(
scoreboard = score.toScoreboardState(
@@ -366,6 +374,7 @@ fun BroadcastScreen(
}
if (!loading && permissions.granted && currentMatch != null && scoreActions != null) {
+ key(score.progressKey()) {
BroadcastControlsOverlay(
controlsVisible = controlsVisible,
onToggleControls = { controlsVisible = !controlsVisible },
@@ -436,6 +445,7 @@ fun BroadcastScreen(
networkType = DeviceTelemetry.networkType(context),
deviceHealth = deviceHealth,
)
+ }
}
SnackbarHost(
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt
index d77adae..6652d96 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchesScreen.kt
@@ -62,7 +62,6 @@ fun MatchesScreen(
container: AppContainer,
onOpenSetup: (matchId: String) -> Unit,
onOpenBroadcast: (sessionId: String) -> Unit,
- onOpenArchive: () -> Unit,
onLogout: () -> Unit,
) {
val scope = rememberCoroutineScope()
@@ -157,9 +156,6 @@ fun MatchesScreen(
titleContentColor = Color.White,
),
actions = {
- IconButton(onClick = onOpenArchive) {
- Text("Archivio", color = MatchColors.TextSecondary)
- }
IconButton(onClick = {
scope.launch {
container.authRepository.logout()
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/AppNavHost.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/AppNavHost.kt
index ebe2382..4d387b7 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/AppNavHost.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/AppNavHost.kt
@@ -7,7 +7,6 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import com.matchlivetv.match_live_tv.data.AppContainer
-import com.matchlivetv.match_live_tv.ui.archive.ArchiveScreen
import com.matchlivetv.match_live_tv.ui.broadcast.BroadcastScreen
import com.matchlivetv.match_live_tv.ui.login.LoginScreen
import com.matchlivetv.match_live_tv.ui.matches.MatchesScreen
@@ -50,9 +49,6 @@ fun AppNavHost(container: AppContainer) {
onOpenBroadcast = { sessionId ->
navController.navigate(Routes.broadcast(sessionId))
},
- onOpenArchive = {
- navController.navigate(Routes.Archive)
- },
onLogout = {
navController.navigate(Routes.Login) {
popUpTo(Routes.Matches) { inclusive = true }
@@ -60,17 +56,6 @@ fun AppNavHost(container: AppContainer) {
},
)
}
- composable(Routes.Archive) {
- ArchiveScreen(
- container = container,
- onBack = {
- navController.navigate(Routes.Matches) {
- popUpTo(Routes.Matches) { inclusive = false }
- launchSingleTop = true
- }
- },
- )
- }
composable(
route = Routes.Setup,
arguments = listOf(
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/Routes.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/Routes.kt
index 764e282..e18ae6c 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/Routes.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/navigation/Routes.kt
@@ -4,7 +4,6 @@ object Routes {
const val Splash = "splash"
const val Login = "login"
const val Matches = "matches"
- const val Archive = "archive"
const val Setup = "setup/{matchId}/{step}"
const val Broadcast = "broadcast/{sessionId}"
diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt
index 3d98ba7..cb95bba 100644
--- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt
+++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/wizard/StepMatchScreen.kt
@@ -45,6 +45,15 @@ private const val DEFAULT_POINTS_DECIDING_SET = 15
private const val DEFAULT_MIN_POINT_LEAD = 2
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
+ val rules = match.scoringRules ?: return false
+ return rules.pointsPerSet != DEFAULT_POINTS_PER_SET ||
+ rules.pointsDecidingSet != DEFAULT_POINTS_DECIDING_SET ||
+ rules.minPointLead != DEFAULT_MIN_POINT_LEAD
+}
+
@Composable
fun StepMatchScreen(
container: AppContainer,
@@ -91,9 +100,7 @@ fun StepMatchScreen(
) { uri -> opponentLogoUri = uri }
val existingRules = match.scoringRules
- var customRules by remember {
- mutableStateOf(existingRules != null || match.setsToWin != DEFAULT_SETS_TO_WIN)
- }
+ var customRules by remember { mutableStateOf(matchHasCustomScoring(match)) }
var setsToWin by remember { mutableIntStateOf(match.setsToWin.coerceIn(2, 3)) }
var pointsPerSet by remember {
mutableIntStateOf(existingRules?.pointsPerSet ?: DEFAULT_POINTS_PER_SET)
@@ -262,11 +269,7 @@ fun StepMatchScreen(
minPointLead = existingRules?.minPointLead ?: DEFAULT_MIN_POINT_LEAD,
)
} else {
- ScoringRulesBody(
- pointsPerSet = DEFAULT_POINTS_PER_SET,
- pointsDecidingSet = DEFAULT_POINTS_DECIDING_SET,
- minPointLead = DEFAULT_MIN_POINT_LEAD,
- )
+ null
}
val initialHomeColor = normalizeHexColor(