Aggiunge copertina sponsor custom solo Premium Full, con override in wizard e slate sui nodi di streaming.
Permette upload da portale e app con ereditarietà partita→squadra→società, generazione MP4 via ffmpeg e distribuzione su home lab e nodi CPX per pause e assenza segnale. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -85,6 +85,7 @@ data class TeamDto(
|
||||
@Json(name = "phone_download_enabled") val phoneDownloadEnabled: Boolean? = false,
|
||||
@Json(name = "billing_url") val billingUrl: String? = null,
|
||||
@Json(name = "staff_manage_url") val staffManageUrl: String? = null,
|
||||
@Json(name = "custom_cover_enabled") val customCoverEnabled: Boolean? = false,
|
||||
@Json(name = "logo_url") val logoUrl: String? = null,
|
||||
@Json(name = "primary_color") val primaryColor: String? = null,
|
||||
@Json(name = "secondary_color") val secondaryColor: String? = null,
|
||||
@@ -111,6 +112,7 @@ data class TeamDto(
|
||||
phoneDownloadEnabled = phoneDownloadEnabled ?: false,
|
||||
billingUrl = billingUrl,
|
||||
staffManageUrl = staffManageUrl,
|
||||
customCoverEnabled = customCoverEnabled ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -140,6 +142,9 @@ data class MatchDto(
|
||||
@Json(name = "home_logo_url") val homeLogoUrl: String? = null,
|
||||
@Json(name = "opponent_primary_color") val opponentPrimaryColor: String? = null,
|
||||
@Json(name = "opponent_logo_url") val opponentLogoUrl: String? = null,
|
||||
@Json(name = "effective_cover_url") val effectiveCoverUrl: String? = null,
|
||||
@Json(name = "cover_source") val coverSource: String? = null,
|
||||
@Json(name = "custom_cover_enabled") val customCoverEnabled: Boolean? = null,
|
||||
) {
|
||||
fun toDomain() = Match(
|
||||
id = id,
|
||||
@@ -165,6 +170,9 @@ data class MatchDto(
|
||||
homeLogoUrl = homeLogoUrl,
|
||||
opponentPrimaryColor = opponentPrimaryColor,
|
||||
opponentLogoUrl = opponentLogoUrl,
|
||||
effectiveCoverUrl = effectiveCoverUrl,
|
||||
coverSource = coverSource ?: "default",
|
||||
customCoverEnabled = customCoverEnabled ?: false,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -91,6 +91,8 @@ interface MatchLiveApi {
|
||||
@Part("match[scoring_rules][points_deciding_set]") pointsDecidingSet: okhttp3.RequestBody?,
|
||||
@Part("match[scoring_rules][min_point_lead]") minPointLead: okhttp3.RequestBody?,
|
||||
@Part opponentLogoFile: MultipartBody.Part?,
|
||||
@Part coverImage: MultipartBody.Part?,
|
||||
@Part("remove_cover") removeCover: okhttp3.RequestBody?,
|
||||
): MatchDto
|
||||
|
||||
@DELETE("matches/{matchId}")
|
||||
|
||||
+10
-2
@@ -122,6 +122,8 @@ class MatchRepository(
|
||||
sportKey: String? = null,
|
||||
overlayKind: String? = null,
|
||||
opponentLogoUri: Uri? = null,
|
||||
coverImageUri: Uri? = null,
|
||||
removeCover: Boolean = false,
|
||||
): Match {
|
||||
val body = UpdateMatchBodyFull(
|
||||
opponentName = opponentName,
|
||||
@@ -134,7 +136,7 @@ class MatchRepository(
|
||||
opponentPrimaryColor = opponentPrimaryColor,
|
||||
scoringRules = scoringRules?.toMap().takeIf { !it.isNullOrEmpty() },
|
||||
)
|
||||
if (opponentLogoUri != null) {
|
||||
if (opponentLogoUri != null || coverImageUri != null || removeCover) {
|
||||
return api.updateMatchMultipart(
|
||||
matchId = matchId,
|
||||
opponentName = MultipartBodies.textPart(opponentName),
|
||||
@@ -146,7 +148,13 @@ class MatchRepository(
|
||||
pointsPerSet = scoringRules?.pointsPerSet?.let { MultipartBodies.textPart(it.toString()) },
|
||||
pointsDecidingSet = scoringRules?.pointsDecidingSet?.let { MultipartBodies.textPart(it.toString()) },
|
||||
minPointLead = scoringRules?.minPointLead?.let { MultipartBodies.textPart(it.toString()) },
|
||||
opponentLogoFile = MultipartBodies.logoPart(appContext, opponentLogoUri, "opponent_logo_file"),
|
||||
opponentLogoFile = opponentLogoUri?.let {
|
||||
MultipartBodies.logoPart(appContext, it, "opponent_logo_file")
|
||||
},
|
||||
coverImage = coverImageUri?.let {
|
||||
MultipartBodies.logoPart(appContext, it, "cover_image")
|
||||
},
|
||||
removeCover = if (removeCover) MultipartBodies.textPart("1") else null,
|
||||
).toDomain()
|
||||
}
|
||||
return api.updateMatch(
|
||||
|
||||
@@ -43,6 +43,7 @@ data class Team(
|
||||
val phoneDownloadEnabled: Boolean = false,
|
||||
val billingUrl: String? = null,
|
||||
val staffManageUrl: String? = null,
|
||||
val customCoverEnabled: Boolean = false,
|
||||
) {
|
||||
val canUseYoutube: Boolean get() = youtubeEnabled
|
||||
val isYoutubeReady: Boolean get() = youtubeSelectable
|
||||
@@ -100,6 +101,9 @@ data class Match(
|
||||
val homeLogoUrl: String? = null,
|
||||
val opponentPrimaryColor: String? = null,
|
||||
val opponentLogoUrl: String? = null,
|
||||
val effectiveCoverUrl: String? = null,
|
||||
val coverSource: String = "default",
|
||||
val customCoverEnabled: Boolean = false,
|
||||
) {
|
||||
val hasActiveSession: Boolean get() = activeSessionId != null
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package com.matchlivetv.match_live_tv.ui.wizard
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.matchlivetv.match_live_tv.R
|
||||
import com.matchlivetv.match_live_tv.core.resolveMediaUrl
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchPrimaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.components.MatchSecondaryButton
|
||||
import com.matchlivetv.match_live_tv.ui.theme.MatchColors
|
||||
|
||||
@Composable
|
||||
fun coverSourceLabel(source: String): String = when (source) {
|
||||
"match" -> stringResource(R.string.wizard_match_cover_source_match)
|
||||
"team" -> stringResource(R.string.wizard_match_cover_source_team)
|
||||
"club" -> stringResource(R.string.wizard_match_cover_source_club)
|
||||
else -> stringResource(R.string.wizard_match_cover_source_default)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatchCoverSection(
|
||||
effectiveCoverUrl: String?,
|
||||
coverSource: String,
|
||||
customCoverEnabled: Boolean,
|
||||
localCoverUri: Uri?,
|
||||
hasMatchOverride: Boolean,
|
||||
onPickCover: () -> Unit,
|
||||
onClearOverride: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val previewModel = localCoverUri ?: resolveMediaUrl(effectiveCoverUrl)
|
||||
|
||||
Column(modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
stringResource(R.string.wizard_match_cover_title),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MatchColors.TextSecondary,
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.wizard_match_cover_source_label, coverSourceLabel(coverSource)),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MatchColors.TextSecondary,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp)
|
||||
.aspectRatio(16f / 9f)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(MatchColors.Surface),
|
||||
) {
|
||||
if (previewModel != null) {
|
||||
AsyncImage(
|
||||
model = previewModel,
|
||||
contentDescription = stringResource(R.string.wizard_match_cover_preview_cd),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
stringResource(R.string.wizard_match_cover_hint),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MatchColors.TextSecondary,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
if (customCoverEnabled) {
|
||||
MatchPrimaryButton(
|
||||
label = stringResource(R.string.wizard_match_cover_change),
|
||||
onClick = onPickCover,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 12.dp),
|
||||
)
|
||||
if (hasMatchOverride || localCoverUri != null) {
|
||||
MatchSecondaryButton(
|
||||
label = stringResource(R.string.wizard_match_cover_reset),
|
||||
onClick = onClearOverride,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
stringResource(R.string.wizard_match_cover_premium_required),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MatchColors.TextSecondary,
|
||||
modifier = Modifier.padding(top = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -105,6 +105,11 @@ fun StepMatchScreen(
|
||||
}
|
||||
var homeLogoUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var opponentLogoUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var coverImageUri by remember { mutableStateOf<Uri?>(null) }
|
||||
var removeCover by remember { mutableStateOf(false) }
|
||||
var effectiveCoverUrl by remember { mutableStateOf(match.effectiveCoverUrl) }
|
||||
var coverSource by remember { mutableStateOf(match.coverSource) }
|
||||
var customCoverEnabled by remember { mutableStateOf(match.customCoverEnabled) }
|
||||
|
||||
LaunchedEffect(match.teamId) {
|
||||
runCatching { container.matchRepository.fetchTeam(match.teamId) }
|
||||
@@ -113,10 +118,20 @@ fun StepMatchScreen(
|
||||
if (team != null) {
|
||||
homePrimaryColor = normalizeHexColor(team.primaryColor ?: match.homePrimaryColor)
|
||||
homeLogoUrl = team.logoUrl ?: match.homeLogoUrl
|
||||
if (!match.customCoverEnabled) {
|
||||
customCoverEnabled = team.customCoverEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val pickCover = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
) { uri ->
|
||||
coverImageUri = uri
|
||||
removeCover = false
|
||||
}
|
||||
|
||||
val pickHomeLogo = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.PickVisualMedia(),
|
||||
) { uri -> homeLogoUri = uri }
|
||||
@@ -230,6 +245,24 @@ fun StepMatchScreen(
|
||||
onClearLogo = { opponentLogoUri = null },
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
MatchCoverSection(
|
||||
effectiveCoverUrl = effectiveCoverUrl,
|
||||
coverSource = when {
|
||||
coverImageUri != null -> "match"
|
||||
else -> coverSource
|
||||
},
|
||||
customCoverEnabled = customCoverEnabled,
|
||||
localCoverUri = coverImageUri,
|
||||
hasMatchOverride = match.coverSource == "match",
|
||||
onPickCover = {
|
||||
pickCover.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))
|
||||
},
|
||||
onClearOverride = {
|
||||
coverImageUri = null
|
||||
removeCover = true
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
MatchOutlinedField(
|
||||
value = location,
|
||||
onValueChange = { location = it },
|
||||
@@ -500,9 +533,13 @@ fun StepMatchScreen(
|
||||
""
|
||||
},
|
||||
opponentLogoUri = opponentLogoUri,
|
||||
coverImageUri = coverImageUri,
|
||||
removeCover = removeCover,
|
||||
)
|
||||
}.onSuccess { updated ->
|
||||
container.wizardSession.match = updated
|
||||
effectiveCoverUrl = updated.effectiveCoverUrl
|
||||
coverSource = updated.coverSource
|
||||
onNext()
|
||||
}.onFailure {
|
||||
onError(it.message ?: saveGenericError)
|
||||
|
||||
@@ -64,6 +64,17 @@
|
||||
<string name="wizard_match_location_label">Ort</string>
|
||||
<string name="wizard_match_category_label">Liga (optional)</string>
|
||||
<string name="wizard_match_category_hint">Z. B. Serie C, Sommerturnier — wird in Beschreibung und Overlay verwendet.</string>
|
||||
<string name="wizard_match_cover_title">Pausenbild</string>
|
||||
<string name="wizard_match_cover_source_label">Quelle: %1$s</string>
|
||||
<string name="wizard_match_cover_source_default">Match Live TV</string>
|
||||
<string name="wizard_match_cover_source_club">Verein</string>
|
||||
<string name="wizard_match_cover_source_team">Team</string>
|
||||
<string name="wizard_match_cover_source_match">Dieses Spiel</string>
|
||||
<string name="wizard_match_cover_hint">Wird bei Pause oder ohne Signal angezeigt, bis der Stream weitergeht.</string>
|
||||
<string name="wizard_match_cover_change">Bild für dieses Spiel ändern</string>
|
||||
<string name="wizard_match_cover_reset">Standardbild verwenden</string>
|
||||
<string name="wizard_match_cover_premium_required">Individuelles Sponsor-Bild mit Premium Full</string>
|
||||
<string name="wizard_match_cover_preview_cd">Bildvorschau</string>
|
||||
<string name="wizard_match_scheduled_label">Geplant für</string>
|
||||
<string name="wizard_match_custom_overlay_label">Individuelles Video-Overlay</string>
|
||||
<string name="wizard_match_custom_rules_label">Individuelle Punkteregeln</string>
|
||||
|
||||
@@ -64,6 +64,17 @@
|
||||
<string name="wizard_match_location_label">Location</string>
|
||||
<string name="wizard_match_category_label">Category (optional)</string>
|
||||
<string name="wizard_match_category_hint">E.g. Serie C, summer tournament — we\'ll use it in the description and overlay.</string>
|
||||
<string name="wizard_match_cover_title">Pause cover</string>
|
||||
<string name="wizard_match_cover_source_label">Source: %1$s</string>
|
||||
<string name="wizard_match_cover_source_default">Match Live TV</string>
|
||||
<string name="wizard_match_cover_source_club">Club</string>
|
||||
<string name="wizard_match_cover_source_team">Team</string>
|
||||
<string name="wizard_match_cover_source_match">This match</string>
|
||||
<string name="wizard_match_cover_hint">Shown while paused or offline until the stream resumes.</string>
|
||||
<string name="wizard_match_cover_change">Change cover for this match</string>
|
||||
<string name="wizard_match_cover_reset">Use default cover</string>
|
||||
<string name="wizard_match_cover_premium_required">Custom sponsor cover requires Premium Full</string>
|
||||
<string name="wizard_match_cover_preview_cd">Cover preview</string>
|
||||
<string name="wizard_match_scheduled_label">Scheduled for</string>
|
||||
<string name="wizard_match_custom_overlay_label">Custom video overlay</string>
|
||||
<string name="wizard_match_custom_rules_label">Custom scoring rules</string>
|
||||
|
||||
@@ -64,6 +64,17 @@
|
||||
<string name="wizard_match_location_label">Lugar</string>
|
||||
<string name="wizard_match_category_label">Categoría (opcional)</string>
|
||||
<string name="wizard_match_category_hint">Ej. Serie C, torneo de verano — lo usaremos en la descripción y el overlay.</string>
|
||||
<string name="wizard_match_cover_title">Imagen de pausa</string>
|
||||
<string name="wizard_match_cover_source_label">Origen: %1$s</string>
|
||||
<string name="wizard_match_cover_source_default">Match Live TV</string>
|
||||
<string name="wizard_match_cover_source_club">Club</string>
|
||||
<string name="wizard_match_cover_source_team">Equipo</string>
|
||||
<string name="wizard_match_cover_source_match">Este partido</string>
|
||||
<string name="wizard_match_cover_hint">Se muestra en pausa o sin señal hasta que retome la transmisión.</string>
|
||||
<string name="wizard_match_cover_change">Cambiar imagen para este partido</string>
|
||||
<string name="wizard_match_cover_reset">Usar imagen predeterminada</string>
|
||||
<string name="wizard_match_cover_premium_required">Imagen de patrocinio personalizada con Premium Full</string>
|
||||
<string name="wizard_match_cover_preview_cd">Vista previa de la imagen</string>
|
||||
<string name="wizard_match_scheduled_label">Programado para</string>
|
||||
<string name="wizard_match_custom_overlay_label">Overlay de vídeo personalizado</string>
|
||||
<string name="wizard_match_custom_rules_label">Reglas de puntuación personalizadas</string>
|
||||
|
||||
@@ -64,6 +64,17 @@
|
||||
<string name="wizard_match_location_label">Lieu</string>
|
||||
<string name="wizard_match_category_label">Championnat (facultatif)</string>
|
||||
<string name="wizard_match_category_hint">Ex. Serie C, tournoi d\'été — utilisé dans la description et l\'overlay.</string>
|
||||
<string name="wizard_match_cover_title">Image de pause</string>
|
||||
<string name="wizard_match_cover_source_label">Source : %1$s</string>
|
||||
<string name="wizard_match_cover_source_default">Match Live TV</string>
|
||||
<string name="wizard_match_cover_source_club">Club</string>
|
||||
<string name="wizard_match_cover_source_team">Équipe</string>
|
||||
<string name="wizard_match_cover_source_match">Ce match</string>
|
||||
<string name="wizard_match_cover_hint">Affichée en pause ou sans signal jusqu\'à la reprise du direct.</string>
|
||||
<string name="wizard_match_cover_change">Changer l\'image pour ce match</string>
|
||||
<string name="wizard_match_cover_reset">Utiliser l\'image par défaut</string>
|
||||
<string name="wizard_match_cover_premium_required">Image sponsor personnalisée avec Premium Full</string>
|
||||
<string name="wizard_match_cover_preview_cd">Aperçu de l\'image</string>
|
||||
<string name="wizard_match_scheduled_label">Programmé pour</string>
|
||||
<string name="wizard_match_custom_overlay_label">Overlay vidéo personnalisé</string>
|
||||
<string name="wizard_match_custom_rules_label">Règles de score personnalisées</string>
|
||||
|
||||
@@ -65,6 +65,17 @@
|
||||
<string name="wizard_match_location_label">Luogo</string>
|
||||
<string name="wizard_match_category_label">Campionato (facoltativo)</string>
|
||||
<string name="wizard_match_category_hint">Es. Serie C, torneo estivo — lo useremo in descrizione e overlay.</string>
|
||||
<string name="wizard_match_cover_title">Copertina in pausa</string>
|
||||
<string name="wizard_match_cover_source_label">Origine: %1$s</string>
|
||||
<string name="wizard_match_cover_source_default">Match Live TV</string>
|
||||
<string name="wizard_match_cover_source_club">Società</string>
|
||||
<string name="wizard_match_cover_source_team">Squadra</string>
|
||||
<string name="wizard_match_cover_source_match">Questa partita</string>
|
||||
<string name="wizard_match_cover_hint">Mostrata in pausa o senza segnale finché non riparte la diretta.</string>
|
||||
<string name="wizard_match_cover_change">Cambia copertina per questa partita</string>
|
||||
<string name="wizard_match_cover_reset">Usa quella predefinita</string>
|
||||
<string name="wizard_match_cover_premium_required">Copertina sponsor personalizzata con Premium Full</string>
|
||||
<string name="wizard_match_cover_preview_cd">Anteprima copertina</string>
|
||||
<string name="wizard_match_scheduled_label">Programmata per</string>
|
||||
<string name="wizard_match_custom_overlay_label">Overlay video personalizzato</string>
|
||||
<string name="wizard_match_custom_rules_label">Regole punteggio personalizzate</string>
|
||||
|
||||
Reference in New Issue
Block a user