Aggiunge app iOS nativa con parità multi-sport e test unitari.
Porting SwiftUI da Android (auth, hub, wizard, broadcast RTMP, overlay, scoring board-aware), reload hub al ritorno da diretta, decodifica score tollerante e documentazione allineata. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
475
native/ios/MatchLiveTv/Data/API/ApiDtos.swift
Normal file
475
native/ios/MatchLiveTv/Data/API/ApiDtos.swift
Normal file
@@ -0,0 +1,475 @@
|
||||
import Foundation
|
||||
|
||||
struct LoginRequest: Encodable {
|
||||
let email: String
|
||||
let password: String
|
||||
}
|
||||
|
||||
struct RefreshRequest: Encodable {
|
||||
let refreshToken: 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 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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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?
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 startedAt: String?
|
||||
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,
|
||||
startedAt: startedAt,
|
||||
score: score?.toDomain()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SetPartialDto: Codable {
|
||||
let set: Int?
|
||||
let home: Int?
|
||||
let away: Int?
|
||||
}
|
||||
|
||||
/// Decodifica tollerante per campi JSONB (`score_states.data`).
|
||||
private enum FlexibleJSON: Decodable {
|
||||
case int(Int)
|
||||
case bool(Bool)
|
||||
case string(String)
|
||||
|
||||
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 }
|
||||
if let v = try? container.decode(String.self) { self = .string(v); return }
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unsupported JSON value")
|
||||
}
|
||||
|
||||
var intValue: Int? {
|
||||
switch self {
|
||||
case .int(let v): return v
|
||||
case .string(let s): return Int(s)
|
||||
case .bool(let v): return v ? 1 : 0
|
||||
}
|
||||
}
|
||||
|
||||
var boolValue: Bool? {
|
||||
switch self {
|
||||
case .bool(let v): return v
|
||||
case .int(let v): return v != 0
|
||||
case .string(let s): return (s as NSString).boolValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ScoreStateDto: Decodable {
|
||||
let boardType: String?
|
||||
let homeSets: Int?
|
||||
let awaySets: Int?
|
||||
let homePoints: Int?
|
||||
let awayPoints: Int?
|
||||
let currentSet: Int?
|
||||
let setPartials: [SetPartialDto]?
|
||||
let timeoutHome: Bool?
|
||||
let timeoutAway: Bool?
|
||||
let period: String?
|
||||
let clockSecs: Int?
|
||||
let clockRunning: Bool?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case boardType = "board_type"
|
||||
case homeSets = "home_sets"
|
||||
case awaySets = "away_sets"
|
||||
case homePoints = "home_points"
|
||||
case awayPoints = "away_points"
|
||||
case currentSet = "current_set"
|
||||
case setPartials = "set_partials"
|
||||
case timeoutHome = "timeout_home"
|
||||
case timeoutAway = "timeout_away"
|
||||
case period
|
||||
case clockSecs = "clock_secs"
|
||||
case clockRunning = "clock_running"
|
||||
case data
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||
boardType = try c.decodeIfPresent(String.self, forKey: .boardType)
|
||||
homeSets = Self.decodeInt(c, key: .homeSets)
|
||||
awaySets = Self.decodeInt(c, key: .awaySets)
|
||||
homePoints = Self.decodeInt(c, key: .homePoints)
|
||||
awayPoints = Self.decodeInt(c, key: .awayPoints)
|
||||
currentSet = Self.decodeInt(c, key: .currentSet)
|
||||
setPartials = try c.decodeIfPresent([SetPartialDto].self, forKey: .setPartials)
|
||||
timeoutHome = Self.decodeBool(c, key: .timeoutHome)
|
||||
timeoutAway = Self.decodeBool(c, key: .timeoutAway)
|
||||
period = Self.decodeString(c, key: .period)
|
||||
nestedData = try c.decodeIfPresent([String: FlexibleJSON].self, forKey: .data)
|
||||
clockSecs = Self.decodeInt(c, key: .clockSecs) ?? nestedData?["clock_secs"]?.intValue
|
||||
clockRunning = Self.decodeBool(c, key: .clockRunning) ?? nestedData?["clock_running"]?.boolValue
|
||||
}
|
||||
|
||||
private let nestedData: [String: FlexibleJSON]?
|
||||
|
||||
private static func decodeInt(_ c: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> Int? {
|
||||
if let v = try? c.decodeIfPresent(Int.self, forKey: key) { return v }
|
||||
if let s = try? c.decodeIfPresent(String.self, forKey: key), let v = Int(s) { return v }
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func decodeBool(_ c: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> Bool? {
|
||||
if let v = try? c.decodeIfPresent(Bool.self, forKey: key) { return v }
|
||||
if let i = try? c.decodeIfPresent(Int.self, forKey: key) { return i != 0 }
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func decodeString(_ c: KeyedDecodingContainer<CodingKeys>, key: CodingKeys) -> String? {
|
||||
if let s = try? c.decodeIfPresent(String.self, forKey: key) { return s }
|
||||
if let i = try? c.decodeIfPresent(Int.self, forKey: key) { return String(i) }
|
||||
return nil
|
||||
}
|
||||
|
||||
func toDomain() -> ScoreState {
|
||||
let board = boardType ?? "volley"
|
||||
let homePts = homePoints ?? nestedData?["home_score"]?.intValue ?? 0
|
||||
let awayPts = awayPoints ?? nestedData?["away_score"]?.intValue ?? 0
|
||||
let overtime = nestedData?["overtime"]?.boolValue ?? false
|
||||
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 ?? false,
|
||||
timeoutAway: timeoutAway ?? false,
|
||||
boardType: board,
|
||||
period: parsePeriodNumber(period, currentSet: currentSet),
|
||||
periodLabel: period,
|
||||
clockSecs: clockSecs ?? 0,
|
||||
clockRunning: clockRunning ?? false,
|
||||
overtime: overtime
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 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?
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user