Weather widget: launch weather app on tap if installed

This commit is contained in:
MM20 2025-04-28 21:48:33 +02:00
parent c217c346b2
commit a711b39b9c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
2 changed files with 205 additions and 148 deletions

View File

@ -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,
)
}
}
}
}

View File

@ -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) {