Files
MatchLiveTv/native/ios/MatchLiveTv/UI/Wizard/StepNetworkTestScreen.swift
T
Emiliano FrascaroandCursor ea5da1eb86 Allinea iOS a i18n Android 2.0.5 e aggiunge monitoraggio termico nativo.
Completa L10n su login/hub/wizard/broadcast e introduce ThermalStateManager su iOS/Android con degradazione qualità e indicatore in overlay.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-24 11:19:37 +02:00

255 lines
10 KiB
Swift

import SwiftUI
import UIKit
struct StepNetworkTestScreen: View {
@ObservedObject var container: AppContainer
let match: Match
let session: StreamSession
let onBack: () -> Void
let onStartLive: (String) -> Void
let onError: (String) -> Void
@State private var testing = false
@State private var testCompleted = false
@State private var ready = false
@State private var downloadMbps = 0.0
@State private var uploadMbps = 0.0
@State private var latencyMs = 0
@State private var networkType = "—"
@State private var selectedQualityLabel: String?
@State private var starting = false
@State private var currentSession: StreamSession
@State private var shareItem: ShareItem?
init(
container: AppContainer,
match: Match,
session: StreamSession,
onBack: @escaping () -> Void,
onStartLive: @escaping (String) -> Void,
onError: @escaping (String) -> Void
) {
self.container = container
self.match = match
self.session = session
self.onBack = onBack
self.onStartLive = onStartLive
self.onError = onError
_currentSession = State(initialValue: session)
}
private var shareUrl: String? {
currentSession.watchShareUrl()
}
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
Text(L10n.t("wizard.network.test.title"))
.font(MatchTypography.headlineMedium)
Text(L10n.t("wizard.network.test.subtitle"))
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.padding(.top, 8)
HStack(spacing: 8) {
MetricCard(
label: L10n.t("wizard.network.download.label"),
value: testing ? "..." : String(format: "%.1f Mbps", downloadMbps)
)
MetricCard(
label: L10n.t("wizard.network.upload.label"),
value: testing ? "..." : String(format: "%.1f Mbps", uploadMbps),
highlight: ready
)
MetricCard(
label: L10n.t("wizard.network.latency.label"),
value: testing ? "..." : "\(latencyMs) ms"
)
}
.padding(.top, 24)
MetricCard(label: L10n.t("wizard.network.type.label"), value: networkType)
.padding(.top, 12)
if testCompleted && ready {
Text(L10n.t("wizard.network.ready.label"))
.font(MatchTypography.titleMedium)
.foregroundStyle(MatchColors.successGreen)
.frame(maxWidth: .infinity)
.multilineTextAlignment(.center)
.padding(.top, 20)
if let quality = selectedQualityLabel {
WizardReadOnlyField(
label: L10n.t("wizard.network.quality.label"),
value: quality
)
.padding(.top, 12)
}
}
if testCompleted, let shareUrl {
WizardReadOnlyField(
label: currentSession.platform == "youtube" ? L10n.t("wizard.network.link.youtube.label") : L10n.t("wizard.network.link.live.label"),
value: shareUrl
)
.padding(.top, 16)
HStack(spacing: 8) {
MatchSecondaryButton(label: L10n.t("wizard.action.copy"), action: { copyToClipboard(shareUrl) })
if let url = URL(string: shareUrl) {
ShareLink(
item: url,
subject: Text(L10n.t("wizard.network.share.subject", match.teamName, match.opponentName)),
message: Text(shareUrl)
) {
Text(L10n.t("wizard.action.share"))
.font(MatchTypography.labelLarge)
.frame(maxWidth: .infinity)
.frame(height: 52)
.overlay(RoundedRectangle(cornerRadius: 8).stroke(MatchColors.outline, lineWidth: 1))
}
.buttonStyle(.plain)
}
}
.padding(.top, 8)
MatchSecondaryButton(label: L10n.t("wizard.action.share.regia.link"), action: shareRegiaLink)
.padding(.top, 8)
}
if !testCompleted {
MatchSecondaryButton(
label: testing ? L10n.t("wizard.network.test.running.label") : L10n.t("wizard.network.test.start.label"),
action: runTest,
enabled: !testing
)
.padding(.top, 24)
}
GeometryReader { proxy in
let spacing: CGFloat = 12
let backWidth = (proxy.size.width - spacing) / 3
let forwardWidth = (proxy.size.width - spacing) * 2 / 3
HStack(spacing: spacing) {
MatchSecondaryButton(label: L10n.t("wizard.action.back"), action: onBack, enabled: !starting)
.frame(width: backWidth)
MatchPrimaryButton(
label: L10n.t("wizard.action.start"),
action: startLive,
enabled: ready,
loading: starting
)
.frame(width: forwardWidth)
}
}
.frame(height: 52)
.padding(.top, 32)
}
.padding(.horizontal, 20)
.padding(.top, 12)
.padding(.bottom, 32)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.task(id: session.id) {
await pollYoutubeIfNeeded()
}
.sheet(item: $shareItem) { item in
ShareSheet(items: item.items, subject: item.subject) {
shareItem = nil
}
}
}
private func runTest() {
testing = true
ready = false
Task { @MainActor in
networkType = DeviceTelemetry.snapshot().networkType
try? await Task.sleep(nanoseconds: 2_000_000_000)
downloadMbps = Double.random(in: 8...20)
uploadMbps = Double.random(in: 2...6)
latencyMs = Int.random(in: 20...99)
let result = try? await container.sessionRepository.submitNetworkTest(
sessionId: currentSession.id,
downloadMbps: downloadMbps,
uploadMbps: uploadMbps,
latencyMs: latencyMs,
networkType: networkType
)
if let result {
selectedQualityLabel = formatQualityLabel(result)
if let updated = try? await container.sessionRepository.fetchSession(id: currentSession.id) {
currentSession = updated
container.wizardSession.session = updated
}
}
ready = result?.ready ?? (uploadMbps >= 2.0)
testCompleted = true
testing = false
}
}
private func startLive() {
guard ready, !starting else { return }
starting = true
Task { @MainActor in
defer { starting = false }
do {
let started = try await container.sessionRepository.startSession(id: currentSession.id)
currentSession = started
container.wizardSession.session = started
onStartLive(started.id)
} catch {
if let message = UserFacingError.message(for: error) {
onError(message)
}
}
}
}
private func pollYoutubeIfNeeded() async {
guard currentSession.platform == "youtube", !currentSession.youtubeReady else { return }
for _ in 0..<20 {
try? await Task.sleep(nanoseconds: 3_000_000_000)
guard let updated = try? await container.sessionRepository.fetchSession(id: currentSession.id) else { continue }
currentSession = updated
container.wizardSession.session = updated
if updated.youtubeReady || !(updated.youtubeWatchUrl?.isEmpty ?? true) { return }
}
}
private func copyToClipboard(_ text: String) {
UIPasteboard.general.string = text
}
private func shareRegiaLink() {
Task { @MainActor in
do {
let url = try await container.sessionRepository.createRegiaLink(sessionId: currentSession.id)
guard let link = URL(string: url) else {
onError(L10n.t("wizard.error.regia.link"))
return
}
shareItem = ShareItem(
items: [link],
subject: L10n.t("broadcast.share.regia.subject", "\(match.teamName) vs \(match.opponentName)")
)
} catch {
if let message = UserFacingError.message(for: error) {
onError(message)
}
}
}
}
private func formatQualityLabel(_ result: NetworkTestResponse) -> String {
let bitrateMbps = Double(result.targetBitrate ?? 2_500_000) / 1_000_000.0
let fps = result.targetFps ?? 30
let resolution = result.qualityPreset?.hasPrefix("1080p") == true ? "1080p" : "720p"
return "\(resolution) · \(fps)fps · \(String(format: "%.1f", bitrateMbps)) Mbps"
}
}