Refactor weather widget
This commit is contained in:
parent
2ee50459a9
commit
b9054f04cd
@ -21,6 +21,7 @@ import de.mm20.launcher2.websites.websitesModule
|
|||||||
import de.mm20.launcher2.widgets.widgetsModule
|
import de.mm20.launcher2.widgets.widgetsModule
|
||||||
import de.mm20.launcher2.wikipedia.wikipediaModule
|
import de.mm20.launcher2.wikipedia.wikipediaModule
|
||||||
import de.mm20.launcher2.database.databaseModule
|
import de.mm20.launcher2.database.databaseModule
|
||||||
|
import de.mm20.launcher2.weather.weatherModule
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
@ -67,6 +68,7 @@ class LauncherApplication : Application(), CoroutineScope {
|
|||||||
musicModule,
|
musicModule,
|
||||||
searchModule,
|
searchModule,
|
||||||
unitConverterModule,
|
unitConverterModule,
|
||||||
|
weatherModule,
|
||||||
websitesModule,
|
websitesModule,
|
||||||
widgetsModule,
|
widgetsModule,
|
||||||
wikipediaModule
|
wikipediaModule
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
package de.mm20.launcher2.database
|
package de.mm20.launcher2.database
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy.REPLACE
|
import androidx.room.OnConflictStrategy.REPLACE
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Transaction
|
import androidx.room.Transaction
|
||||||
import de.mm20.launcher2.database.entities.ForecastEntity
|
import de.mm20.launcher2.database.entities.ForecastEntity
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface WeatherDao {
|
interface WeatherDao {
|
||||||
@Query("SELECT * FROM ${ForecastEntity.TABLE_NAME} ORDER BY timestamp ASC")
|
@Query("SELECT * FROM ${ForecastEntity.TABLE_NAME} ORDER BY timestamp ASC")
|
||||||
fun getForecasts(): LiveData<List<ForecastEntity>>
|
fun getForecasts(): Flow<List<ForecastEntity>>
|
||||||
|
|
||||||
@Insert(onConflict = REPLACE)
|
@Insert(onConflict = REPLACE)
|
||||||
fun insertAll(forecasts: List<ForecastEntity>)
|
fun insertAll(forecasts: List<ForecastEntity>)
|
||||||
|
|||||||
@ -20,10 +20,8 @@ import androidx.compose.runtime.livedata.observeAsState
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.colorResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.DpOffset
|
import androidx.compose.ui.unit.DpOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@ -31,11 +29,11 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import de.mm20.launcher2.ktx.tryStartActivity
|
import de.mm20.launcher2.ktx.tryStartActivity
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.widgets.weather.WeatherWidgetWM
|
||||||
import de.mm20.launcher2.ui.weather.AnimatedWeatherIcon
|
import de.mm20.launcher2.ui.weather.AnimatedWeatherIcon
|
||||||
import de.mm20.launcher2.ui.weather.WeatherIcon
|
import de.mm20.launcher2.ui.weather.WeatherIcon
|
||||||
import de.mm20.launcher2.weather.DailyForecast
|
import de.mm20.launcher2.weather.DailyForecast
|
||||||
import de.mm20.launcher2.weather.Forecast
|
import de.mm20.launcher2.weather.Forecast
|
||||||
import de.mm20.launcher2.weather.WeatherViewModel
|
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@ -44,155 +42,153 @@ import kotlin.math.roundToInt
|
|||||||
@OptIn(ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalAnimationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun WeatherWidget() {
|
fun WeatherWidget() {
|
||||||
val viewModel: WeatherViewModel = viewModel()
|
val viewModel: WeatherWidgetWM = viewModel()
|
||||||
val weatherData by viewModel.forecasts.observeAsState(initial = emptyList())
|
|
||||||
var selectedDayIndex by remember { mutableStateOf(0) }
|
|
||||||
var selectedForecastIndex by remember { mutableStateOf(0) }
|
|
||||||
var detailsExpanded by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
|
val selectedForecast by viewModel.currentForecast.observeAsState()
|
||||||
|
|
||||||
if (weatherData.isNotEmpty() && weatherData.size <= selectedDayIndex) {
|
val imperialUnits by viewModel.imperialUnits.observeAsState(false)
|
||||||
selectedDayIndex = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weatherData.isNotEmpty() && weatherData[selectedDayIndex].hourlyForecasts.size <= selectedForecastIndex) {
|
val forecast = selectedForecast ?: run {
|
||||||
selectedForecastIndex = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (weatherData.isEmpty()) {
|
|
||||||
NoData()
|
NoData()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val selectedForecast = weatherData[selectedDayIndex].hourlyForecasts[selectedForecastIndex]
|
|
||||||
val imperialUnits = LauncherPreferences.instance.imperialUnits
|
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Row {
|
CurrentWeather(forecast, imperialUnits)
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(start = 16.dp, top = 16.dp)
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = selectedForecast.location,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = convertTemperature(
|
|
||||||
imperialUnits = imperialUnits,
|
|
||||||
temp = selectedForecast.temperature
|
|
||||||
).toString() + "°",
|
|
||||||
style = MaterialTheme.typography.headlineLarge,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = selectedForecast.condition,
|
|
||||||
style = MaterialTheme.typography.bodySmall
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = {
|
|
||||||
detailsExpanded = !detailsExpanded
|
|
||||||
})
|
|
||||||
.padding(vertical = 12.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = if (detailsExpanded) R.string.weather_widget_hide_details else R.string.weather_widget_show_details),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(visible = detailsExpanded) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
WeatherDetailRow(
|
|
||||||
title = stringResource(id = R.string.weather_humidity),
|
|
||||||
value = "${selectedForecast.humidity.roundToInt()} %"
|
|
||||||
)
|
|
||||||
WeatherDetailRow(
|
|
||||||
title = stringResource(id = R.string.weather_wind),
|
|
||||||
value = formatWindSpeed(imperialUnits, selectedForecast)
|
|
||||||
)
|
|
||||||
val precipitation = formatPrecipitation(imperialUnits, selectedForecast)
|
|
||||||
if (precipitation != null) {
|
|
||||||
WeatherDetailRow(
|
|
||||||
title = stringResource(id = R.string.weather_precipitation),
|
|
||||||
value = precipitation
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
|
||||||
horizontalAlignment = Alignment.End
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
AnimatedWeatherIcon(
|
|
||||||
modifier = Modifier.padding(all = 16.dp),
|
|
||||||
icon = weatherIconById(selectedForecast.icon),
|
|
||||||
night = selectedForecast.night
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 50),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
|
||||||
modifier = Modifier.align(Alignment.End)
|
|
||||||
|
|
||||||
) {
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = "${selectedForecast.provider} (${
|
|
||||||
formatTime(
|
|
||||||
LocalContext.current,
|
|
||||||
selectedForecast.updateTime
|
|
||||||
)
|
|
||||||
})",
|
|
||||||
style = TextStyle(
|
|
||||||
fontSize = 10.sp
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(onClick = {
|
|
||||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
|
||||||
data = Uri.parse(selectedForecast.providerUrl)
|
|
||||||
?: return@clickable
|
|
||||||
}
|
|
||||||
context.tryStartActivity(intent)
|
|
||||||
})
|
|
||||||
.padding(start = 8.dp, top = 4.dp, bottom = 4.dp, end = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
WeatherDaySelector(
|
val dailyForecasts by viewModel.dailyForecasts.observeAsState(emptyList())
|
||||||
days = weatherData,
|
val selectedDayForecast by viewModel.currentDailyForecast.observeAsState()
|
||||||
selectedDay = weatherData[selectedDayIndex],
|
selectedDayForecast?.let {
|
||||||
onDaySelected = {
|
WeatherDaySelector(
|
||||||
selectedDayIndex = it
|
days = dailyForecasts,
|
||||||
selectedForecastIndex = 0
|
selectedDay = it,
|
||||||
},
|
onDaySelected = {
|
||||||
modifier = Modifier.weight(1f)
|
viewModel.selectDay(it)
|
||||||
)
|
},
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val currentDayForecasts by viewModel.currentDayForecasts.observeAsState(emptyList())
|
||||||
WeatherTimeSelector(
|
WeatherTimeSelector(
|
||||||
forecasts = weatherData[selectedDayIndex].hourlyForecasts,
|
forecasts = currentDayForecasts,
|
||||||
selectedForecast = selectedForecast,
|
selectedForecast = forecast,
|
||||||
|
imperialUnits = imperialUnits,
|
||||||
onTimeSelected = {
|
onTimeSelected = {
|
||||||
selectedForecastIndex = it
|
viewModel.selectForecast(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CurrentWeather(forecast: Forecast, imperialUnits: Boolean) {
|
||||||
|
var detailsExpanded by remember { mutableStateOf(false) }
|
||||||
|
Row {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 16.dp, top = 16.dp)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = forecast.location,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = convertTemperature(
|
||||||
|
imperialUnits = imperialUnits,
|
||||||
|
temp = forecast.temperature
|
||||||
|
).toString() + "°",
|
||||||
|
style = MaterialTheme.typography.headlineLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = forecast.condition,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = {
|
||||||
|
detailsExpanded = !detailsExpanded
|
||||||
|
})
|
||||||
|
.padding(vertical = 12.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = if (detailsExpanded) R.string.weather_widget_hide_details else R.string.weather_widget_show_details),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = detailsExpanded) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
WeatherDetailRow(
|
||||||
|
title = stringResource(id = R.string.weather_humidity),
|
||||||
|
value = "${forecast.humidity.roundToInt()} %"
|
||||||
|
)
|
||||||
|
WeatherDetailRow(
|
||||||
|
title = stringResource(id = R.string.weather_wind),
|
||||||
|
value = formatWindSpeed(imperialUnits, forecast)
|
||||||
|
)
|
||||||
|
val precipitation = formatPrecipitation(imperialUnits, forecast)
|
||||||
|
if (precipitation != null) {
|
||||||
|
WeatherDetailRow(
|
||||||
|
title = stringResource(id = R.string.weather_precipitation),
|
||||||
|
value = precipitation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
|
horizontalAlignment = Alignment.End
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
AnimatedWeatherIcon(
|
||||||
|
modifier = Modifier.padding(all = 16.dp),
|
||||||
|
icon = weatherIconById(forecast.icon),
|
||||||
|
night = forecast.night
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(topStartPercent = 50, bottomStartPercent = 50),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f),
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "${forecast.provider} (${
|
||||||
|
formatTime(
|
||||||
|
LocalContext.current,
|
||||||
|
forecast.updateTime
|
||||||
|
)
|
||||||
|
})",
|
||||||
|
style = TextStyle(
|
||||||
|
fontSize = 10.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 = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun WeatherDetailRow(title: String, value: String) {
|
fun WeatherDetailRow(title: String, value: String) {
|
||||||
Row {
|
Row {
|
||||||
@ -299,12 +295,12 @@ fun WeatherTimeSelector(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forecasts: List<Forecast>,
|
forecasts: List<Forecast>,
|
||||||
selectedForecast: Forecast,
|
selectedForecast: Forecast,
|
||||||
|
imperialUnits: Boolean,
|
||||||
onTimeSelected: (Int) -> Unit
|
onTimeSelected: (Int) -> Unit
|
||||||
) {
|
) {
|
||||||
val menuExpanded = remember { mutableStateOf(false) }
|
val menuExpanded = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val dateFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT) }
|
val dateFormat = remember { DateFormat.getTimeInstance(DateFormat.SHORT) }
|
||||||
val imperialUnits = LauncherPreferences.instance.imperialUnits
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -455,7 +451,8 @@ fun NoData() {
|
|||||||
.wrapContentHeight(),
|
.wrapContentHeight(),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(imageVector = Icons.Rounded.LightMode,
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.LightMode,
|
||||||
contentDescription = "",
|
contentDescription = "",
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(24.dp)
|
.padding(24.dp)
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.widgets.weather
|
||||||
|
|
||||||
|
import androidx.lifecycle.*
|
||||||
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
|
import de.mm20.launcher2.weather.DailyForecast
|
||||||
|
import de.mm20.launcher2.weather.Forecast
|
||||||
|
import de.mm20.launcher2.weather.WeatherRepository
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
class WeatherWidgetWM : ViewModel(), KoinComponent {
|
||||||
|
private val weatherRepository: WeatherRepository by inject()
|
||||||
|
|
||||||
|
private var selectedDayIndex = 0
|
||||||
|
set(value) {
|
||||||
|
field = min(value, forecasts.lastIndex)
|
||||||
|
selectedForecastIndex = min(
|
||||||
|
selectedForecastIndex,
|
||||||
|
forecasts[value].hourlyForecasts.lastIndex
|
||||||
|
)
|
||||||
|
currentDayForecasts.postValue(forecasts[value].hourlyForecasts)
|
||||||
|
currentDailyForecast.postValue(forecasts[value])
|
||||||
|
currentForecast.postValue(getCurrentlySelectedForecast())
|
||||||
|
}
|
||||||
|
|
||||||
|
private var selectedForecastIndex = 0
|
||||||
|
set(value) {
|
||||||
|
field = min(value, forecasts[selectedDayIndex].hourlyForecasts.lastIndex)
|
||||||
|
currentForecast.postValue(getCurrentlySelectedForecast())
|
||||||
|
}
|
||||||
|
|
||||||
|
private val forecastsFlow = weatherRepository.forecasts
|
||||||
|
|
||||||
|
private var forecasts: List<DailyForecast> = emptyList()
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
selectedDayIndex = 0
|
||||||
|
selectedForecastIndex = 0
|
||||||
|
dailyForecasts.postValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
forecastsFlow.collectLatest {
|
||||||
|
forecasts = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentForecast = MutableLiveData<Forecast?>(getCurrentlySelectedForecast())
|
||||||
|
val dailyForecasts = MutableLiveData<List<DailyForecast>>(emptyList())
|
||||||
|
val currentDayForecasts = MutableLiveData<List<Forecast>>(emptyList())
|
||||||
|
val currentDailyForecast = MutableLiveData<DailyForecast>(null)
|
||||||
|
|
||||||
|
val imperialUnits = MutableLiveData(LauncherPreferences.instance.imperialUnits)
|
||||||
|
|
||||||
|
fun selectDay(index: Int) {
|
||||||
|
selectedDayIndex = min(index, forecasts.lastIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectForecast(index: Int) {
|
||||||
|
selectedForecastIndex = index
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getCurrentlySelectedForecast(): Forecast? {
|
||||||
|
return forecasts.getOrNull(selectedDayIndex)?.hourlyForecasts?.getOrNull(selectedForecastIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,8 +42,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.work)
|
implementation(libs.androidx.work)
|
||||||
implementation(libs.okhttp)
|
implementation(libs.okhttp)
|
||||||
implementation(libs.bundles.retrofit)
|
implementation(libs.bundles.retrofit)
|
||||||
|
|
||||||
implementation(libs.suncalc)
|
implementation(libs.suncalc)
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|||||||
@ -0,0 +1,8 @@
|
|||||||
|
package de.mm20.launcher2.weather
|
||||||
|
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.dsl.module
|
||||||
|
|
||||||
|
val weatherModule = module {
|
||||||
|
single { WeatherRepository(androidContext(), get()) }
|
||||||
|
}
|
||||||
@ -2,31 +2,35 @@ package de.mm20.launcher2.weather
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.MediatorLiveData
|
|
||||||
import androidx.work.*
|
import androidx.work.*
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class WeatherRepository(
|
class WeatherRepository(
|
||||||
val context: Context
|
val context: Context,
|
||||||
|
val database: AppDatabase,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val forecasts = MediatorLiveData<List<DailyForecast>>()
|
val forecasts = database.weatherDao().getForecasts()
|
||||||
|
.map { it.map { Forecast(it) } }
|
||||||
init {
|
.map {
|
||||||
|
groupForecastsPerDay(it)
|
||||||
forecasts.addSource(AppDatabase.getInstance(context).weatherDao().getForecasts()) { entities ->
|
|
||||||
forecasts.value = sortDailyForecasts(entities.map { Forecast(it) })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val weatherRequest = PeriodicWorkRequest.Builder(WeatherUpdateWorker::class.java, 60, TimeUnit.MINUTES)
|
init {
|
||||||
|
val weatherRequest =
|
||||||
|
PeriodicWorkRequest.Builder(WeatherUpdateWorker::class.java, 60, TimeUnit.MINUTES)
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance().enqueueUniquePeriodicWork("weather",
|
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||||
ExistingPeriodicWorkPolicy.KEEP, weatherRequest)
|
"weather",
|
||||||
|
ExistingPeriodicWorkPolicy.KEEP, weatherRequest
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sortDailyForecasts(forecasts: List<Forecast>): List<DailyForecast> {
|
private fun groupForecastsPerDay(forecasts: List<Forecast>): List<DailyForecast> {
|
||||||
val dailyForecasts = mutableListOf<DailyForecast>()
|
val dailyForecasts = mutableListOf<DailyForecast>()
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
var currentDay = 0
|
var currentDay = 0
|
||||||
@ -35,12 +39,16 @@ class WeatherRepository(
|
|||||||
calendar.timeInMillis = fc.timestamp
|
calendar.timeInMillis = fc.timestamp
|
||||||
if (currentDay != calendar.get(Calendar.DAY_OF_YEAR)) {
|
if (currentDay != calendar.get(Calendar.DAY_OF_YEAR)) {
|
||||||
if (currentDayForecasts.isNotEmpty()) {
|
if (currentDayForecasts.isNotEmpty()) {
|
||||||
dailyForecasts.add(DailyForecast(
|
dailyForecasts.add(
|
||||||
|
DailyForecast(
|
||||||
timestamp = currentDayForecasts.first().timestamp,
|
timestamp = currentDayForecasts.first().timestamp,
|
||||||
minTemp = currentDayForecasts.minByOrNull { it.temperature }?.temperature ?: 0.0,
|
minTemp = currentDayForecasts.minByOrNull { it.temperature }?.temperature
|
||||||
maxTemp = currentDayForecasts.maxByOrNull { it.temperature }?.temperature ?: 0.0,
|
?: 0.0,
|
||||||
|
maxTemp = currentDayForecasts.maxByOrNull { it.temperature }?.temperature
|
||||||
|
?: 0.0,
|
||||||
hourlyForecasts = currentDayForecasts
|
hourlyForecasts = currentDayForecasts
|
||||||
))
|
)
|
||||||
|
)
|
||||||
currentDayForecasts = mutableListOf()
|
currentDayForecasts = mutableListOf()
|
||||||
}
|
}
|
||||||
currentDay = calendar.get(Calendar.DAY_OF_YEAR)
|
currentDay = calendar.get(Calendar.DAY_OF_YEAR)
|
||||||
@ -48,12 +56,16 @@ class WeatherRepository(
|
|||||||
currentDayForecasts.add(fc)
|
currentDayForecasts.add(fc)
|
||||||
}
|
}
|
||||||
if (currentDayForecasts.isNotEmpty()) {
|
if (currentDayForecasts.isNotEmpty()) {
|
||||||
dailyForecasts.add(DailyForecast(
|
dailyForecasts.add(
|
||||||
|
DailyForecast(
|
||||||
timestamp = currentDayForecasts.first().timestamp,
|
timestamp = currentDayForecasts.first().timestamp,
|
||||||
minTemp = currentDayForecasts.minByOrNull { it.temperature }?.temperature ?: 0.0,
|
minTemp = currentDayForecasts.minByOrNull { it.temperature }?.temperature
|
||||||
maxTemp = currentDayForecasts.maxByOrNull { it.temperature }?.temperature ?: 0.0,
|
?: 0.0,
|
||||||
|
maxTemp = currentDayForecasts.maxByOrNull { it.temperature }?.temperature
|
||||||
|
?: 0.0,
|
||||||
hourlyForecasts = currentDayForecasts
|
hourlyForecasts = currentDayForecasts
|
||||||
))
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return dailyForecasts
|
return dailyForecasts
|
||||||
}
|
}
|
||||||
@ -63,8 +75,8 @@ class WeatherRepository(
|
|||||||
val provider = WeatherProvider.getInstance(context) ?: return
|
val provider = WeatherProvider.getInstance(context) ?: return
|
||||||
if (provider.isUpdateRequired()) {
|
if (provider.isUpdateRequired()) {
|
||||||
val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java)
|
val weatherRequest = OneTimeWorkRequest.Builder(WeatherUpdateWorker::class.java)
|
||||||
.addTag("weather")
|
.addTag("weather")
|
||||||
.build()
|
.build()
|
||||||
WorkManager.getInstance(context).enqueue(weatherRequest)
|
WorkManager.getInstance(context).enqueue(weatherRequest)
|
||||||
} else {
|
} else {
|
||||||
Log.d("MM20", "No weather update required")
|
Log.d("MM20", "No weather update required")
|
||||||
@ -72,7 +84,8 @@ class WeatherRepository(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class WeatherUpdateWorker(val context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
|
class WeatherUpdateWorker(val context: Context, params: WorkerParameters) :
|
||||||
|
CoroutineWorker(context, params) {
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val provider = WeatherProvider.getInstance(context) ?: return Result.failure()
|
val provider = WeatherProvider.getInstance(context) ?: return Result.failure()
|
||||||
if (!provider.isAvailable()) return Result.failure()
|
if (!provider.isAvailable()) return Result.failure()
|
||||||
@ -83,7 +96,8 @@ class WeatherUpdateWorker(val context: Context, params: WorkerParameters) : Coro
|
|||||||
Result.retry()
|
Result.retry()
|
||||||
} else {
|
} else {
|
||||||
Log.d("MM20", "Weather update succeeded")
|
Log.d("MM20", "Weather update succeeded")
|
||||||
AppDatabase.getInstance(applicationContext).weatherDao().replaceAll(weatherData.map { it.toDatabaseEntity() })
|
AppDatabase.getInstance(applicationContext).weatherDao()
|
||||||
|
.replaceAll(weatherData.map { it.toDatabaseEntity() })
|
||||||
Result.success()
|
Result.success()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,19 +4,17 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class WeatherViewModel(application: Application) : AndroidViewModel(application) {
|
class WeatherViewModel(application: Application) : AndroidViewModel(application), KoinComponent {
|
||||||
|
|
||||||
private val repository = WeatherRepository(application)
|
private val repository : WeatherRepository by inject()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
requestUpdate(application)
|
requestUpdate(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
val forecasts: LiveData<List<DailyForecast>> by lazy {
|
|
||||||
repository.forecasts
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requestUpdate(context: Context) {
|
fun requestUpdate(context: Context) {
|
||||||
repository.requestUpdate(context)
|
repository.requestUpdate(context)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user