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>
255 lines
10 KiB
Swift
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"
|
|
}
|
|
}
|