diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt index 418c6fee3..dc70aaa02 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/weather/WeatherWidget.kt @@ -5,7 +5,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.text.format.DateFormat -import android.text.format.DateUtils import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState @@ -81,6 +80,10 @@ import de.mm20.launcher2.ui.component.Tooltip import de.mm20.launcher2.ui.component.weather.AnimatedWeatherIcon import de.mm20.launcher2.ui.component.weather.WeatherIcon import de.mm20.launcher2.ui.theme.transparency.transparency +import de.mm20.launcher2.ui.utils.formatPercent +import de.mm20.launcher2.ui.utils.formatPrecipitation +import de.mm20.launcher2.ui.utils.formatSpeed +import de.mm20.launcher2.ui.utils.formatTemperature import de.mm20.launcher2.weather.DailyForecast import de.mm20.launcher2.weather.Forecast import de.mm20.launcher2.widgets.WeatherWidget @@ -88,7 +91,6 @@ import kotlinx.coroutines.flow.map import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.Locale -import kotlin.math.roundToInt @Composable fun WeatherWidget(widget: WeatherWidget) { @@ -337,10 +339,11 @@ fun CurrentWeather( ) { Text( modifier = Modifier.weight(1f), - text = convertTemperature( - useFahrenheit = useFahrenheit, - temp = forecast.temperature - ).toString() + "°", + text = formatTemperature( + context, + forecast.temperature.toFloat(), + measurementSystem + ), style = MaterialTheme.typography.headlineMedium, ) Text( @@ -379,7 +382,7 @@ fun CurrentWeather( ) Spacer(modifier = Modifier.padding(3.dp)) Text( - text = "${forecast.humidity!!.roundToInt()} %", + text = formatPercent(forecast.humidity!!.toFloat()), style = MaterialTheme.typography.bodySmall, ) } @@ -415,7 +418,11 @@ fun CurrentWeather( Spacer(modifier = Modifier.padding(3.dp)) Text( text = if (forecast.windSpeed != null) { - formatWindSpeed(useMph, forecast) + formatSpeed( + context, + forecast.windSpeed!!.toFloat(), + measurementSystem + ) } else { windDirectionAsWord(forecast.windDirection!!) }, @@ -439,7 +446,15 @@ fun CurrentWeather( ) Spacer(modifier = Modifier.padding(3.dp)) Text( - text = formatPrecipitation(useInches, forecast), + text = if (forecast.precipProbability != null) { + formatPercent(forecast.precipProbability!!.toFloat()) + } else { + formatPrecipitation( + context, + forecast.precipitation?.toFloat() ?: 0f, + measurementSystem + ) + }, style = MaterialTheme.typography.bodySmall, ) } @@ -523,7 +538,11 @@ fun WeatherTimeSelector( Text( modifier = Modifier .alpha(alpha), - text = "${convertTemperature(useFahrenheit, fc.temperature)}°", + text = formatTemperature( + context, + fc.temperature.toFloat(), + measurementSystem + ), softWrap = false, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -550,7 +569,7 @@ fun WeatherDaySelector( measurementSystem: MeasurementSystem ) { val dateFormat = SimpleDateFormat("EEE") - val useFahrenheit = measurementSystem == MeasurementSystem.UnitedStates + val context = LocalContext.current val listState = rememberLazyListState() LazyRow( @@ -592,8 +611,18 @@ fun WeatherDaySelector( Text( modifier = Modifier.padding(start = 8.dp), text = "${dateFormat.format(day.timestamp)} " + - "${convertTemperature(useFahrenheit, day.minTemp)}° / " + - "${convertTemperature(useFahrenheit, day.maxTemp)}°", + "${ + formatTemperature( + context, + day.minTemp.toFloat(), + measurementSystem + ) + } / " + + formatTemperature( + context, + day.maxTemp.toFloat(), + measurementSystem + ), softWrap = false, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, @@ -619,36 +648,13 @@ fun WeatherDaySelector( private fun formatTime(context: Context, timestamp: Long, timeFormat: TimeFormat): String { val skeleton = if (timeFormat == TimeFormat.TwelveHour) "h:mm a" else "H:mm" - val dateFormat = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton), Locale.getDefault()) + val dateFormat = SimpleDateFormat( + DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton), + Locale.getDefault() + ) return dateFormat.format(timestamp) } -private fun convertTemperature(useFahrenheit: Boolean, temp: Double): Int { - return (if (useFahrenheit) temp * 9.0 / 5.0 - 459.67 else temp + -273.15).roundToInt() -} - -@Composable -private fun formatWindSpeed(imperialUnits: Boolean, forecast: Forecast): String { - if (forecast.windSpeed == null) return "" - val formatter = DecimalFormat("0.#") - val speedValue = formatter.format(forecast.windSpeed!! * if (imperialUnits) 2.2369 else 1.0) - val speedUnit = - stringResource(id = if (imperialUnits) R.string.unit_mile_per_hour_symbol else R.string.unit_meter_per_second_symbol) - return "$speedValue $speedUnit" -} - -@Composable -private fun formatPrecipitation(useInches: Boolean, forecast: Forecast): String { - if (forecast.precipProbability != null) { - return "${forecast.precipProbability} %" - } - val formatter = if (useInches) DecimalFormat("#.##") else DecimalFormat("#.#") - val precipUnit = - if (useInches) stringResource(id = R.string.unit_inch_symbol) else stringResource(id = R.string.unit_millimeter_symbol) - - return "${formatter.format(forecast.precipitation)} $precipUnit" -} - @Composable private fun windDirectionAsWord(direction: Double): String { return stringResource( diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/utils/Units.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/utils/Units.kt new file mode 100644 index 000000000..5e110c009 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/utils/Units.kt @@ -0,0 +1,293 @@ +package de.mm20.launcher2.ui.utils + +import android.content.Context +import android.icu.number.NumberFormatter +import android.icu.number.Precision +import android.icu.util.Measure +import android.icu.util.MeasureUnit +import de.mm20.launcher2.ktx.isAtLeastApiLevel +import de.mm20.launcher2.preferences.MeasurementSystem +import de.mm20.launcher2.ui.R +import java.text.NumberFormat +import java.util.Locale + + +internal fun formatDistance( + context: Context, + meters: Float, + measurementSystem: MeasurementSystem, +): String { + when (measurementSystem) { + MeasurementSystem.UnitedStates -> { + val feet = meters * 3.28084f + + if (feet >= 2640) { + return formatMiles(context, feet / 5280f) + } + return formatFeet(context, feet) + } + + MeasurementSystem.UnitedKingdom -> { + val yards = meters * 1.09361f + + if (yards >= 880) { + return formatMiles(context, yards / 1760f) + } + return formatYards(context, yards) + } + + else -> { + if (meters >= 1000) { + return formatKilometers(context, meters / 1000f) + } + return formatMeters(context, meters) + } + } +} + +private fun formatMeters( + context: Context, + meters: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(meters, MeasureUnit.METER)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(meters) + return "$formatted ${context.getString(R.string.unit_meter_symbol)}" + } +} + +private fun formatKilometers( + context: Context, + kilometers: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(1)) + .format(Measure(kilometers, MeasureUnit.KILOMETER)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 1 + }.format(kilometers) + return "$formatted ${context.getString(R.string.unit_kilometer_symbol)}" + } +} + + +private fun formatFeet( + context: Context, + feet: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(feet, MeasureUnit.FOOT)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(feet) + return "$formatted ${context.getString(R.string.unit_foot_symbol)}" + } +} + +private fun formatYards( + context: Context, + yards: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(yards, MeasureUnit.YARD)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(yards) + return "$formatted ${context.getString(R.string.unit_yard_symbol)}" + } +} + +private fun formatMiles( + context: Context, + miles: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(1)) + .format(Measure(miles, MeasureUnit.MILE)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 1 + }.format(miles) + return "$formatted ${context.getString(R.string.unit_mile_symbol)}" + } +} + +internal fun formatSpeed( + context: Context, + metersPerSecond: Float, + measurementSystem: MeasurementSystem, +): String { + if (measurementSystem == MeasurementSystem.UnitedStates) { + return formatMpH(context, metersPerSecond * 2.2369f) + } else { + return formatKmH(context, metersPerSecond * 3.6f) + } +} + +private fun formatKmH( + context: Context, + kmH: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(kmH, MeasureUnit.KILOMETER_PER_HOUR)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(kmH) + return "$formatted ${context.getString(R.string.unit_kilometer_per_hour_symbol)}" + } +} + +private fun formatMpH( + context: Context, + mph: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(mph, MeasureUnit.MILE_PER_HOUR)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(mph) + return "$formatted ${context.getString(R.string.unit_mile_per_hour_symbol)}" + } +} + +internal fun formatTemperature( + context: Context, + kelvins: Float, + measurementSystem: MeasurementSystem, +): String { + if (measurementSystem == MeasurementSystem.UnitedStates) { + return formatFahrenheit(context, kelvins * 9f / 5f - 459.67f) + } else { + return formatCelsius(context, kelvins + -273.15f) + } +} + +private fun formatCelsius( + context: Context, + celsius: Float, +): String { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(celsius) + return "$formatted°" +} + +private fun formatFahrenheit( + context: Context, + fahrenheit: Float, +): String { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(fahrenheit) + return "$formatted°" + +} + +internal fun formatPercent( + percent: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(percent, MeasureUnit.PERCENT)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(percent) + return "$formatted %" + } +} + +internal fun formatPrecipitation( + context: Context, + millimeters: Float, + measurementSystem: MeasurementSystem, +): String { + if (measurementSystem == MeasurementSystem.UnitedStates) { + return formatInches(context, millimeters / 25.4f) + } else { + return formatMillimeters(context, millimeters) + } +} + +private fun formatMillimeters( + context: Context, + millimeters: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(0)) + .format(Measure(millimeters, MeasureUnit.MILLIMETER)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 0 + }.format(millimeters) + return "$formatted ${context.getString(R.string.unit_millimeter_symbol)}" + } +} + +private fun formatInches( + context: Context, + inches: Float, +): String { + if (isAtLeastApiLevel(30)) { + return NumberFormatter + .withLocale( + Locale.getDefault() + ).unitWidth(NumberFormatter.UnitWidth.NARROW) + .precision(Precision.maxFraction(1)) + .format(Measure(inches, MeasureUnit.INCH)).toString() + } else { + val formatted = NumberFormat.getInstance().apply { + maximumFractionDigits = 1 + }.format(inches) + return "$formatted ${context.getString(R.string.unit_inch_symbol)}" + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/utils/Utils.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/utils/Utils.kt deleted file mode 100644 index d34aaef60..000000000 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/utils/Utils.kt +++ /dev/null @@ -1,141 +0,0 @@ -package de.mm20.launcher2.ui.utils - -import android.content.Context -import android.icu.number.NumberFormatter -import android.icu.number.Precision -import android.icu.util.Measure -import android.icu.util.MeasureUnit -import de.mm20.launcher2.ktx.isAtLeastApiLevel -import de.mm20.launcher2.preferences.MeasurementSystem -import de.mm20.launcher2.ui.R -import java.text.NumberFormat -import java.util.Locale - - -internal fun formatDistance( - context: Context, - meters: Float, - measurementSystem: MeasurementSystem, -): String { - when (measurementSystem) { - MeasurementSystem.UnitedStates -> { - val feet = meters * 3.28084f - - if (feet >= 2640) { - return formatMiles(context, feet / 5280f) - } - return formatFeet(context, feet) - } - MeasurementSystem.UnitedKingdom -> { - val yards = meters * 1.09361f - - if (yards >= 880) { - return formatMiles(context, yards / 1760f) - } - return formatYards(context, yards) - } - - else -> { - if (meters >= 1000) { - return formatKilometers(context, meters / 1000f) - } - return formatMeters(context, meters) - } - } -} - -private fun formatMeters( - context: Context, - meters: Float, -): String { - if (isAtLeastApiLevel(30)) { - return NumberFormatter - .withLocale( - Locale.getDefault() - ).unitWidth(NumberFormatter.UnitWidth.NARROW) - .precision(Precision.maxFraction(0)) - .format(Measure(meters, MeasureUnit.METER)).toString() - } else { - val formatted = NumberFormat.getInstance().apply { - maximumFractionDigits = 0 - }.format(meters) - return "$formatted ${context.getString(R.string.unit_meter_symbol)}" - } -} - -private fun formatKilometers( - context: Context, - kilometers: Float, -): String { - if (isAtLeastApiLevel(30)) { - return NumberFormatter - .withLocale( - Locale.getDefault() - ).unitWidth(NumberFormatter.UnitWidth.NARROW) - .precision(Precision.maxFraction(1)) - .format(Measure(kilometers, MeasureUnit.KILOMETER)).toString() - } else { - val formatted = NumberFormat.getInstance().apply { - maximumFractionDigits = 1 - }.format(kilometers) - return "$formatted ${context.getString(R.string.unit_kilometer_symbol)}" - } -} - - -private fun formatFeet( - context: Context, - feet: Float, -): String { - if (isAtLeastApiLevel(30)) { - return NumberFormatter - .withLocale( - Locale.getDefault() - ).unitWidth(NumberFormatter.UnitWidth.NARROW) - .precision(Precision.maxFraction(0)) - .format(Measure(feet, MeasureUnit.FOOT)).toString() - } else { - val formatted = NumberFormat.getInstance().apply { - maximumFractionDigits = 0 - }.format(feet) - return "$formatted ${context.getString(R.string.unit_foot_symbol)}" - } -} - -private fun formatYards( - context: Context, - yards: Float, -): String { - if (isAtLeastApiLevel(30)) { - return NumberFormatter - .withLocale( - Locale.getDefault() - ).unitWidth(NumberFormatter.UnitWidth.NARROW) - .precision(Precision.maxFraction(0)) - .format(Measure(yards, MeasureUnit.YARD)).toString() - } else { - val formatted = NumberFormat.getInstance().apply { - maximumFractionDigits = 0 - }.format(yards) - return "$formatted ${context.getString(R.string.unit_yard_symbol)}" - } -} - -private fun formatMiles( - context: Context, - miles: Float, -): String { - if (isAtLeastApiLevel(30)) { - return NumberFormatter - .withLocale( - Locale.getDefault() - ).unitWidth(NumberFormatter.UnitWidth.NARROW) - .precision(Precision.maxFraction(1)) - .format(Measure(miles, MeasureUnit.MILE)).toString() - } else { - val formatted = NumberFormat.getInstance().apply { - maximumFractionDigits = 1 - }.format(miles) - return "$formatted ${context.getString(R.string.unit_mile_symbol)}" - } -} \ No newline at end of file