From a711b39b9c55e2b53ef24eb346d48398c6a90615 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 28 Apr 2025 21:48:33 +0200 Subject: [PATCH] Weather widget: launch weather app on tap if installed --- .../launcher/widgets/weather/WeatherWidget.kt | 350 ++++++++++-------- .../weather/breezy/BreezyWeatherProvider.kt | 3 +- 2 files changed, 205 insertions(+), 148 deletions(-) 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 22c9e837..ff540f94 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 @@ -1,13 +1,18 @@ package de.mm20.launcher2.ui.launcher.widgets.weather +import android.content.ComponentName import android.content.Context import android.content.Intent import android.net.Uri import android.text.format.DateUtils +import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -48,13 +53,18 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.app.ActivityOptionsCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -208,173 +218,219 @@ fun WeatherWidget(widget: WeatherWidget) { @Composable fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) { val context = LocalContext.current - Column { - Row( - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - val latLonRegexp = - remember { Regex("^\\d{1,2}°\\d{1,2}'[NS] \\d{1,3}°\\d{1,2}'[EW]\$") } - val isLatLon = latLonRegexp.matches(forecast.location) - Row( - modifier = Modifier - .weight(1f) - .padding(top = 16.dp, start = 16.dp, end = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - imageVector = if (isLatLon) Icons.Rounded.MyLocation else Icons.Rounded.LocationCity, - contentDescription = null - ) - Spacer(modifier = Modifier.padding(4.dp)) - Text( - text = forecast.location, - style = MaterialTheme.typography.titleMedium - ) - } - Tooltip( - tooltipText = stringResource(R.string.preference_weather_provider) - ) { - Surface( - shape = MaterialTheme.shapes.extraSmall.copy( - topStart = CornerSize(0), - topEnd = CornerSize(0), - bottomEnd = CornerSize(0) - ), - color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalCardStyle.current.opacity), - ) { - Text( - text = "${forecast.provider} (${ - formatTime( - LocalContext.current, - forecast.updateTime - ) - })", - style = MaterialTheme.typography.bodySmall.copy(fontSize = 8.sp), - modifier = Modifier - .clickable(onClick = { - val intent = Intent(Intent.ACTION_VIEW).apply { - data = Uri.parse(forecast.providerUrl) - ?: return@clickable - } - context.tryStartActivity(intent) - }) - .padding(start = 8.dp, top = 4.dp, bottom = 4.dp, end = 12.dp) - ) - } - } - } - Row( - modifier = Modifier.padding(start = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - modifier = Modifier.weight(1f), - text = convertTemperature( - imperialUnits = imperialUnits, - temp = forecast.temperature - ).toString() + "°", - style = MaterialTheme.typography.headlineMedium, - ) - Text( - text = forecast.condition, - style = MaterialTheme.typography.labelMedium, - ) - AnimatedWeatherIcon( - modifier = Modifier.padding( - start = 8.dp, - end = 8.dp - ), - icon = weatherIconById(forecast.icon), - night = forecast.night - ) - } + val weatherApp = remember { + context.packageManager.resolveActivity( + Intent(Intent.ACTION_MAIN).also { + it.addCategory(Intent.CATEGORY_APP_WEATHER) + }, 0 + ) } - - Row( + var bounds by remember { mutableStateOf(Rect.Zero) } + val view = LocalView.current + Column( modifier = Modifier - .padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + .onPlaced { + val size = it.size + val offset = it.localToRoot(Offset.Zero) + bounds = Rect( + offset.x, + offset.y, + offset.x + size.width, + offset.y + size.height + ) + } + .clickable( + enabled = weatherApp != null, + onClick = { + context.tryStartActivity( + Intent().also { + it.component = weatherApp?.activityInfo?.let { + ComponentName(it.packageName, it.name) + } + }, + ActivityOptionsCompat.makeClipRevealAnimation( + view, + bounds.left.toInt(), + bounds.top.toInt(), + bounds.width.toInt(), + bounds.height.toInt() + ).toBundle() + ) + }, + interactionSource = MutableInteractionSource(), + indication = LocalIndication.current, + ) ) { - if (forecast.humidity != null) { - Tooltip( - tooltipText = stringResource(R.string.weather_forecast_humidity) + + Column( + ) { + Row( + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.SpaceBetween, ) { + val latLonRegexp = + remember { Regex("^\\d{1,2}°\\d{1,2}'[NS] \\d{1,3}°\\d{1,2}'[EW]\$") } + val isLatLon = latLonRegexp.matches(forecast.location) Row( - verticalAlignment = Alignment.CenterVertically + modifier = Modifier + .weight(1f) + .padding(top = 16.dp, start = 16.dp, end = 16.dp), + verticalAlignment = Alignment.CenterVertically, ) { Icon( - imageVector = Icons.Rounded.HumidityPercentage, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.secondary, + imageVector = if (isLatLon) Icons.Rounded.MyLocation else Icons.Rounded.LocationCity, contentDescription = null ) - Spacer(modifier = Modifier.padding(3.dp)) + Spacer(modifier = Modifier.padding(4.dp)) Text( - text = "${forecast.humidity!!.roundToInt()} %", - style = MaterialTheme.typography.bodySmall, + text = forecast.location, + style = MaterialTheme.typography.titleMedium ) } - } - - } - if (forecast.windDirection != null || forecast.windSpeed != null) { - Tooltip( - tooltipText = stringResource(R.string.weather_forecast_wind) - ) { - Row( - verticalAlignment = Alignment.CenterVertically + Tooltip( + tooltipText = stringResource(R.string.preference_weather_provider) ) { - if (forecast.windDirection != null) { - // windDirection is "fromDirection"; Wind (arrow) blows into opposite direction - val angle by animateFloatAsState(forecast.windDirection!!.toFloat() + 180f) - Icon( - imageVector = Icons.Rounded.North, + Surface( + shape = MaterialTheme.shapes.extraSmall.copy( + topStart = CornerSize(0), + topEnd = CornerSize(0), + bottomEnd = CornerSize(0) + ), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = LocalCardStyle.current.opacity), + ) { + Text( + text = "${forecast.provider} (${ + formatTime( + LocalContext.current, + forecast.updateTime + ) + })", + style = MaterialTheme.typography.bodySmall.copy(fontSize = 8.sp), modifier = Modifier - .rotate(angle) - .size(20.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary, - ) - } else { - Icon( - imageVector = Icons.Rounded.Air, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.secondary, + .clickable(onClick = { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(forecast.providerUrl) + ?: return@clickable + } + context.tryStartActivity(intent) + }) + .padding(start = 8.dp, top = 4.dp, bottom = 4.dp, end = 12.dp) ) } - Spacer(modifier = Modifier.padding(3.dp)) - Text( - text = if (forecast.windSpeed != null) { - formatWindSpeed(imperialUnits, forecast) - } else { - windDirectionAsWord(forecast.windDirection!!) - }, - style = MaterialTheme.typography.bodySmall, - ) } } - } - if (forecast.precipitation != null) { - Tooltip( - tooltipText = stringResource(id = R.string.weather_forecast_precipitation) + Row( + modifier = Modifier.padding(start = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - verticalAlignment = Alignment.CenterVertically + Text( + modifier = Modifier.weight(1f), + text = convertTemperature( + imperialUnits = imperialUnits, + temp = forecast.temperature + ).toString() + "°", + style = MaterialTheme.typography.headlineMedium, + ) + Text( + text = forecast.condition, + style = MaterialTheme.typography.labelMedium, + ) + AnimatedWeatherIcon( + modifier = Modifier.padding( + start = 8.dp, + end = 8.dp + ), + icon = weatherIconById(forecast.icon), + night = forecast.night + ) + } + } + + Row( + modifier = Modifier + .padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 8.dp) + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (forecast.humidity != null) { + Tooltip( + tooltipText = stringResource(R.string.weather_forecast_humidity) ) { - Icon( - imageVector = Icons.Rounded.Rain, - modifier = Modifier.size(20.dp), - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary, - ) - Spacer(modifier = Modifier.padding(3.dp)) - Text( - text = formatPrecipitation(imperialUnits, forecast), - style = MaterialTheme.typography.bodySmall, - ) + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.HumidityPercentage, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.secondary, + contentDescription = null + ) + Spacer(modifier = Modifier.padding(3.dp)) + Text( + text = "${forecast.humidity!!.roundToInt()} %", + style = MaterialTheme.typography.bodySmall, + ) + } + } + + } + if (forecast.windDirection != null || forecast.windSpeed != null) { + Tooltip( + tooltipText = stringResource(R.string.weather_forecast_wind) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (forecast.windDirection != null) { + // windDirection is "fromDirection"; Wind (arrow) blows into opposite direction + val angle by animateFloatAsState(forecast.windDirection!!.toFloat() + 180f) + Icon( + imageVector = Icons.Rounded.North, + modifier = Modifier + .rotate(angle) + .size(20.dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + ) + } else { + Icon( + imageVector = Icons.Rounded.Air, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + Spacer(modifier = Modifier.padding(3.dp)) + Text( + text = if (forecast.windSpeed != null) { + formatWindSpeed(imperialUnits, forecast) + } else { + windDirectionAsWord(forecast.windDirection!!) + }, + style = MaterialTheme.typography.bodySmall, + ) + } + } + } + if (forecast.precipitation != null) { + Tooltip( + tooltipText = stringResource(id = R.string.weather_forecast_precipitation) + ) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.Rain, + modifier = Modifier.size(20.dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + ) + Spacer(modifier = Modifier.padding(3.dp)) + Text( + text = formatPrecipitation(imperialUnits, forecast), + style = MaterialTheme.typography.bodySmall, + ) + } } } } diff --git a/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt index 4cedd142..844ce739 100644 --- a/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt +++ b/data/weather/src/main/java/de/mm20/launcher2/weather/breezy/BreezyWeatherProvider.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.core.component.KoinComponent import org.koin.core.component.inject +import kotlin.time.Duration.Companion.days class BreezyWeatherProvider( private val context: Context, @@ -38,7 +39,7 @@ class BreezyWeatherProvider( override suspend fun getUpdateInterval(): Long { // Updates are pushed, no need to pull - return Long.MAX_VALUE + return 365.days.inWholeMilliseconds } internal suspend fun pushWeatherData(data: BreezyWeatherData) {