Files
MatchLiveTv/native/ios/MatchLiveTv/UI/Broadcast/LiveScoreActions.swift
T
eminuxandCursor 0702cb699c Rendi affidabile pausa con slate e conferma i controlli laterali.
Evita buchi HLS/YouTube in pausa (path slate ricreata, reload player, no patch recording inutili) e richiede conferma su tasti laterali app/regia.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-20 12:44:23 +02:00

178 lines
6.1 KiB
Swift

import Foundation
import SwiftUI
enum ScoreDialogKind {
case setWon
case closeSet
case closeSetAnyway
case matchWon
}
struct ScoreDialogState: Identifiable {
let id = UUID()
let kind: ScoreDialogKind
var winnerSide: ScoringSide?
var homePoints: Int = 0
var awayPoints: Int = 0
var homeSets: Int = 0
var awaySets: Int = 0
}
@MainActor
final class LiveScoreDialogHost: ObservableObject {
@Published private(set) var pending: ScoreDialogState?
private var waiter: CheckedContinuation<Bool, Never>?
func show(_ state: ScoreDialogState) async -> Bool {
guard pending == nil else { return false }
return await withCheckedContinuation { continuation in
pending = state
waiter = continuation
}
}
func resolve(_ value: Bool) {
guard let continuation = waiter else { return }
pending = nil
waiter = nil
continuation.resume(returning: value)
}
}
@MainActor
final class LiveScoreActions {
private let rules: MatchScoringContext
private let scoreController: ScoreController
private let dialogHost: LiveScoreDialogHost
private let onStopStream: () async -> Void
private let onScoreboardChanged: () -> Void
init(
rules: MatchScoringContext,
scoreController: ScoreController,
dialogHost: LiveScoreDialogHost,
onStopStream: @escaping () async -> Void,
onScoreboardChanged: @escaping () -> Void = {}
) {
self.rules = rules
self.scoreController = scoreController
self.dialogHost = dialogHost
self.onStopStream = onStopStream
self.onScoreboardChanged = onScoreboardChanged
}
func afterPointChange(score: ScoreState) async {
guard rules.usesSetLogic else { return }
guard let winner = rules.setWinnerFromPoints(
homePoints: score.homePoints,
awayPoints: score.awayPoints,
currentSet: score.currentSet
) else { return }
let close = await dialogHost.show(
ScoreDialogState(
kind: .setWon,
winnerSide: winner,
homePoints: score.homePoints,
awayPoints: score.awayPoints
)
)
if close {
guard await scoreController.closeSetAsync() else { return }
onScoreboardChanged()
await afterCloseSet()
}
}
func requestCloseSet() async {
guard rules.usesSetLogic else { return }
let score = scoreController.score
let winner = rules.setWinnerFromPoints(
homePoints: score.homePoints,
awayPoints: score.awayPoints,
currentSet: score.currentSet
)
let ok = await dialogHost.show(
ScoreDialogState(kind: winner == nil ? .closeSetAnyway : .closeSet)
)
if !ok { return }
guard await scoreController.closeSetAsync() else { return }
onScoreboardChanged()
await afterCloseSet()
}
private func afterCloseSet() async {
let score = scoreController.score
guard let winner = rules.matchWinner(homeSets: score.homeSets, awaySets: score.awaySets) else { return }
let stop = await dialogHost.show(
ScoreDialogState(
kind: .matchWon,
winnerSide: winner,
homeSets: score.homeSets,
awaySets: score.awaySets
)
)
if stop { await onStopStream() }
}
}
struct ScoreDialogRouter: View {
@ObservedObject var host: LiveScoreDialogHost
let homeName: String
let awayName: String
var body: some View {
Color.clear
.frame(width: 0, height: 0)
.alert(item: dialogBinding) { dialog in
alert(for: dialog)
}
}
private var dialogBinding: Binding<ScoreDialogState?> {
Binding(
get: { host.pending },
set: { _ in
// Non risolvere qui: su iPad la dismiss dell'alert può azzerare l'item
// dopo il tap su «Chiudi set» e sovrascrivere resolve(true) con false.
}
)
}
private func alert(for dialog: ScoreDialogState) -> Alert {
switch dialog.kind {
case .setWon:
let winner = dialog.winnerSide.map { scoringSideName($0, homeName: homeName, awayName: awayName) } ?? ""
return Alert(
title: Text(L10n.t("score.set.won.title")),
message: Text(L10n.t("score.set.won.message", winner, dialog.homePoints, dialog.awayPoints)),
primaryButton: .default(Text(L10n.t("score.action.close.set"))) { host.resolve(true) },
secondaryButton: .cancel(Text(L10n.t("score.action.continue.scoring"))) { host.resolve(false) }
)
case .closeSet:
return Alert(
title: Text(L10n.t("score.action.close.set")),
message: Text(L10n.t("broadcast.close.set.confirm.message")),
primaryButton: .default(Text(L10n.t("score.action.close.set"))) { host.resolve(true) },
secondaryButton: .cancel(Text(L10n.t("action.cancel"))) { host.resolve(false) }
)
case .closeSetAnyway:
return Alert(
title: Text(L10n.t("score.action.close.set")),
message: Text(L10n.t("score.close.set.anyway.message")),
primaryButton: .default(Text(L10n.t("score.action.close.anyway"))) { host.resolve(true) },
secondaryButton: .cancel(Text(L10n.t("action.cancel"))) { host.resolve(false) }
)
case .matchWon:
let winner = dialog.winnerSide.map { scoringSideName($0, homeName: homeName, awayName: awayName) } ?? ""
return Alert(
title: Text(L10n.t("score.match.won.title")),
message: Text(L10n.t("score.match.won.message", winner, dialog.homeSets, dialog.awaySets)),
primaryButton: .default(Text(L10n.t("score.action.close.live"))) { host.resolve(true) },
secondaryButton: .cancel(Text(L10n.t("score.action.continue.live"))) { host.resolve(false) }
)
}
}
}