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>
178 lines
6.1 KiB
Swift
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) }
|
|
)
|
|
}
|
|
}
|
|
}
|