From 7c28251b02b9b96ac92e22e458632bae2515f634 Mon Sep 17 00:00:00 2001 From: timklge <2026103+timklge@users.noreply.github.com> Date: Sun, 2 Mar 2025 23:56:34 +0100 Subject: [PATCH] Enlarge font in single date weather display (#55) * Shorter date format * Enlarge font size in single date weather widget and make clickable * Fix off by one in route forecast * Fix indentation lint --- .../karooheadwind/KarooHeadwindExtension.kt | 5 +- .../datatypes/WeatherDataType.kt | 12 +- .../datatypes/WeatherForecastDataType.kt | 10 +- .../karooheadwind/datatypes/WeatherView.kt | 19 +- .../karooheadwind/screens/WeatherScreen.kt | 26 +-- .../karooheadwind/screens/WeatherWidget.kt | 218 +++++++++--------- app/src/main/res/drawable/droplet_regular.png | Bin 0 -> 7844 bytes 7 files changed, 150 insertions(+), 140 deletions(-) create mode 100644 app/src/main/res/drawable/droplet_regular.png diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt index fbd8cbb..7f8b202 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/KarooHeadwindExtension.kt @@ -163,7 +163,8 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") { add(gps) var currentPosition = positionOnRoute + calculatedDistanceToNextFullHour - var lastRequestedPosition = currentPosition + var lastRequestedPosition = positionOnRoute + while (currentPosition < upcomingRoute.routeLength && size < 10) { val point = TurfMeasurement.along( upcomingRoute.routePolyline, @@ -182,7 +183,7 @@ class KarooHeadwindExtension : KarooExtension("karoo-headwind", "1.3") { currentPosition += distancePerHour } - if (upcomingRoute.routeLength > lastRequestedPosition + 5_000) { + if (upcomingRoute.routeLength > lastRequestedPosition + 1_000) { val point = TurfMeasurement.along( upcomingRoute.routePolyline, upcomingRoute.routeLength, diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt index f85dbdc..ac7b5f8 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherDataType.kt @@ -5,6 +5,8 @@ import android.graphics.BitmapFactory import android.util.Log import androidx.compose.ui.unit.DpSize import androidx.glance.GlanceModifier +import androidx.glance.action.actionStartActivity +import androidx.glance.action.clickable import androidx.glance.appwidget.ExperimentalGlanceRemoteViewsApi import androidx.glance.appwidget.GlanceRemoteViews import androidx.glance.layout.Alignment @@ -13,6 +15,7 @@ import androidx.glance.layout.fillMaxSize import de.timklge.karooheadwind.HeadingResponse import de.timklge.karooheadwind.HeadwindSettings import de.timklge.karooheadwind.KarooHeadwindExtension +import de.timklge.karooheadwind.MainActivity import de.timklge.karooheadwind.OpenMeteoCurrentWeatherResponse import de.timklge.karooheadwind.OpenMeteoData import de.timklge.karooheadwind.TemperatureUnit @@ -43,7 +46,6 @@ import kotlinx.coroutines.launch import java.time.Instant import java.time.ZoneId import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import kotlin.math.roundToInt @OptIn(ExperimentalGlanceRemoteViewsApi::class) @@ -124,11 +126,13 @@ class WeatherDataType( val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode) val formattedTime = timeFormatter.format(Instant.ofEpochSecond(data.current.time)) - val formattedDate = Instant.ofEpochSecond(data.current.time).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate( - FormatStyle.SHORT)) + val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(data.current.time)) val result = glance.compose(context, DpSize.Unspecified) { - Box(modifier = GlanceModifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + var modifier = GlanceModifier.fillMaxSize() + if (!config.preview) modifier = modifier.clickable(onClick = actionStartActivity()) + + Box(modifier = modifier, contentAlignment = Alignment.CenterEnd) { Weather( baseBitmap, current = interpretation, diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt index d31d1f8..918aee7 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherForecastDataType.kt @@ -165,7 +165,7 @@ class WeatherForecastDataType( var previousDate: String? = let { val unixTime = allData.getOrNull(positionOffset)?.data?.forecastData?.time?.getOrNull(hourOffset) - val formattedDate = unixTime?.let { Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) } + val formattedDate = unixTime?.let { getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) } formattedDate } @@ -182,8 +182,6 @@ class WeatherForecastDataType( val distanceAlongRoute = allData.getOrNull(positionIndex)?.requestedPosition?.distanceAlongRoute val position = allData.getOrNull(positionIndex)?.requestedPosition?.let { "${(it.distanceAlongRoute?.div(1000.0))?.toInt()} at ${it.lat}, ${it.lon}" } - Log.d(KarooHeadwindExtension.TAG, "Distance along route ${positionIndex}: $position") - if (baseIndex > hourOffset) { Spacer( modifier = GlanceModifier.fillMaxHeight().background( @@ -192,6 +190,8 @@ class WeatherForecastDataType( ) } + Log.d(KarooHeadwindExtension.TAG, "Distance along route ${positionIndex}: $position") + val distanceFromCurrent = upcomingRoute?.distanceAlongRoute?.let { currentDistanceAlongRoute -> distanceAlongRoute?.minus(currentDistanceAlongRoute) } @@ -202,7 +202,7 @@ class WeatherForecastDataType( val interpretation = WeatherInterpretation.fromWeatherCode(data.current.weatherCode) val unixTime = data.current.time val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime)) - val formattedDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) val hasNewDate = formattedDate != previousDate || baseIndex == 0 Weather( @@ -225,7 +225,7 @@ class WeatherForecastDataType( val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(baseIndex) ?: 0) val unixTime = data?.forecastData?.time?.get(baseIndex) ?: 0 val formattedTime = timeFormatter.format(Instant.ofEpochSecond(unixTime)) - val formattedDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + val formattedDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) val hasNewDate = formattedDate != previousDate || baseIndex == 0 Weather( diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt index 95e3435..5e6226e 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/datatypes/WeatherView.kt @@ -24,7 +24,6 @@ import androidx.glance.layout.padding import androidx.glance.layout.width import androidx.glance.layout.wrapContentWidth import androidx.glance.preview.ExperimentalGlancePreviewApi -import androidx.glance.preview.Preview import androidx.glance.text.FontFamily import androidx.glance.text.FontWeight import androidx.glance.text.Text @@ -33,9 +32,19 @@ import androidx.glance.text.TextStyle import de.timklge.karooheadwind.R import de.timklge.karooheadwind.TemperatureUnit import de.timklge.karooheadwind.WeatherInterpretation +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.util.Locale import kotlin.math.absoluteValue import kotlin.math.ceil +fun getShortDateFormatter(): DateTimeFormatter = DateTimeFormatter.ofPattern( + when (Locale.getDefault().country) { + "US" -> "MM/dd" + else -> "dd.MM" + } +).withZone(ZoneId.systemDefault()) + fun getWeatherIcon(interpretation: WeatherInterpretation): Int { return when (interpretation){ WeatherInterpretation.CLEAR -> R.drawable.bx_clear @@ -49,7 +58,6 @@ fun getWeatherIcon(interpretation: WeatherInterpretation): Int { } @OptIn(ExperimentalGlancePreviewApi::class) -@Preview(widthDp = 200, heightDp = 150) @Composable fun Weather( baseBitmap: Bitmap, @@ -69,7 +77,7 @@ fun Weather( isImperial: Boolean? ) { - val fontSize = 14f + val fontSize = if (singleDisplay) 19f else 14f Column(modifier = if (singleDisplay) GlanceModifier.fillMaxSize().padding(1.dp) else GlanceModifier.fillMaxHeight().padding(1.dp).width(86.dp), horizontalAlignment = rowAlignment) { Row(modifier = GlanceModifier.defaultWeight().wrapContentWidth(), horizontalAlignment = rowAlignment, verticalAlignment = Alignment.CenterVertically) { @@ -130,7 +138,7 @@ fun Weather( } Image( - modifier = GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), + modifier = if (singleDisplay) GlanceModifier.height(20.dp).width(16.dp) else GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), provider = ImageProvider(R.drawable.thermometer), contentDescription = "Temperature", contentScale = ContentScale.Fit, @@ -163,14 +171,13 @@ fun Weather( Spacer(modifier = GlanceModifier.width(5.dp)) Image( - modifier = GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), + modifier = if (singleDisplay) GlanceModifier.height(20.dp).width(16.dp) else GlanceModifier.height(16.dp).width(12.dp).padding(1.dp), provider = ImageProvider(getArrowBitmapByBearing(baseBitmap, windBearing + 180)), contentDescription = "Current wind direction", contentScale = ContentScale.Fit, colorFilter = ColorFilter.tint(ColorProvider(Color.Black, Color.White)) ) - Text( text = "$windSpeed,${windGusts}", style = TextStyle(color = ColorProvider(Color.Black, Color.White), fontFamily = FontFamily.Monospace, fontSize = TextUnit(fontSize, TextUnitType.Sp)) diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt index 207fbfc..88ed319 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherScreen.kt @@ -26,12 +26,12 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import de.timklge.karooheadwind.HeadwindStats import de.timklge.karooheadwind.KarooHeadwindExtension -import de.timklge.karooheadwind.PrecipitationUnit import de.timklge.karooheadwind.R import de.timklge.karooheadwind.TemperatureUnit import de.timklge.karooheadwind.WeatherInterpretation import de.timklge.karooheadwind.datatypes.WeatherDataType.Companion.timeFormatter import de.timklge.karooheadwind.datatypes.WeatherForecastDataType +import de.timklge.karooheadwind.datatypes.getShortDateFormatter import de.timklge.karooheadwind.getGpsCoordinateFlow import de.timklge.karooheadwind.streamCurrentWeatherData import de.timklge.karooheadwind.streamStats @@ -41,10 +41,7 @@ import io.hammerhead.karooext.KarooSystemService import io.hammerhead.karooext.models.UserProfile import java.time.Instant import java.time.LocalDateTime -import java.time.ZoneId import java.time.ZoneOffset -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.time.temporal.ChronoUnit import kotlin.math.roundToInt @@ -91,15 +88,10 @@ fun WeatherScreen(onFinish: () -> Unit) { val requestedWeatherPosition = weatherData.firstOrNull()?.requestedPosition val formattedTime = currentWeatherData?.let { timeFormatter.format(Instant.ofEpochSecond(currentWeatherData.current.time)) } - val formattedDate = currentWeatherData?.let { Instant.ofEpochSecond(currentWeatherData.current.time).atZone(ZoneId.systemDefault()).toLocalDate().format( - DateTimeFormatter.ofLocalizedDate( - FormatStyle.SHORT)) - } + val formattedDate = currentWeatherData?.let { getShortDateFormatter().format(Instant.ofEpochSecond(currentWeatherData.current.time)) } if (karooConnected == true && currentWeatherData != null) { WeatherWidget( - dateLabel = formattedDate, - timeLabel = formattedTime, baseBitmap = baseBitmap, current = WeatherInterpretation.fromWeatherCode(currentWeatherData.current.weatherCode), windBearing = currentWeatherData.current.windDirection.roundToInt(), @@ -108,10 +100,11 @@ fun WeatherScreen(onFinish: () -> Unit) { precipitation = currentWeatherData.current.precipitation, temperature = currentWeatherData.current.temperature.toInt(), temperatureUnit = if(profile?.preferredUnit?.temperature == UserProfile.PreferredUnit.UnitType.METRIC) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT, - isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, - precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH, + timeLabel = formattedTime, + dateLabel = formattedDate, distance = requestedWeatherPosition?.let { l -> location?.distanceTo(l)?.times(1000) }, includeDistanceLabel = false, + isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, ) } @@ -208,7 +201,7 @@ fun WeatherScreen(onFinish: () -> Unit) { val interpretation = WeatherInterpretation.fromWeatherCode(data?.forecastData?.weatherCode?.get(index) ?: 0) val unixTime = data?.forecastData?.time?.get(index) ?: 0 val formattedForecastTime = WeatherForecastDataType.timeFormatter.format(Instant.ofEpochSecond(unixTime)) - val formattedForecastDate = Instant.ofEpochSecond(unixTime).atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ofLocalizedDate(FormatStyle.SHORT)) + val formattedForecastDate = getShortDateFormatter().format(Instant.ofEpochSecond(unixTime)) WeatherWidget( baseBitmap, @@ -217,15 +210,14 @@ fun WeatherScreen(onFinish: () -> Unit) { windSpeed = data?.forecastData?.windSpeed?.get(index)?.roundToInt() ?: 0, windGusts = data?.forecastData?.windGusts?.get(index)?.roundToInt() ?: 0, precipitation = data?.forecastData?.precipitation?.get(index) ?: 0.0, - precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0, temperature = data?.forecastData?.temperature?.get(index)?.roundToInt() ?: 0, temperatureUnit = if (profile?.preferredUnit?.temperature != UserProfile.PreferredUnit.UnitType.IMPERIAL) TemperatureUnit.CELSIUS else TemperatureUnit.FAHRENHEIT, timeLabel = formattedForecastTime, dateLabel = formattedForecastDate, distance = distanceFromCurrent, - isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL, - precipitationUnit = if (profile?.preferredUnit?.distance != UserProfile.PreferredUnit.UnitType.IMPERIAL) PrecipitationUnit.MILLIMETERS else PrecipitationUnit.INCH, - includeDistanceLabel = true + includeDistanceLabel = true, + precipitationProbability = data?.forecastData?.precipitationProbability?.get(index) ?: 0, + isImperial = profile?.preferredUnit?.distance == UserProfile.PreferredUnit.UnitType.IMPERIAL ) } diff --git a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt index 47f9b7b..ba3f5b6 100644 --- a/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt +++ b/app/src/main/kotlin/de/timklge/karooheadwind/screens/WeatherWidget.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import de.timklge.karooheadwind.PrecipitationUnit import de.timklge.karooheadwind.R import de.timklge.karooheadwind.TemperatureUnit import de.timklge.karooheadwind.WeatherInterpretation @@ -47,122 +46,129 @@ fun WeatherWidget( distance: Double? = null, includeDistanceLabel: Boolean = false, precipitationProbability: Int? = null, - precipitationUnit: PrecipitationUnit, isImperial: Boolean ) { - Row( - modifier = Modifier.fillMaxWidth().padding(5.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Column { - if (dateLabel != null) { - Text( - text = dateLabel, - style = TextStyle( - fontSize = 14.sp - ) - ) - } + val fontSize = 20.sp - if (distance != null) { - val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt() - val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}" - val text = if (includeDistanceLabel){ - if(distanceInUserUnit > 0){ - "In $label" - } else { - "$label ago" - } - } else { - label - } - - Text( - text = text, - style = TextStyle( - fontSize = 14.sp - ) + Row( + modifier = Modifier.fillMaxWidth().padding(5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column { + if (dateLabel != null) { + Text( + text = dateLabel, + style = TextStyle( + fontSize = fontSize ) - } - - if (timeLabel != null) { - Text( - text = timeLabel, - style = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 14.sp - ) - ) - } + ) } - // Weather icon (larger) - Icon( - painter = painterResource(id = getWeatherIcon(current)), - contentDescription = "Current weather", - modifier = Modifier.size(72.dp) - ) - - Column { - // Temperature (larger) - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - painter = painterResource(id = R.drawable.thermometer), - contentDescription = "Temperature", - modifier = Modifier.size(18.dp) - ) - - Spacer(modifier = Modifier.width(4.dp)) - - Text( - text = "${temperature}${temperatureUnit.unitDisplay}", - style = TextStyle( - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) - ) + if (distance != null) { + val distanceInUserUnit = (distance / (if(!isImperial) 1000.0 else 1609.34)).toInt() + val label = "${distanceInUserUnit.absoluteValue}${if(!isImperial) "km" else "mi"}" + val text = if (includeDistanceLabel){ + if(distanceInUserUnit > 0){ + "In $label" + } else { + "$label ago" + } + } else { + label } - // Precipitation - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 4.dp) - ) { - val precipitationProbabilityLabel = - if (precipitationProbability != null) "${precipitationProbability}% " else "" - - Text( - text = "${precipitationProbabilityLabel}${ceil(precipitation).toInt()}${precipitationUnit.unitDisplay}", - style = TextStyle( - fontSize = 14.sp - ) + Text( + text = text, + style = TextStyle( + fontSize = fontSize ) - } + ) + } - // Wind - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 4.dp) - ) { - Image( - bitmap = getArrowBitmapByBearing(baseBitmap, windBearing).asImageBitmap(), - colorFilter = ColorFilter.tint(Color.Black), - contentDescription = "Wind direction", - modifier = Modifier.size(16.dp) + if (timeLabel != null) { + Text( + text = timeLabel, + style = TextStyle( + fontWeight = FontWeight.Bold, + fontSize = fontSize ) - - Spacer(modifier = Modifier.width(4.dp)) - - Text( - text = "$windSpeed,$windGusts", - style = TextStyle( - fontSize = 14.sp - ) - ) - } + ) } } + + // Weather icon (larger) + Icon( + painter = painterResource(id = getWeatherIcon(current)), + contentDescription = "Current weather", + modifier = Modifier.size(72.dp) + ) + + Column(horizontalAlignment = Alignment.End) { + // Temperature (larger) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.thermometer), + contentDescription = "Temperature", + modifier = Modifier.size(18.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "${temperature}${temperatureUnit.unitDisplay}", + style = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.Bold + ) + ) + } + + // Precipitation + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 4.dp) + ) { + val precipitationProbabilityLabel = + if (precipitationProbability != null) "${precipitationProbability}% " else "" + + Icon( + painter = painterResource(id = R.drawable.droplet_regular), + contentDescription = "Precipitation", + modifier = Modifier.size(18.dp) + ) + + Text( + text = "${precipitationProbabilityLabel}${ceil(precipitation).toInt()}", + style = TextStyle( + fontSize = fontSize + ) + ) + } + + // Wind + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 4.dp) + ) { + Image( + bitmap = getArrowBitmapByBearing(baseBitmap, windBearing).asImageBitmap(), + colorFilter = ColorFilter.tint(Color.Black), + contentDescription = "Wind direction", + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(4.dp)) + + Text( + text = "$windSpeed,$windGusts", + style = TextStyle( + fontSize = fontSize + ) + ) + } + } + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/droplet_regular.png b/app/src/main/res/drawable/droplet_regular.png new file mode 100644 index 0000000000000000000000000000000000000000..1e579f7862406f81c2b1ce1bf2ef554f34e0ea41 GIT binary patch literal 7844 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 J=B|D8{{b!DlK%hz literal 0 HcmV?d00001