diff --git a/docs/IOS_ANDROID_I18N_GAP.md b/docs/IOS_ANDROID_I18N_GAP.md index b5bb9b9..ed22f92 100644 --- a/docs/IOS_ANDROID_I18N_GAP.md +++ b/docs/IOS_ANDROID_I18N_GAP.md @@ -1,12 +1,12 @@ # Gap Android → iOS: allineamento app nativa -Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android `2.0.4-native`**. +Documento operativo per continuare su **Mac** lo sviluppo iOS e rilasciare un’app **allineata ad Android `2.0.5-native`**. **Aggiornato:** 24 luglio 2026 -**Riferimento Android (produzione / telefono):** `2.0.4-native` (`versionCode` **25**), API `https://www.matchlivetv.it` +**Riferimento Android (produzione / telefono / Play):** `2.0.5-native` (`versionCode` **26**), API `https://www.matchlivetv.it` **Stato iOS attuale:** marketing `2.0.0`, build `21` — i18n **solo Login parziale** -Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione. +Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, wizard, broadcast) + bump versione, **senza** i bug di sessione/crash già risolti su Android 2.0.5. --- @@ -14,8 +14,8 @@ Obiettivo iOS: **stessa copertura lingua** di Android (Login, hub, sheet/dialog, Su Android (rilasciato) la lingua scelta dall’utente vale su **tutta l’app** usabile: -- Persistenza + apply (`AppLocale` / SharedPreferences `mltv_locale` + recreate) -- Icona **globo** su Login e Matches +- Persistenza + apply live (`AppLocale` / SharedPreferences `mltv_locale` + `ProvideAppLocale`) +- Icona **globo** su Login (TopAppBar) e Matches - Hub Partite localizzato - Bottom sheet e dialog Matches (es. «Choose team», nuova partita, programma, elimina, riprendi) - Wizard 3 step + branding colori @@ -28,12 +28,42 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — --- -## Stato a confronto (target = Android 2.0.4) +## Lezioni Android 2.0.5 (obbligatorie su iOS) -| Area | Android 2.0.4 | iOS oggi | Da fare su iOS | +Queste sono regressioni/bug già visti su Android; **non ripeterli** su iOS. + +| Problema | Sintomo | Fix Android (da replicare in spirito) | +|----------|---------|----------------------------------------| +| Recreate Activity al cambio lingua | Torna al Login / perde nav | **Niente recreate** obbligatorio: aggiorna UI via stato lingua + refresh stringhe | +| Context “nudo” per le stringhe | Crash wizard: `No ActivityResultRegistryOwner` / picker foto | Mantieni **Activity** come host; localizza solo Resources / environment | +| `findActivity()` su context configurazione | Cambio lingua non applica (apply silent no-op) | Applica lingua con `applicationContext` / store globale, non dipendere da Activity dal dialog | +| Globe sotto lo scroll del form Login | Tap ignorato | Globe in **toolbar/top bar**, non overlay sotto `ScrollView`/`Column` | +| Splash che richiede rete per restare loggato | Dopo cambio lingua → Login se API lenta | Se token locale presente → resta autenticato; refresh rete best-effort | + +### Pattern Android di riferimento (Compose) + +1. `AppLocale.tagFlow` + prefs `mltv_locale` +2. `ProvideAppLocale`: `LocalConfiguration` + `LocalizedContextWrapper(Activity, localizedResources)` + → stringhe aggiornate **e** `LocalActivityResultRegistryOwner` integro +3. `LanguagePickerDialog` chiama `AppLocale.apply(context, tag)` (applicationContext), non `findActivity()` obbligatorio +4. Splash: `currentSession() != null` → Matches; `validateOrRefresh()` in background + +### Equivalente SwiftUI consigliato + +1. `AppLanguage` / `@AppStorage` + `NotificationCenter` (già presente) o `environment(\.locale, …)` +2. Root: `.environment(\.locale, locale)` **e** `.id(languageTick)` sulle schermate che mostrano testo +3. **Non** distruggere `NavigationStack` / session store al cambio lingua +4. Photo picker / `PhotosPicker` / `UIImagePicker`: restano agganciati alla `View` host, non a un context “fake” +5. Cold start: se token in Keychain/UserDefaults → vai a Matches anche se `/me` fallisce temporaneamente + +--- + +## Stato a confronto (target = Android 2.0.5) + +| Area | Android 2.0.5 | iOS oggi | Da fare su iOS | |------|---------------|----------|----------------| -| Persistenza lingua | SharedPreferences + wrap + recreate | UserDefaults + notification | Verificare refresh UI su **tutte** le schermate (non solo Login/Matches root) | -| Picker Login | Icona globo top-right | Testo / picker presente | Allineare a **icona globo** come Android | +| Persistenza lingua | SharedPreferences + `ProvideAppLocale` (no recreate nav) | UserDefaults + notification | Refresh UI su **tutte** le schermate **senza** reset sessione/nav | +| Picker Login | Icona globo in **TopAppBar** | Testo / picker presente | Allineare a **icona globo** in toolbar | | Picker Matches | Icona globo top bar | Presente | OK se già icona; altrimenti allineare | | Login stringhe | Sì | Sì (`L10n`) | OK | | Slogan wordmark | `app_slogan` | Chiave `app.slogan` **non usata** | Collegare `L10n.t("app.slogan")` | @@ -46,7 +76,7 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — | Broadcast + score dialog | Sì (`broadcast_*`, `score_*`) | IT | Localizzare | | Catalogo stringhe | `values` + `values-en/fr/de/es` (~230 chiavi) | ~14 chiavi in `AppLanguage.swift` | Espandere `L10n` (o migrare a `Localizable.xcstrings`) | | Logout UI | Icona | Testo | Icona SF Symbol | -| Versione | `2.0.4-native` / **25** | `2.0.0` / **21** | Bump a **2.0.4** / **25** (o build successiva se 22–25 già usati su TestFlight) | +| Versione | `2.0.5-native` / **26** | `2.0.0` / **21** | Bump a **2.0.5** / **26** (o build successiva se conflitto TestFlight) | | RTMP ingest | `RtmpIngestUrl.kt` | `MediaUrl.swift` | OK in prod; allineare solo semantica dev se serve | --- @@ -57,9 +87,10 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — | Ruolo | Path | |-------|------| -| Locale | `native/android/.../core/AppLocale.kt` | +| Locale + ProvideAppLocale | `native/android/.../core/AppLocale.kt` | | Picker | `native/android/.../ui/components/LanguagePickerDialog.kt` | | Activity | `native/android/.../MainActivity.kt` | +| Splash (sessione locale) | `native/android/.../ui/splash/SplashScreen.kt` | | Stringhe | `native/android/app/src/main/res/values/strings.xml` + `values-{en,fr,de,es}/` | | Login | `.../ui/login/LoginScreen.kt` | | Hub | `.../ui/matches/MatchesScreen.kt` | @@ -79,6 +110,7 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — | Wizard | `UI/Wizard/` (o path equivalente) | | Broadcast | `UI/Broadcast/` (o path equivalente) | | Wordmark | `native/ios/MatchLiveTv/UI/Components/MatchLiveWordmark.swift` | +| Auth / splash equivalent | entry root / session restore | | Versioni | `generate_xcodeproj.py`, `project.pbxproj`, `Info.plist` | | Build | `scripts/build_ios_release.sh` | @@ -99,9 +131,9 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — ### Fase B — Login + wordmark (rapido) -5. Login: icona globo in alto a destra (come Android), non solo testo. +5. Login: icona globo in toolbar (come Android TopAppBar), non overlay che lo scroll può coprire. 6. `MatchLiveWordmark`: `L10n.t("app.slogan")`. -7. Verificare che al cambio lingua Login si ricrei/rinfreschi (notification già presente). +7. Cambio lingua: aggiorna stringhe **senza** buttare fuori l’utente (se già loggato altrove, stessa regola). ### Fase C — Hub Matches + sheet/dialog (priorità UX — screenshot «Scegli squadra») @@ -120,17 +152,19 @@ API produzione: entrambe le app puntano già a `https://www.matchlivetv.it` — 13. Localizzare step Partita / Trasmissione / Test rete + branding editor + color hue (`wizard.*`). 14. Errori validazione e CTA AVANTI/INDIETRO/INIZIA. +15. Verificare photo/logo picker dopo cambio lingua (su Android crashava senza Activity host). ### Fase E — Broadcast -15. Overlay controlli, contentDescription, snackbar pausa/ripresa/regia. -16. Dialog termina diretta, set concluso, chiudi set, partita terminata (`broadcast.*`, `score.*`). +16. Overlay controlli, contentDescription, snackbar pausa/ripresa/regia. +17. Dialog termina diretta, set concluso, chiudi set, partita terminata (`broadcast.*`, `score.*`). -### Fase F — Versione e release +### Fase F — Sessione + versione e release -17. Bump `MARKETING_VERSION` → `2.0.4`, `CURRENT_PROJECT_VERSION` → `25` (o successivo se conflitto TestFlight). -18. Aggiornare `generate_xcodeproj.py` **e** `Info.plist`, poi `python3 generate_xcodeproj.py`. -19. `./scripts/build_ios_release.sh` (API default già produzione). +18. Cold start / cambio lingua: token locale → resta in Matches (non forzare Login se `/me` fallisce). +19. Bump `MARKETING_VERSION` → `2.0.5`, `CURRENT_PROJECT_VERSION` → `26` (o successivo se conflitto TestFlight). +20. Aggiornare `generate_xcodeproj.py` **e** `Info.plist`, poi `python3 generate_xcodeproj.py`. +21. `./scripts/build_ios_release.sh` (API default già produzione). --- @@ -156,14 +190,15 @@ Non lasciare fuori scope wizard/broadcast: su Android **sono già tradotti**; iO 1. `cd native/ios && python3 generate_xcodeproj.py` 2. Build simulatore; login produzione. 3. Login: globo → English → label/errori/slogan in EN. -4. Matches: saluto, CTA, empty state in EN. -5. Apri «Change» / scegli squadra: titolo **«Choose team»** (non «Scegli squadra»). -6. Nuova partita / programma / elimina: tutto EN. -7. Wizard 3 step in EN. -8. (Se possibile) overlay diretta: dialog/termina in EN. -9. Kill app → riapri: lingua persistita. -10. Spot-check FR/DE/ES. -11. Release: `./scripts/build_ios_release.sh` +4. **Loggato:** cambio lingua da Matches → resti su Matches, testi EN (niente ritorno al Login). +5. Matches: saluto, CTA, empty state in EN. +6. Apri «Change» / scegli squadra: titolo **«Choose team»** (non «Scegli squadra»). +7. Nuova partita → Avvia ora → wizard si apre **senza crash**; step in EN. +8. Wizard 3 step in EN; logo picker funziona. +9. (Se possibile) overlay diretta: dialog/termina in EN. +10. Kill app → riapri: lingua persistita **e** sessione resta se token valido. +11. Spot-check FR/DE/ES. +12. Release: `./scripts/build_ios_release.sh` Test: @@ -189,15 +224,28 @@ xcodebuild -project MatchLiveTv.xcodeproj -scheme MatchLiveTv \ Nessuna modifica server richiesta. +### Artefatti Android Play (riferimento) + +```bash +# APK installazione diretta +./scripts/build_native_android_apk_prod.sh + +# AAB per Google Play Console +cd native/android && ./gradlew bundleRelease -PAPI_BASE_URL=https://www.matchlivetv.it +# → app/build/outputs/bundle/release/app-release.aab +``` + +Versione corrente store: **`2.0.5-native` / versionCode `26`**. + --- ## Sintesi priorità (ordine di esecuzione) 1. **A** Catalogo stringhe completo (copiare da Android `values*`) 2. **C** Hub + sheet/dialog (fix «Scegli squadra» in EN) -3. **B** Slogan + globo Login -4. **D** Wizard +3. **B** Slogan + globo Login (toolbar) +4. **D** Wizard (+ no crash picker) 5. **E** Broadcast -6. **F** Versione `2.0.4` / `25` + build release +6. **F** Sessione stabile al cambio lingua + versione `2.0.5` / `26` + build release -Quando A–F sono chiusi, iOS è allineato ad Android **2.0.4-native** per UX multilingua e versioning store. +Quando A–F sono chiusi, iOS è allineato ad Android **2.0.5-native** per UX multilingua, stabilità sessione e versioning store. diff --git a/native/android/app/build.gradle.kts b/native/android/app/build.gradle.kts index 6c9d0b0..ebb00d4 100644 --- a/native/android/app/build.gradle.kts +++ b/native/android/app/build.gradle.kts @@ -20,8 +20,8 @@ android { applicationId = "com.matchlivetv.match_live_tv" minSdk = 24 targetSdk = 36 - versionCode = 25 - versionName = "2.0.4-native" + versionCode = 26 + versionName = "2.0.5-native" val apiBaseUrl = project.findProperty("API_BASE_URL") as String? ?: "https://www.matchlivetv.it" diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt index c36d47d..94ce481 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/MainActivity.kt @@ -6,8 +6,8 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.runtime.key import com.matchlivetv.match_live_tv.core.AppLocale +import com.matchlivetv.match_live_tv.core.ProvideAppLocale import com.matchlivetv.match_live_tv.ui.navigation.AppNavHost import com.matchlivetv.match_live_tv.ui.theme.MatchLiveTheme @@ -28,9 +28,10 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - val localeTag = AppLocale.currentTag(this) + // Inizializza il flow dalla preferenza salvata prima del primo frame Compose. + AppLocale.currentTag(this) setContent { - key(localeTag) { + ProvideAppLocale { MatchLiveTheme { AppNavHost(container) } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/core/AppLocale.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/core/AppLocale.kt index 4fa5d80..a514c28 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/core/AppLocale.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/core/AppLocale.kt @@ -8,12 +8,25 @@ import android.os.Handler import android.os.LocaleList import android.os.Looper import androidx.appcompat.app.AppCompatDelegate +import androidx.activity.compose.LocalActivityResultRegistryOwner +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext import androidx.core.os.LocaleListCompat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import java.util.Locale /** - * Lingua app persistita in SharedPreferences + context wrapping. - * Più affidabile di soli AppCompat locales su ComponentActivity/Compose. + * Lingua app persistita in SharedPreferences. + * L'UI Compose si aggiorna via [ProvideAppLocale] senza recreate Activity + * (così non si perde navigazione/sessione). */ object AppLocale { const val SYSTEM = "" @@ -21,6 +34,11 @@ object AppLocale { private const val PREFS = "mltv_locale" private const val KEY_TAG = "tag" + private val tagState = MutableStateFlow(null) + + /** Tag corrente (null = non ancora letto; i consumer usano [currentTag]). */ + val tagFlow: StateFlow = tagState.asStateFlow() + data class Option(val tag: String, val labelRes: Int) val options = listOf( @@ -38,46 +56,117 @@ object AppLocale { } fun currentTag(context: Context): String { + tagState.value?.let { return normalize(it) } val stored = context.applicationContext .getSharedPreferences(PREFS, Context.MODE_PRIVATE) .getString(KEY_TAG, SYSTEM) .orEmpty() - return normalize(stored) + return normalize(stored).also { tagState.value = it } } fun wrap(context: Context): Context { val tag = currentTag(context) - if (tag.isBlank()) return context - - val locale = Locale.forLanguageTag(tag) - Locale.setDefault(locale) - val config = Configuration(context.resources.configuration) - config.setLocales(LocaleList(locale)) - return context.createConfigurationContext(config) + return wrapWithTag(context, tag) } - fun apply(activity: Activity, tag: String) { + fun wrapWithTag(context: Context, tag: String): Context { + return context.createConfigurationContext(configurationFor(context, tag)) + } + + /** + * Context per Compose: mantiene l'Activity come base (serve a + * LocalActivityResultRegistryOwner / dialog / picker), ma espone + * Resources nella lingua scelta. + */ + fun wrapForCompose(context: Context, tag: String): Context { + val config = configurationFor(context, tag) + val localized = context.createConfigurationContext(config) + val activity = context.findActivity() ?: return localized + return LocalizedContextWrapper(activity, localized) + } + + fun configurationFor(context: Context, tag: String): Configuration { val normalized = normalize(tag) - val prefs = activity.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE) - if (normalized == currentTag(activity)) return + if (normalized.isBlank()) { + // Lingua di sistema: usa la config del device, non quella già wrappata dall'Activity. + return Configuration(context.applicationContext.resources.configuration) + } + val locale = Locale.forLanguageTag(normalized) + Locale.setDefault(locale) + val config = Configuration(context.applicationContext.resources.configuration) + config.setLocales(LocaleList(locale)) + return config + } + + /** + * Applica la lingua. Non richiede Activity: basta qualsiasi Context + * (anche createConfigurationContext) perché usa applicationContext. + */ + fun apply(context: Context, tag: String) { + val app = context.applicationContext + val normalized = normalize(tag) + val prefs = app.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + if (normalized == currentTag(app)) return - // commit sincronizzato: deve essere scritto prima del recreate prefs.edit().putString(KEY_TAG, normalized).commit() + tagState.value = normalized val locales = if (normalized.isBlank()) { LocaleListCompat.getEmptyLocaleList() } else { LocaleListCompat.forLanguageTags(normalized) } - AppCompatDelegate.setApplicationLocales(locales) - activity.recreate() + runCatching { AppCompatDelegate.setApplicationLocales(locales) } } - fun applyAfterDismiss(activity: Activity?, tag: String, dismiss: () -> Unit) { + fun applyAfterDismiss(context: Context, tag: String, dismiss: () -> Unit) { dismiss() Handler(Looper.getMainLooper()).post { - if (activity == null || activity.isFinishing || activity.isDestroyed) return@post - apply(activity, tag) + apply(context, tag) + } + } +} + +/** + * Wrapper che resta un [ContextWrapper] sull'Activity (findActivity / ActivityResult) + * ma legge stringhe/asset dal context localizzato. + */ +private class LocalizedContextWrapper( + base: Context, + private val localized: Context, +) : ContextWrapper(base) { + override fun getResources() = localized.resources + override fun getAssets() = localized.assets +} + +@Composable +fun ProvideAppLocale(content: @Composable () -> Unit) { + val baseContext = LocalContext.current + val baseConfiguration = LocalConfiguration.current + val activityResultOwner = LocalActivityResultRegistryOwner.current + ?: baseContext.findActivity() as? ActivityResultRegistryOwner + val storedTag = remember(baseContext) { AppLocale.currentTag(baseContext) } + val tagFromFlow by AppLocale.tagFlow.collectAsState(initial = storedTag) + val tag = AppLocale.normalize(tagFromFlow ?: storedTag) + + val localizedContext = remember(baseContext, tag, baseConfiguration) { + AppLocale.wrapForCompose(baseContext, tag) + } + val localizedConfiguration = remember(baseContext, tag, baseConfiguration) { + AppLocale.configurationFor(baseContext, tag) + } + + CompositionLocalProvider( + LocalContext provides localizedContext, + LocalConfiguration provides localizedConfiguration, + ) { + if (activityResultOwner != null) { + CompositionLocalProvider( + LocalActivityResultRegistryOwner provides activityResultOwner, + content = content, + ) + } else { + content() } } } diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/components/LanguagePickerDialog.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/components/LanguagePickerDialog.kt index 7a1e287..977a139 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/components/LanguagePickerDialog.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/components/LanguagePickerDialog.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.matchlivetv.match_live_tv.R import com.matchlivetv.match_live_tv.core.AppLocale -import com.matchlivetv.match_live_tv.core.findActivity import com.matchlivetv.match_live_tv.ui.theme.MatchColors @Composable @@ -26,7 +25,6 @@ fun LanguagePickerDialog( onDismiss: () -> Unit, ) { val context = LocalContext.current - val activity = context.findActivity() val selected = AppLocale.currentTag(context) AlertDialog( onDismissRequest = onDismiss, @@ -37,7 +35,7 @@ fun LanguagePickerDialog( val optionTag = AppLocale.normalize(option.tag) val isSelected = selected == optionTag fun choose() { - AppLocale.applyAfterDismiss(activity, option.tag, onDismiss) + AppLocale.applyAfterDismiss(context, option.tag, onDismiss) } Row( modifier = Modifier diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt index 87a1fb3..571c617 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/login/LoginScreen.kt @@ -1,6 +1,5 @@ package com.matchlivetv.match_live_tv.ui.login -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -14,11 +13,14 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -28,6 +30,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -45,6 +48,7 @@ import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold import com.matchlivetv.match_live_tv.ui.theme.MatchColors import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen( container: AppContainer, @@ -82,26 +86,33 @@ fun LoginScreen( } } - MatchScreenScaffold { + MatchScreenScaffold( + topBar = { + TopAppBar( + title = {}, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MatchColors.Background, + actionIconContentColor = MatchColors.TextSecondary, + ), + actions = { + IconButton(onClick = { showLanguagePicker = true }) { + Icon( + imageVector = Icons.Filled.Language, + contentDescription = stringResource(R.string.language_label), + tint = MatchColors.TextSecondary, + ) + } + }, + ) + }, + ) { if (showLanguagePicker) { LanguagePickerDialog(onDismiss = { showLanguagePicker = false }) } - Box(modifier = Modifier.fillMaxWidth()) { - IconButton( - onClick = { showLanguagePicker = true }, - modifier = Modifier.align(Alignment.TopEnd), - ) { - Icon( - Icons.Default.Language, - contentDescription = stringResource(R.string.language_label), - tint = MatchColors.TextSecondary, - ) - } - } Column( modifier = Modifier .verticalScroll(rememberScrollState()) - .padding(horizontal = 24.dp, vertical = 32.dp), + .padding(horizontal = 24.dp, vertical = 16.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { MatchLiveWordmark(showSlogan = true) @@ -181,8 +192,8 @@ fun LoginScreen( @Composable private fun matchTextFieldColors() = OutlinedTextFieldDefaults.colors( - focusedTextColor = androidx.compose.ui.graphics.Color.White, - unfocusedTextColor = androidx.compose.ui.graphics.Color.White, + focusedTextColor = Color.White, + unfocusedTextColor = Color.White, focusedBorderColor = MatchColors.PrimaryRed, unfocusedBorderColor = MatchColors.Outline, focusedLabelColor = MatchColors.PrimaryRed, diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt index f6da059..9c22e0d 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/matches/MatchSheets.kt @@ -55,6 +55,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.matchlivetv.match_live_tv.R +import com.matchlivetv.match_live_tv.core.findActivity import com.matchlivetv.match_live_tv.core.isScheduledFuture import com.matchlivetv.match_live_tv.core.parseApiInstant import com.matchlivetv.match_live_tv.domain.Match @@ -139,11 +140,12 @@ fun ScheduleMatchBottomSheet( } fun pickDateTime() { + val dialogContext = context.findActivity() ?: context DatePickerDialog( - context, + dialogContext, { _, year, month, day -> TimePickerDialog( - context, + dialogContext, { _, hour, minute -> scheduledAt = LocalDateTime.of(year, month + 1, day, hour, minute) }, diff --git a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/splash/SplashScreen.kt b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/splash/SplashScreen.kt index ec4b5fe..23c7d9e 100644 --- a/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/splash/SplashScreen.kt +++ b/native/android/app/src/main/kotlin/com/matchlivetv/match_live_tv/ui/splash/SplashScreen.kt @@ -22,8 +22,15 @@ fun SplashScreen( ) { LaunchedEffect(Unit) { container.bootstrapAuth() - val session = container.authRepository.validateOrRefresh() - if (session != null) onAuthenticated() else onUnauthenticated() + val stored = container.authRepository.currentSession() + if (stored == null) { + onUnauthenticated() + return@LaunchedEffect + } + // Sessione locale presente: resta autenticato anche se la rete fallisce + // (es. recreate dopo cambio lingua). Refresh best-effort in background. + onAuthenticated() + runCatching { container.authRepository.validateOrRefresh() } } MatchScreenScaffold {