Merge pull request 'new-sensors-screen' (#6) from new-sensors-screen into master

Reviewed-on: nm17/narodmon#6
Reviewed-by: nm17 <nm17@riseup.net>
This commit is contained in:
nm17 2023-06-18 13:23:06 +03:00
commit 7e5673f96e
14 changed files with 663 additions and 415 deletions

View file

@ -1,4 +1,5 @@
@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class, @file:OptIn(
ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class,
ExperimentalMaterial3Api::class ExperimentalMaterial3Api::class
) )
@ -7,12 +8,20 @@ package ru.nm17.narodmon
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -24,8 +33,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
@ -36,8 +46,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import ru.nm17.narodmon.db.AppDatabase import ru.nm17.narodmon.db.AppDatabase
import ru.nm17.narodmon.db.entities.KVSetting import ru.nm17.narodmon.db.entities.KVSetting
import ru.nm17.narodmon.ui.elements.AgreementDialog import ru.nm17.narodmon.ui.dialogs.AgreementDialog
import ru.nm17.narodmon.ui.pages.SensorsPage import ru.nm17.narodmon.ui.sensorsScreen.SensorsScreen
import ru.nm17.narodmon.ui.theme.NarodMonTheme import ru.nm17.narodmon.ui.theme.NarodMonTheme
@ -46,18 +56,39 @@ import ru.nm17.narodmon.ui.theme.NarodMonTheme
fun AppNavHost() { fun AppNavHost() {
val navController = rememberNavController() val navController = rememberNavController()
val coScope = rememberCoroutineScope() val coScope = rememberCoroutineScope()
NavHost(navController = navController, startDestination = "sensors") { NavHost(navController = navController, startDestination = "sensors") {
composable("agreement") { composable("agreement") {
} }
composable("sensors") { composable("sensors") {
SensorsPage(navController) Scaffold(bottomBar = {
} BottomAppBar(actions = {
Image(
Icons.Rounded.Menu,
contentDescription = null
)
}, floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
Image(
Icons.Rounded.Add,
contentDescription = ""
)
}
},
contentPadding = PaddingValues(start = 16.dp)
)
}) {
Column(modifier = Modifier.padding(it)) {
SensorsScreen(navController)
}
}
/*...*/ }
} }
/*...*/
} }
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -69,20 +100,20 @@ class MainActivity : ComponentActivity() {
AppDatabase::class.java, "data" AppDatabase::class.java, "data"
).build() ).build()
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) // val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
//
val sharedPreferences = EncryptedSharedPreferences.create( // val sharedPreferences = EncryptedSharedPreferences.create(
"secret_shared_prefs", // "secret_shared_prefs",
masterKeyAlias, // masterKeyAlias,
createDeviceProtectedStorageContext(), // createDeviceProtectedStorageContext(),
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, // EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM // EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
) // )
// use the shared preferences and editor as you normally would // use the shared preferences and editor as you normally would
// use the shared preferences and editor as you normally would // use the shared preferences and editor as you normally would
val credSharedPreferences = sharedPreferences // val credSharedPreferences = sharedPreferences
setContent { setContent {
@ -120,9 +151,9 @@ class MainActivity : ComponentActivity() {
Text(text = stringResource(R.string.waiting_for_user_agreement)) Text(text = stringResource(R.string.waiting_for_user_agreement))
} }
} }
} else { } else {
AppNavHost() AppNavHost()
} }
// A surface container using the 'background' color from the theme // A surface container using the 'background' color from the theme

View file

@ -0,0 +1,9 @@
package ru.nm17.narodmon.ui
fun String.toChipTitle(): String {
return if (length >= 20) {
this.slice(0..16) + ".."
} else {
this
}
}

View file

@ -1,97 +0,0 @@
package ru.nm17.narodmon.ui.bottomSheets
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.nm17.narodmon.R
import ru.nm17.narodmon.ui.entities.SensorSortingUiEntity
import ru.nm17.narodmon.ui.entities.SortingTypes
import ru.nm17.narodmon.ui.pages.FilterRadioButton
import ru.nm17.narodmon.ui.theme.NarodMonTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SortSensorsBottomSheet(
onApply: (sortingType: SortingTypes) -> Unit,
onDismissRequest: () -> Unit
) {
var sortingType by remember { mutableStateOf(SortingTypes.DISTANCE) }
val sortingTypes = remember {
listOf(
SensorSortingUiEntity(R.string.sort_by_name, SortingTypes.NAME),
SensorSortingUiEntity(R.string.sort_by_name_desc, SortingTypes.NAME_DESC),
SensorSortingUiEntity(R.string.sort_by_distance, SortingTypes.DISTANCE),
SensorSortingUiEntity(R.string.sort_by_distance_desc, SortingTypes.DISTANCE_DESC),
SensorSortingUiEntity(R.string.sort_by_type, SortingTypes.TYPE),
SensorSortingUiEntity(R.string.sort_by_type_desc, SortingTypes.TYPE_DESC),
SensorSortingUiEntity(R.string.sort_update_time, SortingTypes.UPD_TIME),
)
}
ModalBottomSheet(
onDismissRequest = { onDismissRequest.invoke() },
sheetState = SheetState(true)
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(R.string.sensors_sort_title),
fontSize = 24.sp,
fontWeight = FontWeight(500),
)
}
LazyColumn(
modifier = Modifier
.padding(horizontal = 4.dp)
.fillMaxWidth(),
) {
items(sortingTypes) {
FilterRadioButton(
selected = (sortingType == it.sortingType),
onClick = { sortingType = it.sortingType },
stringRes = it.stringRes,
)
}
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 64.dp, vertical = 8.dp),
onClick = {
onApply.invoke(sortingType)
}) {
Text(text = stringResource(R.string.apply))
}
}
}
@Preview
@Composable
fun PreviewSortBottomSheet() {
NarodMonTheme {
SortSensorsBottomSheet(onApply = {}) {}
}
}

View file

@ -1,6 +1,6 @@
@file:OptIn(ExperimentalMaterial3Api::class) @file:OptIn(ExperimentalMaterial3Api::class)
package ru.nm17.narodmon.ui.elements package ru.nm17.narodmon.ui.dialogs
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -20,7 +20,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.tooling.preview.Preview
import ru.nm17.narodmon.R import ru.nm17.narodmon.R
import ru.nm17.narodmon.ui.theme.NarodMonTheme
import kotlin.system.exitProcess import kotlin.system.exitProcess
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -84,3 +86,13 @@ fun AgreementDialog(modifier: Modifier = Modifier, onClick: () -> Unit) {
) )
} }
@Preview
@Composable
fun PreviewAgreementDialog(){
NarodMonTheme {
AgreementDialog {
}
}
}

View file

@ -1,37 +1,37 @@
package ru.nm17.narodmon.ui.bottomSheets package ru.nm17.narodmon.ui.dialogs
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState import androidx.compose.material3.Surface
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.nm17.narodmon.R import ru.nm17.narodmon.R
import ru.nm17.narodmon.ui.elements.FilterCheckbox
import ru.nm17.narodmon.ui.entities.SensorFilterUiEntity import ru.nm17.narodmon.ui.entities.SensorFilterUiEntity
import ru.nm17.narodmon.ui.pages.FilterCheckbox
import ru.nm17.narodmon.ui.theme.NarodMonTheme import ru.nm17.narodmon.ui.theme.NarodMonTheme
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun FilterSensorsBottomSheet( fun FilterSensorsDialog(
onApply: (filters: List<SensorFilterUiEntity>) -> Unit, onApply: (filters: List<SensorFilterUiEntity>) -> Unit,
onDismissRequest: () -> Unit onDismissRequest: () -> Unit
) { ) {
@ -67,53 +67,61 @@ fun FilterSensorsBottomSheet(
) )
} }
ModalBottomSheet(
onDismissRequest = { onDismissRequest.invoke() },
sheetState = SheetState(true)
) {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
Text( AlertDialog(
text = stringResource(R.string.sensors_filter_title), onDismissRequest = {
fontSize = 24.sp, onDismissRequest.invoke()
fontWeight = FontWeight(500), }) {
) Surface(
} shape = MaterialTheme.shapes.large,
Box(contentAlignment = Alignment.BottomCenter) { tonalElevation = AlertDialogDefaults.TonalElevation
LazyColumn( ) {
modifier = Modifier Column(modifier = Modifier.padding(vertical = 16.dp)) {
.padding(horizontal = 4.dp) Text(
.fillMaxWidth(), text = stringResource(id = R.string.sensors_filter_title),
) { style = MaterialTheme.typography.titleLarge,
items(filterItems) { textAlign = TextAlign.Center,
FilterCheckbox( modifier = Modifier
checked = it.enabled.value, .padding(horizontal = 16.dp)
stringRes = it.stringRes, .fillMaxWidth()
) { it.enabled.value = !it.enabled.value } )
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(128.dp, 312.dp)
.padding(horizontal = 4.dp),
) {
items(filterItems) {
FilterCheckbox(
checked = it.enabled.value,
stringRes = it.stringRes,
) { it.enabled.value = !it.enabled.value }
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onDismissRequest.invoke() }) {
Text(text = "Отмена")
}
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
Button(onClick = { onApply.invoke(filterItems) }) {
Text(text = stringResource(id = R.string.apply))
}
} }
} }
Button(
onClick = { onApply.invoke(filterItems) },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp, horizontal = 64.dp)
) {
Text(text = stringResource(id = R.string.apply))
}
}
}
} }
} }
@Preview @Preview
@Composable @Composable
fun PreviewSensorFilterBottomSheet() { fun PreviewFilterSensorsDialog() {
NarodMonTheme { NarodMonTheme {
FilterSensorsBottomSheet({}) { FilterSensorsDialog({}) {}
}
} }
} }

View file

@ -0,0 +1,122 @@
package ru.nm17.narodmon.ui.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import ru.nm17.narodmon.R
import ru.nm17.narodmon.ui.elements.FilterRadioButton
import ru.nm17.narodmon.ui.entities.SensorSortingUiEntity
import ru.nm17.narodmon.ui.entities.SortingTypes
import ru.nm17.narodmon.ui.theme.NarodMonTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SortSensorsDialog(
selected: SensorSortingUiEntity,
sortingTypes: List<SensorSortingUiEntity>? = null,
onApply: (sortingType: SensorSortingUiEntity) -> Unit,
onDismissRequest: () -> Unit
) {
var selectedType by remember {
mutableStateOf(selected)
}
val sensorsSortingTypes = listOf(
SensorSortingUiEntity(R.string.sort_by_name, SortingTypes.NAME),
SensorSortingUiEntity(R.string.sort_by_name_desc, SortingTypes.NAME_DESC),
SensorSortingUiEntity(R.string.sort_by_distance, SortingTypes.DISTANCE),
SensorSortingUiEntity(R.string.sort_by_distance_desc, SortingTypes.DISTANCE_DESC),
SensorSortingUiEntity(R.string.sort_by_type, SortingTypes.TYPE),
SensorSortingUiEntity(R.string.sort_by_type_desc, SortingTypes.TYPE_DESC),
SensorSortingUiEntity(R.string.sort_update_time, SortingTypes.UPD_TIME),
)
AlertDialog(
onDismissRequest = {
onDismissRequest.invoke()
}) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = MaterialTheme.shapes.large,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(modifier = Modifier.padding(vertical = 16.dp)) {
Text(
text = stringResource(id = R.string.sensors_sort_title),
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth()
)
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
) {
items(sortingTypes ?: sensorsSortingTypes) {
FilterRadioButton(
selected = (selectedType == it),
onClick = { selectedType = it },
stringRes = it.stringRes,
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.End
) {
TextButton(onClick = { onDismissRequest.invoke() }) {
Text(text = "Отмена")
}
Spacer(modifier = Modifier.padding(horizontal = 8.dp))
Button(onClick = { onApply.invoke(selectedType) }) {
Text(text = stringResource(id = R.string.apply))
}
}
}
}
}
}
@Preview
@Composable
fun SortSensorsPreview() {
NarodMonTheme {
SortSensorsDialog(
SensorSortingUiEntity(R.string.sort_by_distance, SortingTypes.DISTANCE),
onApply = {}) {}
}
}

View file

@ -0,0 +1,31 @@
package ru.nm17.narodmon.ui.elements
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ExperimentalMaterial3Api
@Composable
fun FilterCheckbox(checked: Boolean, stringRes: Int, onCheckedChange: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onCheckedChange() }
) {
Checkbox(
checked = checked,
onCheckedChange = { onCheckedChange() },
)
Text(
text = stringResource(id = stringRes),
)
}
}

View file

@ -0,0 +1,29 @@
package ru.nm17.narodmon.ui.elements
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
@ExperimentalMaterial3Api
@Composable
fun FilterRadioButton(selected: Boolean, onClick: () -> Unit, stringRes: Int) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onClick.invoke() }
) {
RadioButton(
selected = selected,
onClick = onClick,
)
Text(text = stringResource(id = stringRes))
}
}

View file

@ -0,0 +1,56 @@
package ru.nm17.narodmon.ui.elements
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ru.nm17.narodmon.ui.entities.SensorEntity
import ru.nm17.narodmon.ui.iosevkaFamily
@ExperimentalMaterial3Api
@Composable
fun SensorItem(sensorEntity: SensorEntity) {
ListItem(
overlineContent = { Text(text = "${sensorEntity.deviceName} от ${sensorEntity.deviceOwner}") },
headlineContent = { Text(text = sensorEntity.type.name) },
supportingContent = { Text(text = sensorEntity.name) },
trailingContent = {
Column(
horizontalAlignment = Alignment.End,
) {
Text(text = "${sensorEntity.distance} km")
Spacer(modifier = Modifier.size(2.dp))
ElevatedCard(
shape = RectangleShape,
) {
Text(
text = "${sensorEntity.value} ${sensorEntity.unit}",
fontFamily = iosevkaFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 3.dp, vertical = 1.dp)
)
}
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
fun PreviewSensorItem() {
// SensorItem(SensorEntity(0, Se))
}

View file

@ -13,6 +13,7 @@ import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.jvm.javaio.toInputStream import io.ktor.utils.io.jvm.javaio.toInputStream
import ovh.plrapps.mapcompose.api.addLayer import ovh.plrapps.mapcompose.api.addLayer
import ovh.plrapps.mapcompose.api.onTouchDown
import ovh.plrapps.mapcompose.api.scale import ovh.plrapps.mapcompose.api.scale
import ovh.plrapps.mapcompose.api.setScroll import ovh.plrapps.mapcompose.api.setScroll
import ovh.plrapps.mapcompose.core.TileStreamProvider import ovh.plrapps.mapcompose.core.TileStreamProvider
@ -33,7 +34,7 @@ suspend fun requestTile(row: Int, col: Int, zoom: Int): InputStream {
} }
@Composable @Composable
fun TileMap(modifier: Modifier = Modifier) { fun TileMap(modifier: Modifier = Modifier, onTap: () -> Unit) {
val state by remember { val state by remember {
mutableStateOf( mutableStateOf(
MapState( MapState(
@ -43,6 +44,9 @@ fun TileMap(modifier: Modifier = Modifier) {
workerCount = 16, workerCount = 16,
).apply { ).apply {
addLayer(tileStreamProvider) addLayer(tileStreamProvider)
onTouchDown {
onTap.invoke()
}
} }
) )
} }

View file

@ -1,247 +0,0 @@
package ru.nm17.narodmon.ui.pages
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Divider
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.RadioButton
import androidx.compose.material3.SearchBar
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import ru.nm17.narodmon.R
import ru.nm17.narodmon.db.entities.SensorType
import ru.nm17.narodmon.ui.bottomSheets.FilterSensorsBottomSheet
import ru.nm17.narodmon.ui.bottomSheets.SortSensorsBottomSheet
import ru.nm17.narodmon.ui.elements.GenericNavScaffold
import ru.nm17.narodmon.ui.elements.TileMap
import ru.nm17.narodmon.ui.entities.SensorEntity
import ru.nm17.narodmon.ui.entities.SensorFilterUiEntity
import ru.nm17.narodmon.ui.entities.SortingTypes
import ru.nm17.narodmon.ui.iosevkaFamily
@ExperimentalMaterial3Api
@Composable
fun SensorsPage(navController: NavController) {
var searchQuery by remember { mutableStateOf("") }
var searchActive by remember { mutableStateOf(false) }
var sortingShow by remember { mutableStateOf(false) }
var sortingType by remember { mutableStateOf(SortingTypes.DISTANCE) }
var filterShow by remember { mutableStateOf(false) }
var filterMine by remember { mutableStateOf(false) }
val sensorEntities = remember {
mutableListOf(
// TODO: загружать датчики с сервера. Этот список -- для макета
SensorEntity(
0,
SensorType(0, "temp", "C"),
"device0", 0,
"sensor0", favorite = true,
public = true, mine = false,
"Москва", 0.4,
20.0, "C", 1686142800,
),
SensorEntity(
1,
SensorType(4, "humidity", "%"),
"device1", 0,
"sensor1", favorite = true,
public = false, mine = false,
"Подмосковье", 1.1,
39.0, "%", 1686142800,
),
SensorEntity(
2,
SensorType(11, "wind speed", "m/s"),
"device2", 1,
"sensor2", favorite = false,
public = true, mine = true,
"Москва", 0.01,
3.2, "m/s", 1686142800,
),
)
}
val scrConfig = LocalConfiguration.current
val mapHeight = scrConfig.screenHeightDp / 3
GenericNavScaffold(
title = { Text(text = stringResource(R.string.sensors_page_title)) }
) {
Column(modifier = Modifier.padding(it)) {
TileMap(modifier = Modifier.height(mapHeight.dp))
SearchBar(
query = searchQuery,
active = searchActive,
onActiveChange = { active -> searchActive = active },
onQueryChange = { query -> searchQuery = query },
onSearch = { searchActive = false },
placeholder = { Text(stringResource(R.string.search)) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = if (!searchActive) 8.dp else 0.dp)
) {}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 8.dp),
) {
AssistChip(
onClick = { filterShow = true },
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = stringResource(id = R.string.sensors_filter)
)
},
label = { Text(text = stringResource(R.string.sensors_filter)) },
)
AssistChip(
onClick = { sortingShow = true },
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_sort),
contentDescription = stringResource(id = R.string.sensors_sorting)
)
},
label = { Text(text = stringResource(R.string.sensors_sorting)) },
)
FilterChip(
selected = filterMine,
onClick = { filterMine = !filterMine },
label = { Text(text = stringResource(R.string.sensors_mine)) },
)
}
Divider()
LazyColumn(
modifier = Modifier.fillMaxHeight(),
) {
items(sensorEntities) { sensor ->
SensorItem(sensor)
}
}
}
}
if (filterShow) {
FilterSensorsBottomSheet(
onApply = {
// TODO применение фильтров
filterShow = false
},
onDismissRequest = { filterShow = false }
)
}
if (sortingShow) {
SortSensorsBottomSheet(
onApply = { s ->
sortingType = s
sortingShow = false
},
onDismissRequest = { sortingShow = false })
}
}
@ExperimentalMaterial3Api
@Composable
fun SensorItem(sensorEntity: SensorEntity) {
ListItem(
overlineContent = { Text(text = "${sensorEntity.deviceName} от ${sensorEntity.deviceOwner}") },
headlineContent = { Text(text = sensorEntity.type.name) },
supportingContent = { Text(text = sensorEntity.name) },
trailingContent = {
Column(
horizontalAlignment = Alignment.End,
) {
Text(text = "${sensorEntity.distance} km")
ElevatedCard(
shape = RectangleShape,
) {
Text(
text = "${sensorEntity.value} ${sensorEntity.unit}",
fontFamily = iosevkaFamily,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
modifier = Modifier.padding(horizontal = 3.dp, vertical = 1.dp)
)
}
}
}
)
}
@ExperimentalMaterial3Api
@Composable
fun FilterCheckbox(checked: Boolean, stringRes: Int, onCheckedChange: () -> Unit) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onCheckedChange() }
) {
Checkbox(
checked = checked,
onCheckedChange = { onCheckedChange() },
)
Text(
text = stringResource(id = stringRes),
)
}
}
@ExperimentalMaterial3Api
@Composable
fun FilterRadioButton(selected: Boolean, onClick: () -> Unit, stringRes: Int) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onClick.invoke() }
) {
RadioButton(
selected = selected,
onClick = onClick,
)
Text(text = stringResource(id = stringRes))
}
}

View file

@ -0,0 +1,282 @@
package ru.nm17.narodmon.ui.sensorsScreen
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDropDown
import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material3.BottomSheetScaffold
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SearchBar
import androidx.compose.material3.SearchBarDefaults
import androidx.compose.material3.SheetValue
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import ovh.plrapps.mapcompose.core.debounce
import ru.nm17.narodmon.R
import ru.nm17.narodmon.db.entities.SensorType
import ru.nm17.narodmon.ui.dialogs.FilterSensorsDialog
import ru.nm17.narodmon.ui.elements.SensorItem
import ru.nm17.narodmon.ui.dialogs.SortSensorsDialog
import ru.nm17.narodmon.ui.elements.TileMap
import ru.nm17.narodmon.ui.entities.SensorEntity
import ru.nm17.narodmon.ui.entities.SensorSortingUiEntity
import ru.nm17.narodmon.ui.entities.SortingTypes
import ru.nm17.narodmon.ui.theme.NarodMonTheme
import ru.nm17.narodmon.ui.toChipTitle
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun SensorsScreen(navController: NavController) {
val coroutineScope = rememberCoroutineScope()
var searchQuery by remember { mutableStateOf("") }
var searchActive by remember { mutableStateOf(false) }
val scaffoldState = rememberBottomSheetScaffoldState()
val sensorEntities = remember {
mutableListOf(
// TODO: загружать датчики с сервера. Этот список -- для макета
SensorEntity(
0,
SensorType(0, "temp", "C"),
"device0", 0,
"sensor0", favorite = true,
public = true, mine = false,
"Москва", 0.4,
20.0, "C", 1686142800,
),
SensorEntity(
1,
SensorType(4, "humidity", "%"),
"device1", 0,
"sensor1", favorite = true,
public = false, mine = false,
"Подмосковье", 1.1,
39.0, "%", 1686142800,
),
SensorEntity(
2,
SensorType(11, "wind speed", "m/s"),
"device2", 1,
"sensor2", favorite = false,
public = true, mine = true,
"Москва", 0.01,
3.2, "m/s", 1686142800,
),
)
}
var filterShow by remember { mutableStateOf(false) }
var filterMine by remember { mutableStateOf(false) }
var sortingShow by remember { mutableStateOf(false) }
var sortingType by remember {
mutableStateOf(
SensorSortingUiEntity(R.string.sort_by_distance, SortingTypes.DISTANCE)
)
}
var sheetHeight by remember {
mutableStateOf(SheetHeight.ExtraExpanded)
}
BottomSheetScaffold(
modifier = Modifier.fillMaxSize(),
sheetPeekHeight = when (sheetHeight) {
SheetHeight.ExtraExpanded -> 256.dp
SheetHeight.Expanded -> 128.dp
SheetHeight.Hidden -> 0.dp
},
scaffoldState = scaffoldState,
sheetContent = {
AnimatedVisibility(visible = scaffoldState.bottomSheetState.currentValue == SheetValue.Expanded) {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text(stringResource(R.string.search)) },
shape = SearchBarDefaults.inputFieldShape,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp)
)
}
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
) {
item {
FilterChip(
selected = false,
onClick = { filterShow = true },
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = stringResource(id = R.string.sensors_filter)
)
},
trailingIcon = {
// Icon(
// Icons.Filled.ArrowDropDown,
// "",
// tint = MaterialTheme.colorScheme.onBackground
// )
},
label = { Text(text = stringResource(R.string.sensors_filter)) },
)
}
item {
FilterChip(
selected = sortingType.sortingType != SortingTypes.DISTANCE,
onClick = { sortingShow = true },
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_sort),
contentDescription = stringResource(id = R.string.sensors_sorting)
)
},
trailingIcon = {
// Icon(
// Icons.Filled.ArrowDropDown,
// "",
// tint = MaterialTheme.colorScheme.onBackground
// )
},
label = {
Text(
text = stringResource(
if (sortingType.sortingType == SortingTypes.DISTANCE) R.string.sensors_sorting
else sortingType.stringRes
).toChipTitle(),
)
}
)
}
item {
FilterChip(
selected = filterMine,
onClick = { filterMine = !filterMine },
leadingIcon = {
if (filterMine) {
Icon(
Icons.Rounded.Check,
contentDescription = ""
)
}
},
label = { Text(text = stringResource(R.string.sensors_mine)) },
)
}
}
LazyColumn(
modifier = Modifier.fillMaxHeight(),
) {
items(sensorEntities) { sensor ->
SensorItem(sensor)
}
}
}
) {
Box(modifier = Modifier.fillMaxSize()) {
SearchBar(
query = searchQuery,
active = searchActive,
onActiveChange = { active ->
searchActive = active
sheetHeight = if (active) SheetHeight.Hidden else SheetHeight.ExtraExpanded
},
onQueryChange = { query -> searchQuery = query },
onSearch = { searchActive = false },
placeholder = { Text(stringResource(R.string.search)) },
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = if (!searchActive) 8.dp else 0.dp,
vertical = if (!searchActive) 16.dp else 0.dp
)
) {}
TileMap(
modifier = Modifier
.fillMaxSize()
) {
sheetHeight =
SheetHeight.Expanded // TODO придумать, чтобы менялось на SheetHeight.ExtraExpanded после взаимодействия с картой
}
}
}
if (sortingShow) {
SortSensorsDialog(
sortingType,
onApply = {
sortingType = it
sortingShow = false
},
onDismissRequest = {
sortingShow = false
}
)
}
if (filterShow) {
FilterSensorsDialog(
onApply = {
// TODO применение фильтров
filterShow = false
},
onDismissRequest = { filterShow = false }
)
}
}
@Preview
@Composable
fun PreviewNewSensors() {
NarodMonTheme {
SensorsScreen(rememberNavController())
}
}

View file

@ -0,0 +1,7 @@
package ru.nm17.narodmon.ui.sensorsScreen
enum class SheetHeight {
ExtraExpanded,
Expanded,
Hidden
}

View file

@ -15,7 +15,7 @@
<string name="sensors_page_title">Сенсоры</string> <string name="sensors_page_title">Сенсоры</string>
<string name="waiting_for_user_agreement">Ожидаю соглашение пользователя</string> <string name="waiting_for_user_agreement">Ожидаю соглашение пользователя</string>
<string name="search">Поиск</string> <string name="search">Поиск</string>
<string name="sensors_filter">Фильтр</string> <string name="sensors_filter">Тип датчиков</string>
<string name="sensors_sorting">Сортировка</string> <string name="sensors_sorting">Сортировка</string>
<string name="sensors_mine">Мои датчики</string> <string name="sensors_mine">Мои датчики</string>
<string name="filter_temp_dew_point">Температура точки росы</string> <string name="filter_temp_dew_point">Температура точки росы</string>
@ -51,4 +51,5 @@
<string name="sort_update_time">По времени обновления</string> <string name="sort_update_time">По времени обновления</string>
<string name="apply">Применить</string> <string name="apply">Применить</string>
<string name="sort_by_distance_desc">От дальних к ближним</string> <string name="sort_by_distance_desc">От дальних к ближним</string>
<string name="cancel1">Отменить</string>
</resources> </resources>