Aggiunge i18n IT/EN/FR/DE/ES, pagine pubbliche squadre e UX archivio.

Il selettore lingua funziona sul web e sulle app native; su Android la preferenza è persistita e applicata al riavvio. Incluse anche eliminazione replay a scope e campi pagina pubblica squadra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-23 19:30:44 +02:00
co-authored by Cursor
parent 7d5d66840f
commit 1d97271555
71 changed files with 3509 additions and 197 deletions
+5 -4
View File
@@ -14,14 +14,14 @@ if (keystorePropertiesFile.exists()) {
android {
namespace = "com.matchlivetv.match_live_tv"
compileSdk = 35
compileSdk = 36
defaultConfig {
applicationId = "com.matchlivetv.match_live_tv"
minSdk = 24
targetSdk = 35
versionCode = 21
versionName = "2.0.0-native"
targetSdk = 36
versionCode = 23
versionName = "2.0.2-native"
val apiBaseUrl = project.findProperty("API_BASE_URL") as String?
?: "https://www.matchlivetv.it"
@@ -75,6 +75,7 @@ dependencies {
implementation(composeBom)
androidTestImplementation(composeBom)
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.core:core-ktx:1.15.0")
implementation("androidx.activity:activity-compose:1.9.3")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.7")
@@ -21,6 +21,7 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.MatchLiveTv"
android:localeConfig="@xml/locales_config"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config">
@@ -29,7 +30,7 @@
android:exported="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
@@ -57,5 +58,15 @@
android:name=".streaming.LiveBroadcastService"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
<!-- Persistenza lingue AppCompat (API < 33) -->
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application>
</manifest>
@@ -1,10 +1,13 @@
package com.matchlivetv.match_live_tv
import android.content.Context
import android.content.res.Configuration
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.ui.navigation.AppNavHost
import com.matchlivetv.match_live_tv.ui.theme.MatchLiveTheme
@@ -12,6 +15,10 @@ class MainActivity : ComponentActivity() {
private val container by lazy { (application as MatchLiveTvApplication).container }
override fun attachBaseContext(newBase: Context) {
super.attachBaseContext(AppLocale.wrap(newBase))
}
override fun onConfigurationChanged(newConfig: Configuration) {
container.broadcastCoordinator.pauseForConfigurationChange()
super.onConfigurationChanged(newConfig)
@@ -21,9 +28,12 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
val localeTag = AppLocale.currentTag(this)
setContent {
MatchLiveTheme {
AppNavHost(container)
key(localeTag) {
MatchLiveTheme {
AppNavHost(container)
}
}
}
}
@@ -0,0 +1,89 @@
package com.matchlivetv.match_live_tv.core
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.content.res.Configuration
import android.os.Handler
import android.os.LocaleList
import android.os.Looper
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
import java.util.Locale
/**
* Lingua app persistita in SharedPreferences + context wrapping.
* Più affidabile di soli AppCompat locales su ComponentActivity/Compose.
*/
object AppLocale {
const val SYSTEM = ""
private const val PREFS = "mltv_locale"
private const val KEY_TAG = "tag"
data class Option(val tag: String, val labelRes: Int)
val options = listOf(
Option(SYSTEM, com.matchlivetv.match_live_tv.R.string.language_system),
Option("it", com.matchlivetv.match_live_tv.R.string.language_italian),
Option("en", com.matchlivetv.match_live_tv.R.string.language_english),
Option("fr", com.matchlivetv.match_live_tv.R.string.language_french),
Option("de", com.matchlivetv.match_live_tv.R.string.language_german),
Option("es", com.matchlivetv.match_live_tv.R.string.language_spanish),
)
fun normalize(tag: String): String {
if (tag.isBlank()) return SYSTEM
return tag.substringBefore(",").substringBefore("-").substringBefore("_").lowercase()
}
fun currentTag(context: Context): String {
val stored = context.applicationContext
.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
.getString(KEY_TAG, SYSTEM)
.orEmpty()
return normalize(stored)
}
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)
}
fun apply(activity: Activity, tag: String) {
val normalized = normalize(tag)
val prefs = activity.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
if (normalized == currentTag(activity)) return
// commit sincronizzato: deve essere scritto prima del recreate
prefs.edit().putString(KEY_TAG, normalized).commit()
val locales = if (normalized.isBlank()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(normalized)
}
AppCompatDelegate.setApplicationLocales(locales)
activity.recreate()
}
fun applyAfterDismiss(activity: Activity?, tag: String, dismiss: () -> Unit) {
dismiss()
Handler(Looper.getMainLooper()).post {
if (activity == null || activity.isFinishing || activity.isDestroyed) return@post
apply(activity, tag)
}
}
}
tailrec fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
@@ -0,0 +1,47 @@
package com.matchlivetv.match_live_tv.core
import java.net.URI
/**
* Normalizza l'URL RTMP restituito dall'API per il dispositivo corrente.
* In locale Docker espone spesso 127.0.0.1 / mediamtx: dall'emulatore Android
* vanno riscritti sull'host dell'API (es. 10.0.2.2).
*/
object RtmpIngestUrl {
private val rewriteHosts = setOf(
"localhost",
"127.0.0.1",
"mediamtx",
"host.docker.internal",
)
fun resolveForDevice(urlString: String): String =
resolve(urlString, apiBaseUrl = AppConfig.apiBaseUrl)
fun resolve(urlString: String, apiBaseUrl: String): String {
if (urlString.isBlank()) return urlString
val uri = runCatching { URI(urlString) }.getOrNull() ?: return urlString
val host = uri.host?.lowercase() ?: return urlString
if (host !in rewriteHosts) return urlString
val apiHost = runCatching { URI(apiBaseUrl).host }.getOrNull()
?.takeIf { it.isNotBlank() }
?: return urlString
val port = if (uri.port > 0) uri.port else 1935
val path = uri.rawPath.orEmpty()
val query = uri.rawQuery
return buildString {
append("rtmp://")
append(apiHost)
append(':')
append(port)
append(path)
if (!query.isNullOrBlank()) {
append('?')
append(query)
}
}
}
}
@@ -32,6 +32,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import com.matchlivetv.match_live_tv.core.DeviceTelemetry
import com.matchlivetv.match_live_tv.core.parseColorHex
import com.matchlivetv.match_live_tv.core.resolveMediaUrl
import com.matchlivetv.match_live_tv.core.RtmpIngestUrl
import com.matchlivetv.match_live_tv.data.AppContainer
import com.matchlivetv.match_live_tv.domain.Match
import com.matchlivetv.match_live_tv.domain.MatchScoringRules
@@ -85,7 +86,10 @@ fun BroadcastScreen(
}
fun broadcastConfig(loaded: StreamSession): BroadcastConfig =
StreamVideoPreset.broadcastConfig(loaded, loaded.rtmpIngestUrl.orEmpty())
StreamVideoPreset.broadcastConfig(
loaded,
RtmpIngestUrl.resolveForDevice(loaded.rtmpIngestUrl.orEmpty()),
)
suspend fun stopStreamPermanently() {
runCatching { container.sessionRepository.stopSession(sessionId) }
@@ -0,0 +1,69 @@
package com.matchlivetv.match_live_tv.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
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
fun LanguagePickerDialog(
onDismiss: () -> Unit,
) {
val context = LocalContext.current
val activity = context.findActivity()
val selected = AppLocale.currentTag(context)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.language_label)) },
text = {
Column {
AppLocale.options.forEach { option ->
val optionTag = AppLocale.normalize(option.tag)
val isSelected = selected == optionTag
fun choose() {
AppLocale.applyAfterDismiss(activity, option.tag, onDismiss)
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = ::choose)
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Start,
) {
RadioButton(
selected = isSelected,
onClick = ::choose,
)
Text(
text = stringResource(option.labelRes),
color = MatchColors.TextSecondary,
modifier = Modifier.padding(start = 4.dp),
)
}
}
}
},
confirmButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
@@ -17,6 +17,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -27,13 +28,16 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.matchlivetv.match_live_tv.R
import com.matchlivetv.match_live_tv.data.AppContainer
import com.matchlivetv.match_live_tv.ui.components.LanguagePickerDialog
import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
@@ -50,8 +54,12 @@ fun LoginScreen(
var error by remember { mutableStateOf<String?>(null) }
var loading by remember { mutableStateOf(false) }
var passwordVisible by remember { mutableStateOf(false) }
var showLanguagePicker by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val errCredentials = stringResource(R.string.login_error_credentials)
val errUnreachable = stringResource(R.string.login_error_unreachable)
val errGeneric = stringResource(R.string.login_error_generic)
fun submitLogin() {
if (loading || email.isBlank() || password.isBlank()) return
@@ -64,17 +72,19 @@ fun LoginScreen(
onLoggedIn()
}.onFailure {
error = when {
it.message?.contains("401") == true -> "Email o password non corretti"
it.message?.contains("timeout", ignoreCase = true) == true ->
"Server non raggiungibile. Verifica la connessione."
else -> it.message ?: "Login fallito"
it.message?.contains("401") == true -> errCredentials
it.message?.contains("timeout", ignoreCase = true) == true -> errUnreachable
else -> it.message ?: errGeneric
}
}
loading = false
}
}
MatchScreenScaffold {
MatchScreenScaffold {
if (showLanguagePicker) {
LanguagePickerDialog(onDismiss = { showLanguagePicker = false })
}
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
@@ -82,22 +92,20 @@ fun LoginScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
MatchLiveWordmark(showSlogan = true)
Spacer(Modifier.height(48.dp))
Spacer(Modifier.height(16.dp))
TextButton(onClick = { showLanguagePicker = true }) {
Text(stringResource(R.string.language_label), color = MatchColors.TextSecondary)
}
Spacer(Modifier.height(32.dp))
Text(
text = "ACCEDI",
text = stringResource(R.string.login_submit).uppercase(),
style = androidx.compose.material3.MaterialTheme.typography.headlineMedium,
)
Spacer(Modifier.height(8.dp))
Text(
text = "Gestisci le dirette della tua squadra",
style = androidx.compose.material3.MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(32.dp))
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
label = { Text(stringResource(R.string.login_email)) },
placeholder = { Text("coach@squadra.it") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
@@ -114,7 +122,7 @@ fun LoginScreen(
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text("Password") },
label = { Text(stringResource(R.string.login_password)) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (passwordVisible) {
@@ -153,7 +161,7 @@ fun LoginScreen(
}
Spacer(Modifier.height(32.dp))
MatchPrimaryButton(
label = "ACCEDI",
label = stringResource(R.string.login_submit).uppercase(),
loading = loading,
enabled = email.isNotBlank() && password.isNotBlank(),
onClick = { submitLogin() },
@@ -2,24 +2,30 @@ package com.matchlivetv.match_live_tv.ui.matches
import android.app.DatePickerDialog
import android.app.TimePickerDialog
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarToday
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.filled.EventAvailable
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.PlayCircleOutline
import androidx.compose.material.icons.filled.RadioButtonUnchecked
@@ -41,10 +47,15 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
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.isScheduledFuture
import com.matchlivetv.match_live_tv.core.parseApiInstant
import com.matchlivetv.match_live_tv.domain.Match
@@ -473,26 +484,78 @@ fun TeamPickerBar(
team: Team,
showPicker: Boolean,
onClick: () -> Unit,
expandable: Boolean = true,
) {
if (!showPicker) return
val shape = RoundedCornerShape(14.dp)
Row(
Modifier
.fillMaxWidth()
.background(MatchColors.Surface, RoundedCornerShape(12.dp))
.clickable(onClick = onClick)
.padding(horizontal = 14.dp, vertical = 12.dp),
.clip(shape)
.background(MatchColors.SurfaceElevated, shape)
.border(
BorderStroke(
width = if (expandable) 1.5.dp else 1.dp,
color = if (expandable) MatchColors.PrimaryRed.copy(alpha = 0.55f) else MatchColors.Outline,
),
shape,
)
.then(if (expandable) Modifier.clickable(onClick = onClick) else Modifier)
.padding(horizontal = 14.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Default.Groups, null, tint = MatchColors.PrimaryRed)
Box(
Modifier
.size(40.dp)
.background(MatchColors.PrimaryRed.copy(alpha = 0.16f), CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
Icons.Default.Groups,
contentDescription = null,
tint = MatchColors.PrimaryRed,
modifier = Modifier.size(22.dp),
)
}
Spacer(Modifier.width(12.dp))
Column(Modifier.weight(1f)) {
Text("Squadra per la diretta", style = MaterialTheme.typography.bodyMedium)
Text(
if (expandable) {
stringResource(R.string.matches_tap_change_team)
} else {
stringResource(R.string.matches_team_for_live)
},
color = if (expandable) MatchColors.PrimaryRed else MatchColors.TextSecondary,
fontSize = 12.sp,
fontWeight = FontWeight.SemiBold,
)
Text(team.name, style = MaterialTheme.typography.titleMedium)
team.clubName?.takeIf { it.isNotBlank() }?.let {
Text(it, style = MaterialTheme.typography.bodyMedium)
}
}
Icon(Icons.Default.ChevronRight, null, tint = MatchColors.TextSecondary)
if (expandable) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.background(MatchColors.PrimaryRed.copy(alpha = 0.14f), RoundedCornerShape(999.dp))
.padding(horizontal = 10.dp, vertical = 6.dp),
) {
Text(
stringResource(R.string.matches_change),
color = MatchColors.PrimaryRed,
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
)
Icon(
Icons.Default.ExpandMore,
contentDescription = stringResource(R.string.matches_change),
tint = MatchColors.PrimaryRed,
modifier = Modifier.size(18.dp),
)
}
}
}
}
@@ -17,7 +17,9 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Logout
import androidx.compose.material.icons.filled.DeleteOutline
import androidx.compose.material.icons.filled.Language
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -41,14 +43,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.matchlivetv.match_live_tv.R
import com.matchlivetv.match_live_tv.core.isScheduledFuture
import com.matchlivetv.match_live_tv.core.parseApiInstant
import com.matchlivetv.match_live_tv.data.AppContainer
import com.matchlivetv.match_live_tv.data.repository.MatchSessionLauncher
import com.matchlivetv.match_live_tv.domain.Match
import com.matchlivetv.match_live_tv.domain.Team
import com.matchlivetv.match_live_tv.ui.components.LanguagePickerDialog
import com.matchlivetv.match_live_tv.ui.components.MatchLiveWordmark
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
import com.matchlivetv.match_live_tv.ui.components.MatchScreenScaffold
@@ -82,6 +87,7 @@ fun MatchesScreen(
var resumeMatch by remember { mutableStateOf<Match?>(null) }
var configureMatch by remember { mutableStateOf<Match?>(null) }
var deleteMatch by remember { mutableStateOf<Match?>(null) }
var showLanguagePicker by remember { mutableStateOf(false) }
suspend fun showMessage(message: String) {
snackbarHostState.showSnackbar(message)
@@ -159,18 +165,32 @@ fun MatchesScreen(
titleContentColor = Color.White,
),
actions = {
IconButton(onClick = { showLanguagePicker = true }) {
Icon(
imageVector = Icons.Filled.Language,
contentDescription = stringResource(R.string.language_label),
tint = MatchColors.TextSecondary,
)
}
IconButton(onClick = {
scope.launch {
container.authRepository.logout()
onLogout()
}
}) {
Text("Esci", color = MatchColors.TextSecondary)
Icon(
imageVector = Icons.AutoMirrored.Filled.Logout,
contentDescription = stringResource(R.string.action_logout),
tint = MatchColors.TextSecondary,
)
}
},
)
},
) {
if (showLanguagePicker) {
LanguagePickerDialog(onDismiss = { showLanguagePicker = false })
}
Box(Modifier.fillMaxSize()) {
PullToRefreshBox(
isRefreshing = refreshing,
@@ -192,7 +212,10 @@ fun MatchesScreen(
) {
Text(error!!, color = MatchColors.PrimaryRed, textAlign = TextAlign.Center)
Spacer(Modifier.height(16.dp))
MatchPrimaryButton(label = "RIPROVA", onClick = { reload(showInitialSpinner = false) })
MatchPrimaryButton(
label = stringResource(R.string.matches_retry).uppercase(),
onClick = { reload(showInitialSpinner = false) },
)
}
else -> LazyColumn(
contentPadding = PaddingValues(bottom = 24.dp),
@@ -200,23 +223,26 @@ fun MatchesScreen(
item {
Column(Modifier.padding(horizontal = 20.dp, vertical = 8.dp)) {
Text(
"Ciao, ${session?.user?.name.orEmpty()}",
stringResource(
R.string.matches_hello,
session?.user?.name.orEmpty(),
),
style = MaterialTheme.typography.headlineMedium,
)
Spacer(Modifier.height(4.dp))
Text(
"Riprendi una diretta in corso o avvia una partita programmata.",
stringResource(R.string.matches_subtitle),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(Modifier.height(16.dp))
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
MatchSecondaryButton(
label = "PARTITA PROGRAMMATA",
label = stringResource(R.string.matches_schedule).uppercase(),
onClick = { showScheduleSheet = true },
modifier = Modifier.weight(1f),
)
MatchPrimaryButton(
label = "NUOVA PARTITA",
label = stringResource(R.string.matches_new).uppercase(),
onClick = { showNewMatchSheet = true },
modifier = Modifier.weight(1f),
)
@@ -226,6 +252,7 @@ fun MatchesScreen(
TeamPickerBar(
team = team,
showPicker = true,
expandable = teams.size > 1,
onClick = { if (teams.size > 1) showTeamSheet = true },
)
}
@@ -238,9 +265,11 @@ fun MatchesScreen(
item {
Text(
when {
calendarMatches.isEmpty() && activeMatch == null -> "Nessuna partita in calendario"
scheduledMatches.isNotEmpty() -> "Partite programmate"
else -> "Pronte da avviare"
calendarMatches.isEmpty() && activeMatch == null ->
stringResource(R.string.matches_empty_title)
scheduledMatches.isNotEmpty() ->
stringResource(R.string.matches_scheduled_title)
else -> stringResource(R.string.matches_ready_title)
},
style = MaterialTheme.typography.labelLarge,
color = MatchColors.TextSecondary,
@@ -249,16 +278,25 @@ fun MatchesScreen(
}
if (calendarMatches.isEmpty() && activeMatch == null) {
item {
val emptyHint = stringResource(R.string.matches_empty_hint)
val activeTeamLine = activeTeam?.name?.let {
stringResource(R.string.matches_active_team, it)
}
val multiTeamHint = if (teams.size > 1) {
stringResource(R.string.matches_multi_team_hint)
} else {
null
}
Text(
buildString {
append("Programma una partita o avviane una nuova con «Nuova partita».")
activeTeam?.name?.let { teamName ->
append("\n\nSquadra attiva: ")
append(teamName)
append('.')
append(emptyHint)
activeTeamLine?.let {
append("\n\n")
append(it)
}
if (teams.size > 1) {
append("\nHai più squadre: verifica quella selezionata sopra.")
multiTeamHint?.let {
append("\n")
append(it)
}
},
style = MaterialTheme.typography.bodyMedium,
@@ -269,7 +307,7 @@ fun MatchesScreen(
} else if (calendarMatches.isEmpty()) {
item {
Text(
"Nessuna altra partita in calendario.",
stringResource(R.string.matches_no_other),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp, vertical = 12.dp),
@@ -437,19 +475,18 @@ private fun NoTeamContent(onRetry: () -> Unit) {
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
"Nessuna squadra assegnata",
stringResource(R.string.matches_no_team_title),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(8.dp))
Text(
"Per trasmettere devi essere responsabile della trasmissione di almeno una squadra. " +
"Chiedi al tuo club di aggiungerti come staff trasmissione.",
stringResource(R.string.matches_no_team_body),
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
)
Spacer(Modifier.height(16.dp))
MatchPrimaryButton(label = "RIPROVA", onClick = onRetry)
MatchPrimaryButton(label = stringResource(R.string.matches_retry).uppercase(), onClick = onRetry)
}
}
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Match Live TV</string>
<string name="app_slogan">Jedes Spiel, jedes Event, für eure Fans.</string>
<string name="language_label">Sprache</string>
<string name="language_system">System</string>
<string name="language_italian">Italiano</string>
<string name="language_english">English</string>
<string name="language_french">Français</string>
<string name="language_german">Deutsch</string>
<string name="language_spanish">Español</string>
<string name="action_logout">Abmelden</string>
<string name="action_login">Anmelden</string>
<string name="action_cancel">Abbrechen</string>
<string name="action_save">Speichern</string>
<string name="login_email">E-Mail</string>
<string name="login_password">Passwort</string>
<string name="login_submit">Anmelden</string>
<string name="login_error_credentials">Falsche E-Mail oder Passwort</string>
<string name="login_error_unreachable">Server nicht erreichbar. Verbindung prüfen.</string>
<string name="login_error_generic">Anmeldung fehlgeschlagen</string>
<string name="matches_title">Spiele</string>
<string name="matches_hello">Hallo, %1$s</string>
<string name="matches_subtitle">Nimm einen laufenden Livestream wieder auf oder starte ein geplantes Spiel.</string>
<string name="matches_schedule">Geplantes Spiel</string>
<string name="matches_new">Neues Spiel</string>
<string name="matches_empty_title">Keine Spiele im Kalender</string>
<string name="matches_scheduled_title">Geplante Spiele</string>
<string name="matches_ready_title">Bereit zum Start</string>
<string name="matches_empty_hint">Plane ein Spiel oder starte ein neues mit «Neues Spiel».</string>
<string name="matches_active_team">Aktives Team: %1$s.</string>
<string name="matches_multi_team_hint">Du hast mehrere Teams: prüfe die oben ausgewählte.</string>
<string name="matches_no_other">Keine weiteren Spiele im Kalender.</string>
<string name="matches_retry">Erneut versuchen</string>
<string name="matches_load_error">Ladefehler</string>
<string name="matches_no_team_title">Kein Team zugewiesen</string>
<string name="matches_no_team_body">Zum Streamen musst du Übertragungs-Staff für mindestens ein Team sein. Bitte deinen Verein, dich als Streaming-Staff hinzuzufügen.</string>
<string name="matches_tap_change_team">Tippen zum Teamwechsel</string>
<string name="matches_team_for_live">Team für den Livestream</string>
<string name="matches_change">Wechseln</string>
<string name="streaming_notification_channel">Livestream</string>
<string name="streaming_notification_channel_desc">Benachrichtigung während des Livestreams</string>
<string name="streaming_notification_title">Match Live TV</string>
<string name="streaming_notification_active">Stream wird vorbereitet…</string>
<string name="streaming_notification_connecting">RTMP-Verbindung…</string>
<string name="streaming_notification_streaming">Live</string>
<string name="streaming_notification_reconnecting">Erneute Verbindung…</string>
<string name="streaming_notification_error">Streaming-Fehler</string>
<string name="streaming_notification_stop">Beenden</string>
</resources>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Match Live TV</string>
<string name="app_slogan">Every match, every event, for your fans.</string>
<string name="language_label">Language</string>
<string name="language_system">System</string>
<string name="language_italian">Italiano</string>
<string name="language_english">English</string>
<string name="language_french">Français</string>
<string name="language_german">Deutsch</string>
<string name="language_spanish">Español</string>
<string name="action_logout">Log out</string>
<string name="action_login">Log in</string>
<string name="action_cancel">Cancel</string>
<string name="action_save">Save</string>
<string name="login_email">Email</string>
<string name="login_password">Password</string>
<string name="login_submit">Log in</string>
<string name="login_error_credentials">Incorrect email or password</string>
<string name="login_error_unreachable">Server unreachable. Check your connection.</string>
<string name="login_error_generic">Login failed</string>
<string name="matches_title">Matches</string>
<string name="matches_hello">Hi, %1$s</string>
<string name="matches_subtitle">Resume a live stream or start a scheduled match.</string>
<string name="matches_schedule">Scheduled match</string>
<string name="matches_new">New match</string>
<string name="matches_empty_title">No matches on the calendar</string>
<string name="matches_scheduled_title">Scheduled matches</string>
<string name="matches_ready_title">Ready to start</string>
<string name="matches_empty_hint">Schedule a match or start a new one with «New match».</string>
<string name="matches_active_team">Active team: %1$s.</string>
<string name="matches_multi_team_hint">You have multiple teams: check the one selected above.</string>
<string name="matches_no_other">No other matches on the calendar.</string>
<string name="matches_retry">Retry</string>
<string name="matches_load_error">Loading error</string>
<string name="matches_no_team_title">No team assigned</string>
<string name="matches_no_team_body">To stream you must be transmission staff for at least one team. Ask your club to add you as streaming staff.</string>
<string name="matches_tap_change_team">Tap to change team</string>
<string name="matches_team_for_live">Team for the live stream</string>
<string name="matches_change">Change</string>
<string name="streaming_notification_channel">Live stream</string>
<string name="streaming_notification_channel_desc">Notification while live streaming</string>
<string name="streaming_notification_title">Match Live TV</string>
<string name="streaming_notification_active">Preparing stream…</string>
<string name="streaming_notification_connecting">Connecting RTMP…</string>
<string name="streaming_notification_streaming">Live</string>
<string name="streaming_notification_reconnecting">Reconnecting…</string>
<string name="streaming_notification_error">Streaming error</string>
<string name="streaming_notification_stop">Stop</string>
</resources>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Match Live TV</string>
<string name="app_slogan">Cada partido, cada evento, para tus aficionados.</string>
<string name="language_label">Idioma</string>
<string name="language_system">Sistema</string>
<string name="language_italian">Italiano</string>
<string name="language_english">English</string>
<string name="language_french">Français</string>
<string name="language_german">Deutsch</string>
<string name="language_spanish">Español</string>
<string name="action_logout">Salir</string>
<string name="action_login">Acceder</string>
<string name="action_cancel">Cancelar</string>
<string name="action_save">Guardar</string>
<string name="login_email">Email</string>
<string name="login_password">Contraseña</string>
<string name="login_submit">Acceder</string>
<string name="login_error_credentials">Email o contraseña incorrectos</string>
<string name="login_error_unreachable">Servidor no disponible. Comprueba la conexión.</string>
<string name="login_error_generic">Error de acceso</string>
<string name="matches_title">Partidos</string>
<string name="matches_hello">Hola, %1$s</string>
<string name="matches_subtitle">Reanuda un directo en curso o inicia un partido programado.</string>
<string name="matches_schedule">Partido programado</string>
<string name="matches_new">Nuevo partido</string>
<string name="matches_empty_title">Ningún partido en el calendario</string>
<string name="matches_scheduled_title">Partidos programados</string>
<string name="matches_ready_title">Listos para empezar</string>
<string name="matches_empty_hint">Programa un partido o inicia uno nuevo con «Nuevo partido».</string>
<string name="matches_active_team">Equipo activo: %1$s.</string>
<string name="matches_multi_team_hint">Tienes varios equipos: comprueba el seleccionado arriba.</string>
<string name="matches_no_other">Ningún otro partido en el calendario.</string>
<string name="matches_retry">Reintentar</string>
<string name="matches_load_error">Error de carga</string>
<string name="matches_no_team_title">Ningún equipo asignado</string>
<string name="matches_no_team_body">Para emitir debes ser responsable de transmisión de al menos un equipo. Pide a tu club que te añada como staff de streaming.</string>
<string name="matches_tap_change_team">Toca para cambiar de equipo</string>
<string name="matches_team_for_live">Equipo para el directo</string>
<string name="matches_change">Cambiar</string>
<string name="streaming_notification_channel">Directo en curso</string>
<string name="streaming_notification_channel_desc">Notificación durante el streaming en vivo</string>
<string name="streaming_notification_title">Match Live TV</string>
<string name="streaming_notification_active">Preparando el directo…</string>
<string name="streaming_notification_connecting">Conectando RTMP…</string>
<string name="streaming_notification_streaming">En directo</string>
<string name="streaming_notification_reconnecting">Reconectando…</string>
<string name="streaming_notification_error">Error de streaming</string>
<string name="streaming_notification_stop">Terminar</string>
</resources>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Match Live TV</string>
<string name="app_slogan">Chaque match, chaque événement, pour vos fans.</string>
<string name="language_label">Langue</string>
<string name="language_system">Système</string>
<string name="language_italian">Italiano</string>
<string name="language_english">English</string>
<string name="language_french">Français</string>
<string name="language_german">Deutsch</string>
<string name="language_spanish">Español</string>
<string name="action_logout">Déconnexion</string>
<string name="action_login">Connexion</string>
<string name="action_cancel">Annuler</string>
<string name="action_save">Enregistrer</string>
<string name="login_email">E-mail</string>
<string name="login_password">Mot de passe</string>
<string name="login_submit">Connexion</string>
<string name="login_error_credentials">E-mail ou mot de passe incorrect</string>
<string name="login_error_unreachable">Serveur inaccessible. Vérifiez la connexion.</string>
<string name="login_error_generic">Échec de la connexion</string>
<string name="matches_title">Matchs</string>
<string name="matches_hello">Bonjour, %1$s</string>
<string name="matches_subtitle">Reprenez un direct en cours ou démarrez un match programmé.</string>
<string name="matches_schedule">Match programmé</string>
<string name="matches_new">Nouveau match</string>
<string name="matches_empty_title">Aucun match au calendrier</string>
<string name="matches_scheduled_title">Matchs programmés</string>
<string name="matches_ready_title">Prêts à démarrer</string>
<string name="matches_empty_hint">Programmez un match ou démarrez-en un avec « Nouveau match ».</string>
<string name="matches_active_team">Équipe active : %1$s.</string>
<string name="matches_multi_team_hint">Vous avez plusieurs équipes : vérifiez celle sélectionnée ci-dessus.</string>
<string name="matches_no_other">Aucun autre match au calendrier.</string>
<string name="matches_retry">Réessayer</string>
<string name="matches_load_error">Erreur de chargement</string>
<string name="matches_no_team_title">Aucune équipe assignée</string>
<string name="matches_no_team_body">Pour diffuser, vous devez être responsable de la transmission d\'au moins une équipe. Demandez à votre club de vous ajouter comme staff diffusion.</string>
<string name="matches_tap_change_team">Appuyez pour changer d\'équipe</string>
<string name="matches_team_for_live">Équipe pour le direct</string>
<string name="matches_change">Changer</string>
<string name="streaming_notification_channel">Direct en cours</string>
<string name="streaming_notification_channel_desc">Notification pendant le streaming live</string>
<string name="streaming_notification_title">Match Live TV</string>
<string name="streaming_notification_active">Préparation du direct…</string>
<string name="streaming_notification_connecting">Connexion RTMP…</string>
<string name="streaming_notification_streaming">En direct</string>
<string name="streaming_notification_reconnecting">Reconnexion…</string>
<string name="streaming_notification_error">Erreur de streaming</string>
<string name="streaming_notification_stop">Arrêter</string>
</resources>
@@ -2,6 +2,42 @@
<resources>
<string name="app_name">Match Live TV</string>
<string name="app_slogan">Ogni partita, ogni evento, per i tuoi tifosi.</string>
<string name="language_label">Lingua</string>
<string name="language_system">Sistema</string>
<string name="language_italian">Italiano</string>
<string name="language_english">English</string>
<string name="language_french">Français</string>
<string name="language_german">Deutsch</string>
<string name="language_spanish">Español</string>
<string name="action_logout">Esci</string>
<string name="action_login">Accedi</string>
<string name="action_cancel">Annulla</string>
<string name="action_save">Salva</string>
<string name="login_email">Email</string>
<string name="login_password">Password</string>
<string name="login_submit">Accedi</string>
<string name="login_error_credentials">Email o password non corretti</string>
<string name="login_error_unreachable">Server non raggiungibile. Verifica la connessione.</string>
<string name="login_error_generic">Login fallito</string>
<string name="matches_title">Partite</string>
<string name="matches_hello">Ciao, %1$s</string>
<string name="matches_subtitle">Riprendi una diretta in corso o avvia una partita programmata.</string>
<string name="matches_schedule">Partita programmata</string>
<string name="matches_new">Nuova partita</string>
<string name="matches_empty_title">Nessuna partita in calendario</string>
<string name="matches_scheduled_title">Partite programmate</string>
<string name="matches_ready_title">Pronte da avviare</string>
<string name="matches_empty_hint">Programma una partita o avviane una nuova con «Nuova partita».</string>
<string name="matches_active_team">Squadra attiva: %1$s.</string>
<string name="matches_multi_team_hint">Hai più squadre: verifica quella selezionata sopra.</string>
<string name="matches_no_other">Nessuna altra partita in calendario.</string>
<string name="matches_retry">Riprova</string>
<string name="matches_load_error">Errore caricamento</string>
<string name="matches_no_team_title">Nessuna squadra assegnata</string>
<string name="matches_no_team_body">Per trasmettere devi essere responsabile della trasmissione di almeno una squadra. Chiedi al tuo club di aggiungerti come staff trasmissione.</string>
<string name="matches_tap_change_team">Tocca per cambiare squadra</string>
<string name="matches_team_for_live">Squadra per la diretta</string>
<string name="matches_change">Cambia</string>
<string name="streaming_notification_channel">Diretta in corso</string>
<string name="streaming_notification_channel_desc">Notifica durante lo streaming live</string>
<string name="streaming_notification_title">Match Live TV</string>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<locale android:name="it" />
<locale android:name="en" />
<locale android:name="fr" />
<locale android:name="de" />
<locale android:name="es" />
</locale-config>
@@ -0,0 +1,33 @@
package com.matchlivetv.match_live_tv.core
import org.junit.Assert.assertEquals
import org.junit.Test
class RtmpIngestUrlTest {
@Test
fun rewritesLoopbackToApiHost() {
val resolved = RtmpIngestUrl.resolve(
"rtmp://127.0.0.1:1935/live/match_abc",
apiBaseUrl = "http://10.0.2.2:3000",
)
assertEquals("rtmp://10.0.2.2:1935/live/match_abc", resolved)
}
@Test
fun rewritesDockerInternalHost() {
val resolved = RtmpIngestUrl.resolve(
"rtmp://mediamtx:1935/live/match_abc",
apiBaseUrl = "http://10.0.2.2:3000",
)
assertEquals("rtmp://10.0.2.2:1935/live/match_abc", resolved)
}
@Test
fun keepsProductionHost() {
val input = "rtmp://stream.matchlivetv.it:1935/live/match_abc"
assertEquals(
input,
RtmpIngestUrl.resolve(input, apiBaseUrl = "https://www.matchlivetv.it"),
)
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id("com.android.application") version "8.7.3" apply false
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}