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>
This commit is contained in:
Emiliano Frascaro
2026-07-24 11:19:37 +02:00
co-authored by Cursor
parent ff4de33cc5
commit ea5da1eb86
46 changed files with 2675 additions and 1241 deletions
@@ -36,13 +36,16 @@ struct MatchesScreen: View {
.foregroundStyle(MatchColors.textSecondary)
}
.accessibilityLabel(L10n.t("language.label"))
Button(L10n.t("action.logout")) {
Button {
Task {
await container.authRepository.logout()
onLogout()
}
} label: {
Image(systemName: "rectangle.portrait.and.arrow.right")
.foregroundStyle(MatchColors.textSecondary)
}
.foregroundStyle(MatchColors.textSecondary)
.accessibilityLabel(L10n.t("action.logout"))
}
.padding(.horizontal, 16)
.padding(.vertical, 8)
@@ -53,29 +56,33 @@ struct MatchesScreen: View {
ProgressView().tint(MatchColors.primaryRed)
} else if teams.isEmpty {
VStack(spacing: 16) {
Text("Nessuna squadra disponibile")
Text(L10n.t("matches.no.team.title"))
.foregroundStyle(MatchColors.textSecondary)
MatchPrimaryButton(label: "RIPROVA", action: { reload(showSpinner: false) })
Text(L10n.t("matches.no.team.body"))
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.multilineTextAlignment(.center)
MatchPrimaryButton(label: L10n.t("matches.retry").uppercased(), action: { reload(showSpinner: false) })
}
.padding(24)
} else if let error {
VStack(spacing: 16) {
Text(error).foregroundStyle(MatchColors.primaryRed).multilineTextAlignment(.center)
MatchPrimaryButton(label: "RIPROVA", action: { reload(showSpinner: false) })
MatchPrimaryButton(label: L10n.t("matches.retry").uppercased(), action: { reload(showSpinner: false) })
}
.padding(24)
} else {
ScrollView {
VStack(alignment: .leading, spacing: 0) {
VStack(alignment: .leading, spacing: 16) {
Text("Ciao, \(container.tokenStore.session?.user.name ?? "")")
Text(L10n.t("matches.hello", container.tokenStore.session?.user.name ?? ""))
.font(MatchTypography.headlineMedium)
Text("Riprendi una diretta in corso o avvia una partita programmata.")
Text(L10n.t("matches.subtitle"))
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
HStack(spacing: 12) {
MatchSecondaryButton(label: "PARTITA PROGRAMMATA", action: { showSchedule = true })
MatchPrimaryButton(label: "NUOVA PARTITA", action: { showNewMatch = true })
MatchSecondaryButton(label: L10n.t("matches.schedule").uppercased(), action: { showSchedule = true })
MatchPrimaryButton(label: L10n.t("matches.new").uppercased(), action: { showNewMatch = true })
}
if let activeTeam {
TeamPickerBar(
@@ -108,7 +115,7 @@ struct MatchesScreen: View {
.padding(.horizontal, 24)
.padding(.vertical, 12)
} else if calendarMatches.isEmpty {
Text("Nessuna altra partita in calendario.")
Text(L10n.t("matches.no.other"))
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.multilineTextAlignment(.center)
@@ -136,27 +143,41 @@ struct MatchesScreen: View {
}
)
.task(id: refreshToken) { reload(showSpinner: refreshToken == 0) }
.alert("Riprendi diretta?", isPresented: Binding(get: { resumeMatch != nil }, set: { if !$0 { resumeMatch = nil } })) {
Button("Riprendi") {
.alert(L10n.t("sheet.configure.now.title"), isPresented: Binding(get: { resumeMatch != nil }, set: { if !$0 { resumeMatch = nil } })) {
Button(L10n.t("sheet.resume.camera")) {
if let match = resumeMatch { resumeBroadcast(match) }
resumeMatch = nil
}
Button("Configura", role: .cancel) {
Button(L10n.t("sheet.configure"), role: .cancel) {
if let match = resumeMatch { onOpenSetup(match.id) }
resumeMatch = nil
}
} message: {
Text(L10n.t("sheet.configure.now.body"))
}
.alert("Elimina partita?", isPresented: Binding(get: { deleteMatch != nil }, set: { if !$0 { deleteMatch = nil } })) {
Button("Elimina", role: .destructive) {
.alert(
L10n.t("sheet.delete.match.title"),
isPresented: Binding(get: { deleteMatch != nil }, set: { if !$0 { deleteMatch = nil } })
) {
Button(L10n.t("sheet.delete"), role: .destructive) {
if let match = deleteMatch {
Task {
try? await container.matchRepository.deleteMatch(matchId: match.id)
reload(showSpinner: false)
do {
try await container.matchRepository.deleteMatch(matchId: match.id)
snackbar = L10n.t("matches.msg.deleted")
reload(showSpinner: false)
} catch {
snackbar = L10n.t("matches.msg.delete.failed")
}
}
}
deleteMatch = nil
}
Button("Annulla", role: .cancel) { deleteMatch = nil }
Button(L10n.t("action.cancel"), role: .cancel) { deleteMatch = nil }
} message: {
if let match = deleteMatch {
Text(L10n.t("sheet.delete.match.body", match.teamName, match.opponentName))
}
}
.sheet(isPresented: $showNewMatch) {
NewMatchSheet(
@@ -229,21 +250,21 @@ struct MatchesScreen: View {
private var calendarSectionTitle: String {
if calendarMatches.isEmpty && activeMatch == nil {
return "Nessuna partita in calendario"
return L10n.t("matches.empty.title")
}
if !scheduledMatches.isEmpty {
return "Partite programmate"
return L10n.t("matches.scheduled.title")
}
return "Pronte da avviare"
return L10n.t("matches.ready.title")
}
private var emptyCalendarMessage: String {
var message = "Programma una partita o avviane una nuova con «Nuova partita»."
var message = L10n.t("matches.empty.hint")
if let teamName = activeTeam?.name {
message += "\n\nSquadra attiva: \(teamName)."
message += "\n\n" + L10n.t("matches.active.team", teamName)
}
if teams.count > 1 {
message += "\nHai più squadre: verifica quella selezionata sopra."
message += "\n" + L10n.t("matches.multi.team.hint")
}
return message
}
@@ -260,7 +281,7 @@ struct MatchesScreen: View {
matches = try await container.matchRepository.fetchMatchesForTeam(teamId: team.id)
}
} catch {
self.error = UserFacingError.message(for: error)
self.error = UserFacingError.message(for: error) ?? L10n.t("matches.msg.load.error")
}
loading = false
refreshing = false
@@ -279,7 +300,7 @@ struct MatchesScreen: View {
let sessionId = try await MatchSessionLauncher.resumeBroadcastSession(match: match, sessionRepository: container.sessionRepository)
onOpenBroadcast(sessionId)
} catch {
snackbar = UserFacingError.message(for: error)
snackbar = UserFacingError.message(for: error) ?? L10n.t("matches.msg.resume.failed")
}
actionLoading = false
}
@@ -294,7 +315,7 @@ struct MatchesScreen: View {
reload(showSpinner: false)
onOpenSetup(match.id)
} catch {
snackbar = UserFacingError.message(for: error)
snackbar = UserFacingError.message(for: error) ?? L10n.t("matches.msg.create.failed")
}
actionLoading = false
}
@@ -335,7 +356,7 @@ private struct ActiveSessionBanner: View {
Image(systemName: "video.fill")
.foregroundStyle(MatchColors.primaryRed)
VStack(alignment: .leading, spacing: 4) {
Text("Riprendi diretta in corso")
Text(L10n.t("sheet.resume.banner"))
.font(MatchTypography.titleMedium)
.foregroundStyle(MatchColors.primaryRed)
Text("\(match.teamName) vs \(match.opponentName)")
@@ -409,23 +430,23 @@ private struct NewMatchSheet: View {
var body: some View {
VStack(alignment: .leading, spacing: 0) {
Text("Nuova partita")
Text(L10n.t("sheet.new.match.title"))
.font(MatchTypography.headlineMedium)
Text("Programma in anticipo o avvia la configurazione diretta subito.")
Text(L10n.t("sheet.new.match.lead"))
.font(MatchTypography.bodyMedium)
.foregroundStyle(MatchColors.textSecondary)
.padding(.top, 6)
VStack(spacing: 10) {
SheetOptionTile(
systemImage: "calendar.badge.clock",
title: "Programma partita",
subtitle: "Data, ora e avversario — visibile anche sul sito",
title: L10n.t("sheet.schedule.option"),
subtitle: L10n.t("sheet.schedule.option.sub"),
action: onSchedule
)
SheetOptionTile(
systemImage: "play.circle",
title: "Avvia subito",
subtitle: "Crea la partita e passa al wizard senza orario",
title: L10n.t("sheet.quick.option"),
subtitle: L10n.t("sheet.quick.option.sub"),
action: onQuickStart
)
}
@@ -481,15 +502,18 @@ private struct ScheduleMatchSheet: View {
var body: some View {
NavigationStack {
Form {
TextField("Avversario", text: $opponent)
TextField("Luogo", text: $location)
DatePicker("Data", selection: $date)
TextField(L10n.t("sheet.opponent"), text: $opponent)
TextField(L10n.t("sheet.location.optional"), text: $location)
DatePicker(L10n.t("sheet.date.time"), selection: $date)
}
.navigationTitle("Partita programmata")
.navigationTitle(L10n.t("sheet.schedule.title"))
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("Chiudi") { dismiss() } }
ToolbarItem(placement: .cancellationAction) {
Button(L10n.t("common.close")) { dismiss() }
}
ToolbarItem(placement: .confirmationAction) {
Button("Crea") { create() }.disabled(opponent.isEmpty || teamId == nil)
Button(L10n.t("sheet.save.schedule")) { create() }
.disabled(opponent.isEmpty || teamId == nil)
}
}
}
@@ -498,13 +522,16 @@ private struct ScheduleMatchSheet: View {
private func create() {
guard let teamId else { return }
Task {
if let match = try? await container.matchRepository.createScheduledMatch(
teamId: teamId,
opponentName: opponent,
scheduledAt: date,
location: location.nilIfEmpty
) {
do {
let match = try await container.matchRepository.createScheduledMatch(
teamId: teamId,
opponentName: opponent,
scheduledAt: date,
location: location.nilIfEmpty
)
onCreated(match)
} catch {
// Sheet chiude solo su successo; errori gestiti dal chiamante via snackbar se necessario.
}
}
}
@@ -532,7 +559,7 @@ private struct TeamPickerSheet: View {
}
}
}
.navigationTitle("Squadra")
.navigationTitle(L10n.t("sheet.choose.team"))
}
}
}