feat: integrate city selection into dashboard, nav and settings
This commit is contained in:
@@ -1,17 +1,26 @@
|
||||
package org.db3.airmq.features.dashboard
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -20,6 +29,7 @@ import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -34,8 +44,6 @@ import org.db3.airmq.ui.theme.LegacyNavGradientStart
|
||||
|
||||
@Composable
|
||||
fun DashboardScreen(
|
||||
onOpenMap: () -> Unit,
|
||||
onOpenManage: () -> Unit,
|
||||
onOpenCity: () -> Unit,
|
||||
onOpenDevice: () -> Unit,
|
||||
onOpenNews: () -> Unit,
|
||||
@@ -43,6 +51,17 @@ fun DashboardScreen(
|
||||
viewModel: DashboardViewModel = hiltViewModel()
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.actions.collect { action ->
|
||||
when (action) {
|
||||
DashboardScreenContract.Action.OpenCity -> onOpenCity()
|
||||
DashboardScreenContract.Action.OpenNews -> { /* handled elsewhere */ }
|
||||
DashboardScreenContract.Action.OpenWidgetConstructor -> { /* handled elsewhere */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DashboardContent(
|
||||
state = state,
|
||||
onEvent = viewModel::onEvent
|
||||
@@ -69,7 +88,13 @@ internal fun DashboardContent(
|
||||
)
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
CitySelector(city = state.city, modifier = Modifier.padding(top = 12.dp))
|
||||
CitySelector(
|
||||
city = state.city,
|
||||
modifier = Modifier
|
||||
.padding(top = 12.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
onClick = { onEvent(DashboardScreenContract.Event.CitySelectorClicked) }
|
||||
)
|
||||
MetricGaugePager(
|
||||
selectedSensor = state.selectedSensor,
|
||||
values = state.gaugeValues,
|
||||
@@ -100,20 +125,60 @@ internal fun DashboardContent(
|
||||
@Composable
|
||||
private fun CitySelector(
|
||||
city: String,
|
||||
modifier: Modifier = Modifier
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(44.dp),
|
||||
.height(44.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickable(onClick = onClick),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = city,
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(44.dp)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.city_left),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.width(22.dp)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(44.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.city_middle),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.FillBounds
|
||||
)
|
||||
Text(
|
||||
text = city,
|
||||
color = Color.White,
|
||||
fontSize = 22.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.city_right),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.width(22.dp)
|
||||
.fillMaxHeight()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,6 +80,7 @@ object DashboardScreenContract {
|
||||
}
|
||||
|
||||
sealed interface Event {
|
||||
data object CitySelectorClicked : Event
|
||||
data class GaugeSelected(val sensor: SensorType) : Event
|
||||
data class PageChanged(val page: Int) : Event
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.db3.airmq.features.dashboard
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -10,8 +11,12 @@ import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import org.db3.airmq.sdk.city.CityService
|
||||
import org.db3.airmq.R
|
||||
import org.db3.airmq.features.common.chart.ChartConfig
|
||||
import org.db3.airmq.features.common.chart.ChartDataset
|
||||
@@ -23,17 +28,27 @@ import androidx.compose.ui.graphics.Color
|
||||
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
@ApplicationContext private val context: Context,
|
||||
private val cityService: CityService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(initialState())
|
||||
val uiState: StateFlow<DashboardScreenContract.State> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
cityService.observeSelectedCity().collect { city ->
|
||||
_uiState.update { it.copy(city = city) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val _actions = MutableSharedFlow<DashboardScreenContract.Action>(extraBufferCapacity = 1)
|
||||
val actions: SharedFlow<DashboardScreenContract.Action> = _actions.asSharedFlow()
|
||||
|
||||
fun onEvent(event: DashboardScreenContract.Event) {
|
||||
when (event) {
|
||||
DashboardScreenContract.Event.CitySelectorClicked -> _actions.tryEmit(DashboardScreenContract.Action.OpenCity)
|
||||
is DashboardScreenContract.Event.GaugeSelected -> {
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
|
||||
@@ -146,8 +146,6 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) {
|
||||
}
|
||||
composable(AirMqRoutes.DASHBOARD) {
|
||||
DashboardScreen(
|
||||
onOpenMap = { navController.navigate(AirMqRoutes.MAP) },
|
||||
onOpenManage = { navController.navigate(AirMqRoutes.MANAGE) },
|
||||
onOpenCity = { navController.navigate(AirMqRoutes.CITY) },
|
||||
onOpenDevice = { navController.navigate(AirMqRoutes.device()) },
|
||||
onOpenNews = { navController.navigate(AirMqRoutes.NEWS) },
|
||||
@@ -181,13 +179,19 @@ fun AirMQNavGraph(modifier: Modifier = Modifier) {
|
||||
DeviceSettingsScreen(
|
||||
deviceId = backStackEntry.arguments?.getString("deviceId") ?: "mock-device-id",
|
||||
onOpenLocation = { navController.navigate(AirMqRoutes.LOCATION) },
|
||||
onShowOnMap = { navController.navigate(AirMqRoutes.MAP) },
|
||||
onShowOnMap = {
|
||||
navController.navigate(AirMqRoutes.MAP) {
|
||||
popUpTo(AirMqRoutes.MANAGE) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
},
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(AirMqRoutes.CITY) {
|
||||
CityScreen(
|
||||
onBackToDashboard = { navController.navigate(AirMqRoutes.DASHBOARD) }
|
||||
onNavigateBack = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(AirMqRoutes.SETTINGS) {
|
||||
|
||||
@@ -56,6 +56,7 @@ fun SettingsScreen(
|
||||
viewModel.actions.collectLatest { action ->
|
||||
when (action) {
|
||||
Action.OpenDebug -> onOpenDebug()
|
||||
Action.OpenCity -> onOpenCity()
|
||||
Action.LogOutToManage -> onLogOutToManage()
|
||||
is Action.ShowMessage -> Toast.makeText(context, action.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
@@ -178,7 +179,7 @@ private fun PreferenceRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 44.dp)
|
||||
.heightIn(min = 64.dp)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -221,7 +222,7 @@ private fun PreferenceCheckRow(
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = 44.dp)
|
||||
.heightIn(min = 64.dp)
|
||||
.clickable { onToggle(!checked) }
|
||||
.padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
||||
@@ -12,6 +12,7 @@ object SettingsScreenContract {
|
||||
|
||||
sealed interface Action {
|
||||
data object OpenDebug : Action
|
||||
data object OpenCity : Action
|
||||
data object LogOutToManage : Action
|
||||
data class ShowMessage(val message: String) : Action
|
||||
}
|
||||
|
||||
@@ -19,13 +19,15 @@ import org.db3.airmq.features.settings.SettingsScreenContract.Action
|
||||
import org.db3.airmq.features.settings.SettingsScreenContract.Event
|
||||
import org.db3.airmq.features.settings.SettingsScreenContract.State
|
||||
import org.db3.airmq.sdk.auth.AuthService
|
||||
import org.db3.airmq.sdk.city.CityService
|
||||
import org.db3.airmq.sdk.settings.SettingsService
|
||||
|
||||
@HiltViewModel
|
||||
class SettingsViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val settingsService: SettingsService,
|
||||
private val authService: AuthService
|
||||
private val authService: AuthService,
|
||||
private val cityService: CityService
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(State())
|
||||
@@ -40,7 +42,7 @@ class SettingsViewModel @Inject constructor(
|
||||
|
||||
fun onEvent(event: Event) {
|
||||
when (event) {
|
||||
Event.CityClicked -> _actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon)))
|
||||
Event.CityClicked -> _actions.tryEmit(Action.OpenCity)
|
||||
Event.AboutClicked -> _actions.tryEmit(Action.ShowMessage(appContext.getString(R.string.coming_soon)))
|
||||
Event.DebugClicked -> _actions.tryEmit(Action.OpenDebug)
|
||||
Event.LogOutClicked -> logOut()
|
||||
@@ -68,7 +70,7 @@ class SettingsViewModel @Inject constructor(
|
||||
private fun loadSettings() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val session = authService.getUser()
|
||||
val city = settingsService.getCity() ?: appContext.getString(R.string.city_minsk)
|
||||
val city = cityService.getSelectedCity()
|
||||
val deviceStatus = settingsService.getDeviceStatusNotificationsEnabled()
|
||||
val offlineVisible = settingsService.getOfflineDevicesVisible()
|
||||
val advanced = settingsService.getAdvancedEnabled()
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
<string name="text_no_city_hint"><u>Няма вашага горада?</u></string>
|
||||
<string name="text_wrong_city_hint"><u>Няправільны горад?</u></string>
|
||||
<string name="text_city_warning">Гэтая опцыя вызначае, якія дадзеныя будуць адлюстроўвацца ў дашбордзе. Калі горад не выбраны, будзе ўсталяваны стандартны варыянт.</string>
|
||||
<string name="city_list_unavailable">Спіс гарадоў недаступны. Выкарыстоўваецца горад па змаўчанні.</string>
|
||||
<string name="text_no_city">"Калі вы не бачыце ваш горад, значыць, у ім няма прылад "</string>
|
||||
<string name="button_enter_manual">Увесці ўручную</string>
|
||||
<string name="text_welcome">Вітаем вас у AirMQ</string>
|
||||
|
||||
@@ -190,6 +190,7 @@
|
||||
<string name="text_no_city_hint"><u>Нет вашего города?</u></string>
|
||||
<string name="text_wrong_city_hint"><u>Неправильный город?</u></string>
|
||||
<string name="text_city_warning">Эта опция определяет, какие данные будут отображаться в дашборде. Если город не выбран, установится стандартный вариант.</string>
|
||||
<string name="city_list_unavailable">Список городов недоступен. Используется город по умолчанию.</string>
|
||||
<string name="text_no_city">"Если вы не видете ваш город, значит в нем нет устройств "</string>
|
||||
<string name="button_enter_manual">Ввести вручную</string>
|
||||
<string name="text_welcome">Добро пожаловать в AirMQ</string>
|
||||
|
||||
@@ -250,6 +250,7 @@
|
||||
<string name="text_wrong_city_hint"><u>Wrong city?</u></string>
|
||||
<string name="text_no_city">If you don’t see your city, there are not enough sensors there to provide reliable data just yet. Instead, data from the closest available city will be displayed when automatic detection is enabled</string>
|
||||
<string name="text_city_warning">This setting affects what data is displayed in the dashboard. If no city is selected, default value will be set.</string>
|
||||
<string name="city_list_unavailable">City list unavailable. Using default city.</string>
|
||||
<string name="button_enter_manual">Enter manually</string>
|
||||
<string name="text_welcome">Welcome to AirMQ</string>
|
||||
<string name="text_welcome_second">Learn what your city breathes</string>
|
||||
|
||||
Reference in New Issue
Block a user