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:
co-authored by
Cursor
parent
ff4de33cc5
commit
ea5da1eb86
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user