Include admin/API/banner nativi, form contatti e reset copertina standard con layout edit più ampio. Co-authored-by: Cursor <cursoragent@cursor.com>
561 lines
16 KiB
Swift
561 lines
16 KiB
Swift
import Foundation
|
|
|
|
struct LoginRequest: Encodable {
|
|
let email: String
|
|
let password: String
|
|
}
|
|
|
|
struct RefreshRequest: Encodable {
|
|
let refreshToken: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case refreshToken = "refresh_token"
|
|
}
|
|
}
|
|
|
|
struct ForgotPasswordRequest: Encodable {
|
|
let email: String
|
|
}
|
|
|
|
struct ForgotPasswordResponse: Decodable {
|
|
let message: String
|
|
}
|
|
|
|
struct UpdateAccountRequest: Encodable {
|
|
let name: String
|
|
}
|
|
|
|
struct ChangePasswordRequest: Encodable {
|
|
let currentPassword: String
|
|
let password: String
|
|
let passwordConfirmation: String
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case currentPassword = "current_password"
|
|
case password
|
|
case passwordConfirmation = "password_confirmation"
|
|
}
|
|
}
|
|
|
|
struct MessageResponse: Decodable {
|
|
let message: String
|
|
}
|
|
|
|
struct AnnouncementDto: Decodable, Identifiable, Equatable {
|
|
let id: String
|
|
let kind: String
|
|
let severity: String
|
|
let title: String
|
|
let body: String
|
|
let dismissible: Bool
|
|
let actionUrl: String?
|
|
let actionLabel: String?
|
|
}
|
|
|
|
struct LoginResponse: Decodable {
|
|
let user: UserDto
|
|
let accessToken: String
|
|
let refreshToken: String
|
|
|
|
func toDomain() -> (User, AuthTokens) {
|
|
(user.toDomain(), AuthTokens(accessToken: accessToken, refreshToken: refreshToken))
|
|
}
|
|
}
|
|
|
|
struct UserDto: Decodable {
|
|
let id: String
|
|
let email: String
|
|
let name: String
|
|
let role: String?
|
|
|
|
func toDomain() -> User {
|
|
User(id: id, email: email, name: name, role: role ?? "coach")
|
|
}
|
|
}
|
|
|
|
struct SportDto: Decodable {
|
|
let key: String
|
|
let label: String
|
|
let board: String
|
|
let overlay: String
|
|
let allowedOverlays: [String]?
|
|
let defaultRules: [String: Int]?
|
|
|
|
func toDomain() -> SportOption {
|
|
SportOption(
|
|
key: key,
|
|
label: label,
|
|
board: board,
|
|
overlay: overlay,
|
|
allowedOverlays: allowedOverlays ?? [],
|
|
defaultRules: defaultRules ?? [:]
|
|
)
|
|
}
|
|
}
|
|
|
|
struct TeamDto: Decodable {
|
|
let id: String
|
|
let name: String
|
|
let sport: String?
|
|
let sportKey: String?
|
|
let boardType: String?
|
|
let canStream: Bool?
|
|
let clubName: String?
|
|
let youtubeEnabled: Bool?
|
|
let youtubeConnected: Bool?
|
|
let youtubeSelectable: Bool?
|
|
let youtubeChannelTitle: String?
|
|
let youtubeUsesPlatformChannel: Bool?
|
|
let youtubeTeamChannelTitle: String?
|
|
let planName: String?
|
|
let recordingsEnabled: Bool?
|
|
let phoneDownloadEnabled: Bool?
|
|
let billingUrl: String?
|
|
let staffManageUrl: String?
|
|
let customCoverEnabled: Bool?
|
|
let logoUrl: String?
|
|
let primaryColor: String?
|
|
let secondaryColor: String?
|
|
|
|
func toDomain() -> Team {
|
|
Team(
|
|
id: id,
|
|
name: name,
|
|
sport: sportKey ?? sport ?? "pallavolo",
|
|
sportKey: sportKey ?? sport ?? "pallavolo",
|
|
boardType: boardType ?? "volley",
|
|
clubName: clubName,
|
|
logoUrl: logoUrl,
|
|
primaryColor: primaryColor,
|
|
secondaryColor: secondaryColor,
|
|
canStream: canStream ?? true,
|
|
youtubeEnabled: youtubeEnabled ?? false,
|
|
youtubeConnected: youtubeConnected ?? false,
|
|
youtubeSelectable: youtubeSelectable ?? false,
|
|
youtubeChannelTitle: youtubeChannelTitle,
|
|
youtubeUsesPlatformChannel: youtubeUsesPlatformChannel ?? false,
|
|
youtubeTeamChannelTitle: youtubeTeamChannelTitle,
|
|
planName: planName,
|
|
recordingsEnabled: recordingsEnabled ?? false,
|
|
phoneDownloadEnabled: phoneDownloadEnabled ?? false,
|
|
billingUrl: billingUrl,
|
|
staffManageUrl: staffManageUrl,
|
|
customCoverEnabled: customCoverEnabled ?? false
|
|
)
|
|
}
|
|
}
|
|
|
|
struct MatchDto: Decodable {
|
|
let id: String
|
|
let teamId: String
|
|
let teamName: String?
|
|
let opponentName: String
|
|
let location: String?
|
|
let scheduledAt: String?
|
|
let setsToWin: Int?
|
|
let sportKey: String?
|
|
let sport: String?
|
|
let sportLabel: String?
|
|
let boardType: String?
|
|
let overlayKind: String?
|
|
let effectiveOverlayKind: String?
|
|
let category: String?
|
|
let scoringRules: ScoringRulesBody?
|
|
let activeSessionId: String?
|
|
let activeSessionStatus: String?
|
|
let streamCompleted: Bool?
|
|
let coachHubVisible: Bool?
|
|
let homePrimaryColor: String?
|
|
let homeSecondaryColor: String?
|
|
let homeLogoUrl: String?
|
|
let opponentPrimaryColor: String?
|
|
let opponentLogoUrl: String?
|
|
let effectiveCoverUrl: String?
|
|
let coverSource: String?
|
|
let customCoverEnabled: Bool?
|
|
|
|
func toDomain() -> Match {
|
|
Match(
|
|
id: id,
|
|
teamId: teamId,
|
|
teamName: teamName ?? "",
|
|
opponentName: opponentName,
|
|
location: location,
|
|
scheduledAt: scheduledAt,
|
|
sportKey: sportKey ?? sport ?? "pallavolo",
|
|
sportLabel: sportLabel,
|
|
boardType: boardType ?? "volley",
|
|
overlayKind: overlayKind,
|
|
effectiveOverlayKind: effectiveOverlayKind ?? overlayKind ?? "volley",
|
|
setsToWin: setsToWin ?? 3,
|
|
category: category,
|
|
scoringRules: scoringRules?.toDomain(),
|
|
activeSessionId: activeSessionId,
|
|
activeSessionStatus: activeSessionStatus,
|
|
streamCompleted: streamCompleted ?? false,
|
|
coachHubVisible: coachHubVisible,
|
|
homePrimaryColor: homePrimaryColor,
|
|
homeSecondaryColor: homeSecondaryColor,
|
|
homeLogoUrl: homeLogoUrl,
|
|
opponentPrimaryColor: opponentPrimaryColor,
|
|
opponentLogoUrl: opponentLogoUrl,
|
|
effectiveCoverUrl: effectiveCoverUrl,
|
|
coverSource: coverSource ?? "default",
|
|
customCoverEnabled: customCoverEnabled ?? false
|
|
)
|
|
}
|
|
}
|
|
|
|
struct StreamSessionDto: Decodable {
|
|
let id: String
|
|
let matchId: String
|
|
let status: String?
|
|
let platform: String?
|
|
let rtmpIngestUrl: String?
|
|
let hlsPlaybackUrl: String?
|
|
let watchPageUrl: String?
|
|
let shareUrl: String?
|
|
let youtubeWatchUrl: String?
|
|
let youtubeReady: Bool?
|
|
let privacyStatus: String?
|
|
let targetBitrate: Int?
|
|
let targetFps: Int?
|
|
let qualityPreset: String?
|
|
let minQualityPreset: String?
|
|
let startedAt: String?
|
|
let audioMuted: Bool?
|
|
let score: ScoreStateDto?
|
|
|
|
func toDomain() -> StreamSession {
|
|
StreamSession(
|
|
id: id,
|
|
matchId: matchId,
|
|
status: status ?? "idle",
|
|
platform: platform ?? "matchlivetv",
|
|
rtmpIngestUrl: rtmpIngestUrl,
|
|
hlsPlaybackUrl: hlsPlaybackUrl,
|
|
watchPageUrl: watchPageUrl,
|
|
shareUrl: shareUrl,
|
|
youtubeWatchUrl: youtubeWatchUrl,
|
|
youtubeReady: youtubeReady ?? false,
|
|
privacyStatus: privacyStatus ?? "public",
|
|
targetBitrate: targetBitrate ?? 2_500_000,
|
|
targetFps: targetFps ?? 30,
|
|
qualityPreset: qualityPreset ?? "720p_30_2.5mbps",
|
|
minQualityPreset: minQualityPreset ?? "auto",
|
|
startedAt: startedAt,
|
|
score: score?.toDomain(),
|
|
audioMuted: audioMuted ?? false
|
|
)
|
|
}
|
|
}
|
|
|
|
struct SetPartialDto: Codable {
|
|
let set: Int?
|
|
let home: Int?
|
|
let away: Int?
|
|
|
|
init(set: Int?, home: Int?, away: Int?) {
|
|
self.set = set
|
|
self.home = home
|
|
self.away = away
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
set = Self.decodeInt(container, key: .set)
|
|
home = Self.decodeInt(container, key: .home)
|
|
away = Self.decodeInt(container, key: .away)
|
|
}
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case set, home, away
|
|
}
|
|
|
|
private static func decodeInt(_ container: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> Int? {
|
|
if let value = try? container.decode(Int.self, forKey: key) { return value }
|
|
if let text = try? container.decode(String.self, forKey: key), let value = Int(text) { return value }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
struct ScoreStateDto: Decodable {
|
|
let type: String?
|
|
let boardType: String?
|
|
let homeSets: Int?
|
|
let awaySets: Int?
|
|
let homePoints: Int?
|
|
let awayPoints: Int?
|
|
let currentSet: Int?
|
|
let setPartials: [SetPartialDto]?
|
|
let timeoutHome: FlexibleBool?
|
|
let timeoutAway: FlexibleBool?
|
|
let period: FlexiblePeriod?
|
|
let clockSecs: Int?
|
|
let clockRunning: FlexibleBool?
|
|
let data: ScoreStateDataDto?
|
|
|
|
func toDomain() -> ScoreState {
|
|
let board = boardType ?? "volley"
|
|
let homePts = homePoints ?? data?.homeScore ?? 0
|
|
let awayPts = awayPoints ?? data?.awayScore ?? 0
|
|
let periodLabel = period?.label
|
|
return ScoreState(
|
|
homeSets: homeSets ?? 0,
|
|
awaySets: awaySets ?? 0,
|
|
homePoints: homePts,
|
|
awayPoints: awayPts,
|
|
currentSet: max(currentSet ?? 1, 1),
|
|
setPartials: (setPartials ?? []).map {
|
|
SetPartial(set: $0.set ?? 1, home: $0.home ?? 0, away: $0.away ?? 0)
|
|
},
|
|
timeoutHome: timeoutHome?.value ?? false,
|
|
timeoutAway: timeoutAway?.value ?? false,
|
|
boardType: board,
|
|
period: parsePeriodNumber(periodLabel, currentSet: currentSet),
|
|
periodLabel: periodLabel,
|
|
clockSecs: clockSecs ?? data?.clockSecs ?? 0,
|
|
clockRunning: clockRunning?.value ?? data?.clockRunning?.value ?? false,
|
|
overtime: data?.overtime?.value ?? false
|
|
)
|
|
}
|
|
|
|
private func parsePeriodNumber(_ label: String?, currentSet: Int?) -> Int {
|
|
if let text = label {
|
|
if let m = text.range(of: #"Q(\d+)"#, options: .regularExpression) {
|
|
let num = text[m].dropFirst()
|
|
if let v = Int(num) { return v }
|
|
}
|
|
if let v = Int(text), v > 0 { return v }
|
|
}
|
|
return max(currentSet ?? 1, 1)
|
|
}
|
|
}
|
|
|
|
/// `period` può essere stringa ("Q2"), intero o null.
|
|
enum FlexiblePeriod: Decodable {
|
|
case string(String)
|
|
case int(Int)
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if let v = try? container.decode(Int.self) { self = .int(v); return }
|
|
if let v = try? container.decode(String.self) { self = .string(v); return }
|
|
throw DecodingError.typeMismatch(
|
|
FlexiblePeriod.self,
|
|
DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "period non valido")
|
|
)
|
|
}
|
|
|
|
var label: String? {
|
|
switch self {
|
|
case .string(let s): return s
|
|
case .int(let v): return String(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
enum FlexibleBool: Decodable {
|
|
case bool(Bool)
|
|
case int(Int)
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.singleValueContainer()
|
|
if let v = try? container.decode(Bool.self) { self = .bool(v); return }
|
|
if let v = try? container.decode(Int.self) { self = .int(v); return }
|
|
throw DecodingError.typeMismatch(
|
|
FlexibleBool.self,
|
|
DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "bool non valido")
|
|
)
|
|
}
|
|
|
|
var value: Bool {
|
|
switch self {
|
|
case .bool(let v): return v
|
|
case .int(let v): return v != 0
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ScoreStateDataDto: Decodable {
|
|
let homeScore: Int?
|
|
let awayScore: Int?
|
|
let clockSecs: Int?
|
|
let clockRunning: FlexibleBool?
|
|
let period: Int?
|
|
let overtime: FlexibleBool?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case homeScore, awayScore, clockSecs, clockRunning, period, overtime
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
homeScore = Self.decodeInt(container, key: .homeScore)
|
|
awayScore = Self.decodeInt(container, key: .awayScore)
|
|
clockSecs = Self.decodeInt(container, key: .clockSecs)
|
|
clockRunning = try? container.decode(FlexibleBool.self, forKey: .clockRunning)
|
|
period = Self.decodeInt(container, key: .period)
|
|
overtime = try? container.decode(FlexibleBool.self, forKey: .overtime)
|
|
}
|
|
|
|
private static func decodeInt(
|
|
_ container: KeyedDecodingContainer<CodingKeys>,
|
|
key: CodingKeys
|
|
) -> Int? {
|
|
if let value = try? container.decode(Int.self, forKey: key) { return value }
|
|
if let text = try? container.decode(String.self, forKey: key), let value = Int(text) { return value }
|
|
if let value = try? container.decode(Double.self, forKey: key) { return Int(value) }
|
|
return nil
|
|
}
|
|
}
|
|
|
|
struct ScoreActionRequest: Encodable {
|
|
let scoreAction: String
|
|
}
|
|
|
|
struct ScoreSyncRequest: Encodable {
|
|
let homeSets: Int
|
|
let awaySets: Int
|
|
let homePoints: Int
|
|
let awayPoints: Int
|
|
let currentSet: Int
|
|
let setPartials: [SetPartialDto]
|
|
let clockSecs: Int?
|
|
let clockRunning: Bool?
|
|
let period: Int?
|
|
let homeScore: Int?
|
|
let awayScore: Int?
|
|
}
|
|
|
|
struct CreateSessionRequest: Encodable {
|
|
let platform: String
|
|
let privacyStatus: String
|
|
let qualityPreset: String
|
|
let targetBitrate: Int
|
|
let targetFps: Int
|
|
let youtubeChannel: String?
|
|
}
|
|
|
|
struct AudioMuteRequest: Encodable {
|
|
let muted: Bool
|
|
}
|
|
|
|
struct MinQualityRequest: Encodable {
|
|
let minQualityPreset: String
|
|
}
|
|
|
|
struct ScoringRulesBody: Codable {
|
|
let setsToWin: Int?
|
|
let pointsPerSet: Int?
|
|
let pointsDecidingSet: Int?
|
|
let minPointLead: Int?
|
|
let periods: Int?
|
|
let periodDurationSecs: Int?
|
|
let overtimeDurationSecs: Int?
|
|
|
|
func toDomain() -> ScoringRules {
|
|
ScoringRules(
|
|
pointsPerSet: pointsPerSet ?? 25,
|
|
pointsDecidingSet: pointsDecidingSet ?? 15,
|
|
minPointLead: minPointLead ?? 2,
|
|
periods: periods ?? 4,
|
|
periodDurationSecs: periodDurationSecs ?? 600,
|
|
overtimeDurationSecs: overtimeDurationSecs ?? 300
|
|
)
|
|
}
|
|
}
|
|
|
|
struct UpdateMatchBodyFull: Encodable {
|
|
let opponentName: String
|
|
let location: String?
|
|
let scheduledAt: String?
|
|
let setsToWin: Int
|
|
let sportKey: String?
|
|
let overlayKind: String?
|
|
let category: String?
|
|
let opponentPrimaryColor: String?
|
|
let scoringRules: [String: Int]?
|
|
}
|
|
|
|
struct UpdateMatchRequestFull: Encodable {
|
|
let match: UpdateMatchBodyFull
|
|
}
|
|
|
|
struct UpdateTeamBody: Encodable {
|
|
let primaryColor: String?
|
|
let secondaryColor: String?
|
|
}
|
|
|
|
struct UpdateTeamRequest: Encodable {
|
|
let team: UpdateTeamBody
|
|
}
|
|
|
|
struct NetworkTestResponse: Decodable {
|
|
let ready: Bool
|
|
let targetUploadMbps: Double?
|
|
let qualityPreset: String?
|
|
let targetBitrate: Int?
|
|
let targetFps: Int?
|
|
}
|
|
|
|
struct RegiaLinkResponse: Decodable {
|
|
let regiaUrl: String
|
|
}
|
|
|
|
struct CreateMatchBody: Encodable {
|
|
let opponentName: String
|
|
let sport: String
|
|
let setsToWin: Int
|
|
let scheduledAt: String?
|
|
let location: String?
|
|
}
|
|
|
|
struct CreateMatchRequest: Encodable {
|
|
let match: CreateMatchBody
|
|
}
|
|
|
|
struct NetworkTestRequest: Encodable {
|
|
let downloadMbps: Double
|
|
let uploadMbps: Double
|
|
let latencyMs: Int
|
|
let networkType: String
|
|
}
|
|
|
|
struct TelemetryRequest: Encodable {
|
|
let deviceRole: String
|
|
let batteryLevel: Int?
|
|
let networkType: String?
|
|
let signalStrength: Int?
|
|
let currentBitrate: Int?
|
|
let targetBitrate: Int?
|
|
let fps: Int?
|
|
let thermalState: String?
|
|
}
|
|
|
|
extension ScoringRules {
|
|
func toBody(setsToWin: Int? = nil) -> ScoringRulesBody {
|
|
ScoringRulesBody(
|
|
setsToWin: setsToWin,
|
|
pointsPerSet: pointsPerSet,
|
|
pointsDecidingSet: pointsDecidingSet,
|
|
minPointLead: minPointLead,
|
|
periods: periods,
|
|
periodDurationSecs: periodDurationSecs,
|
|
overtimeDurationSecs: overtimeDurationSecs
|
|
)
|
|
}
|
|
|
|
func toMap(setsToWin: Int? = nil) -> [String: Int] {
|
|
var map: [String: Int] = [:]
|
|
if let setsToWin { map["sets_to_win"] = setsToWin }
|
|
map["points_per_set"] = pointsPerSet
|
|
map["points_deciding_set"] = pointsDecidingSet
|
|
map["min_point_lead"] = minPointLead
|
|
map["periods"] = periods
|
|
map["period_duration_secs"] = periodDurationSecs
|
|
map["overtime_duration_secs"] = overtimeDurationSecs
|
|
return map
|
|
}
|
|
}
|