Weather widget: launch weather app on tap if installed
This commit is contained in:
parent
c217c346b2
commit
a711b39b9c
@ -1,13 +1,18 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.widgets.weather
|
package de.mm20.launcher2.ui.launcher.widgets.weather
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import android.util.Log
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
import androidx.compose.foundation.clickable
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@ -48,13 +53,18 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
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.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.onPlaced
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.app.ActivityOptionsCompat
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@ -208,173 +218,219 @@ fun WeatherWidget(widget: WeatherWidget) {
|
|||||||
@Composable
|
@Composable
|
||||||
fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
|
fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
Column {
|
val weatherApp = remember {
|
||||||
Row(
|
context.packageManager.resolveActivity(
|
||||||
verticalAlignment = Alignment.Top,
|
Intent(Intent.ACTION_MAIN).also {
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
it.addCategory(Intent.CATEGORY_APP_WEATHER)
|
||||||
) {
|
}, 0
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var bounds by remember { mutableStateOf(Rect.Zero) }
|
||||||
Row(
|
val view = LocalView.current
|
||||||
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 8.dp)
|
.onPlaced {
|
||||||
.fillMaxWidth(),
|
val size = it.size
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
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(
|
Column(
|
||||||
tooltipText = stringResource(R.string.weather_forecast_humidity)
|
) {
|
||||||
|
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(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(top = 16.dp, start = 16.dp, end = 16.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.HumidityPercentage,
|
imageVector = if (isLatLon) Icons.Rounded.MyLocation else Icons.Rounded.LocationCity,
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
|
||||||
contentDescription = null
|
contentDescription = null
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.padding(3.dp))
|
Spacer(modifier = Modifier.padding(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "${forecast.humidity!!.roundToInt()} %",
|
text = forecast.location,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
Tooltip(
|
||||||
|
tooltipText = stringResource(R.string.preference_weather_provider)
|
||||||
}
|
|
||||||
if (forecast.windDirection != null || forecast.windSpeed != null) {
|
|
||||||
Tooltip(
|
|
||||||
tooltipText = stringResource(R.string.weather_forecast_wind)
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
if (forecast.windDirection != null) {
|
Surface(
|
||||||
// windDirection is "fromDirection"; Wind (arrow) blows into opposite direction
|
shape = MaterialTheme.shapes.extraSmall.copy(
|
||||||
val angle by animateFloatAsState(forecast.windDirection!!.toFloat() + 180f)
|
topStart = CornerSize(0),
|
||||||
Icon(
|
topEnd = CornerSize(0),
|
||||||
imageVector = Icons.Rounded.North,
|
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
|
modifier = Modifier
|
||||||
.rotate(angle)
|
.clickable(onClick = {
|
||||||
.size(20.dp),
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
contentDescription = null,
|
data = Uri.parse(forecast.providerUrl)
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
?: return@clickable
|
||||||
)
|
}
|
||||||
} else {
|
context.tryStartActivity(intent)
|
||||||
Icon(
|
})
|
||||||
imageVector = Icons.Rounded.Air,
|
.padding(start = 8.dp, top = 4.dp, bottom = 4.dp, end = 12.dp)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Row(
|
||||||
if (forecast.precipitation != null) {
|
modifier = Modifier.padding(start = 16.dp),
|
||||||
Tooltip(
|
verticalAlignment = Alignment.CenterVertically
|
||||||
tooltipText = stringResource(id = R.string.weather_forecast_precipitation)
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Text(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
Row(
|
||||||
imageVector = Icons.Rounded.Rain,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
modifier = Modifier.size(20.dp),
|
) {
|
||||||
contentDescription = null,
|
Icon(
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
imageVector = Icons.Rounded.HumidityPercentage,
|
||||||
)
|
modifier = Modifier.size(20.dp),
|
||||||
Spacer(modifier = Modifier.padding(3.dp))
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
Text(
|
contentDescription = null
|
||||||
text = formatPrecipitation(imperialUnits, forecast),
|
)
|
||||||
style = MaterialTheme.typography.bodySmall,
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
class BreezyWeatherProvider(
|
class BreezyWeatherProvider(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
@ -38,7 +39,7 @@ class BreezyWeatherProvider(
|
|||||||
|
|
||||||
override suspend fun getUpdateInterval(): Long {
|
override suspend fun getUpdateInterval(): Long {
|
||||||
// Updates are pushed, no need to pull
|
// Updates are pushed, no need to pull
|
||||||
return Long.MAX_VALUE
|
return 365.days.inWholeMilliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun pushWeatherData(data: BreezyWeatherData) {
|
internal suspend fun pushWeatherData(data: BreezyWeatherData) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user