Merge pull request 'dc09-sensors2' (#5) from dc09-sensors2 into master
Reviewed-on: nm17/narodmon#5 Reviewed-by: nm17 <nm17@riseup.net>
This commit is contained in:
commit
b1e5af538a
22 changed files with 734 additions and 125 deletions
|
@ -106,12 +106,6 @@ dependencies {
|
|||
// optional - Kotlin Extensions and Coroutines support for Room
|
||||
implementation("androidx.room:room-ktx:$room_version")
|
||||
|
||||
// optional - RxJava2 support for Room
|
||||
implementation("androidx.room:room-rxjava2:$room_version")
|
||||
|
||||
// optional - RxJava3 support for Room
|
||||
implementation("androidx.room:room-rxjava3:$room_version")
|
||||
|
||||
// optional - Guava support for Room, including Optional and ListenableFuture
|
||||
implementation("androidx.room:room-guava:$room_version")
|
||||
|
||||
|
@ -151,4 +145,8 @@ dependencies {
|
|||
|
||||
// Map Compose library
|
||||
implementation("ovh.plrapps:mapcompose:2.7.1")
|
||||
|
||||
// Glide
|
||||
implementation ("com.github.bumptech.glide:glide:4.14.2")
|
||||
implementation("com.github.bumptech.glide:compose:1.0.0-alpha.1")
|
||||
}
|
|
@ -41,6 +41,7 @@ import ru.nm17.narodmon.ui.pages.SensorsPage
|
|||
import ru.nm17.narodmon.ui.theme.NarodMonTheme
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppNavHost() {
|
||||
val navController = rememberNavController()
|
||||
|
@ -131,23 +132,7 @@ class MainActivity : ComponentActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = "Hello $name!",
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavHolderEl() {
|
||||
//NavHost(navController = NavHostController(N), graph =)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
NarodMonTheme {
|
||||
Greeting("Android")
|
||||
}
|
||||
}
|
10
app/src/main/java/ru/nm17/narodmon/ui/Font.kt
Normal file
10
app/src/main/java/ru/nm17/narodmon/ui/Font.kt
Normal file
|
@ -0,0 +1,10 @@
|
|||
package ru.nm17.narodmon.ui
|
||||
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import ru.nm17.narodmon.R
|
||||
|
||||
val iosevkaFamily = FontFamily(
|
||||
Font(R.font.iosevka, FontWeight.Medium)
|
||||
)
|
|
@ -0,0 +1,119 @@
|
|||
package ru.nm17.narodmon.ui.bottomSheets
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.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.SheetValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
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.SensorFilterUiEntity
|
||||
import ru.nm17.narodmon.ui.pages.FilterCheckbox
|
||||
import ru.nm17.narodmon.ui.theme.NarodMonTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun FilterSensorsBottomSheet(
|
||||
onApply: (filters: List<SensorFilterUiEntity>) -> Unit,
|
||||
onDismissRequest: () -> Unit
|
||||
) {
|
||||
val filterItems = remember {
|
||||
listOf(
|
||||
/* TODO:
|
||||
* Заменить `code` на настоящее значение
|
||||
* либо динамически его подгружать из ответа АПИ
|
||||
* (см. /appInit, ключ в жсоне: types.type) */
|
||||
SensorFilterUiEntity(R.string.filter_temp, 0),
|
||||
SensorFilterUiEntity(R.string.filter_temp_water, 1),
|
||||
SensorFilterUiEntity(R.string.filter_temp_ground, 2),
|
||||
SensorFilterUiEntity(R.string.filter_temp_dew_point, 3),
|
||||
SensorFilterUiEntity(R.string.filter_humidity, 4),
|
||||
SensorFilterUiEntity(R.string.filter_pressure, 5),
|
||||
SensorFilterUiEntity(R.string.filter_lightness, 6),
|
||||
SensorFilterUiEntity(R.string.filter_uv, 7),
|
||||
SensorFilterUiEntity(R.string.filter_radiation, 8),
|
||||
SensorFilterUiEntity(R.string.filter_rainfall, 9),
|
||||
SensorFilterUiEntity(R.string.filter_dust, 10),
|
||||
SensorFilterUiEntity(R.string.filter_wind_speed, 11),
|
||||
SensorFilterUiEntity(R.string.filter_wind_direction, 12),
|
||||
SensorFilterUiEntity(R.string.filter_concentration, 13),
|
||||
SensorFilterUiEntity(R.string.filter_power, 14),
|
||||
SensorFilterUiEntity(R.string.filter_voltage, 15),
|
||||
SensorFilterUiEntity(R.string.filter_amperage, 16),
|
||||
SensorFilterUiEntity(R.string.filter_energy, 17),
|
||||
SensorFilterUiEntity(R.string.filter_battery, 18),
|
||||
SensorFilterUiEntity(R.string.filter_rxtx, 19),
|
||||
SensorFilterUiEntity(R.string.filter_signal, 20),
|
||||
SensorFilterUiEntity(R.string.filter_water_meter, 21),
|
||||
SensorFilterUiEntity(R.string.filter_time, 22),
|
||||
)
|
||||
}
|
||||
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onDismissRequest.invoke() },
|
||||
sheetState = SheetState(true)
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.sensors_filter_title),
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight(500),
|
||||
)
|
||||
}
|
||||
Box(contentAlignment = Alignment.BottomCenter) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 4.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
items(filterItems) {
|
||||
FilterCheckbox(
|
||||
checked = it.enabled.value,
|
||||
stringRes = it.stringRes,
|
||||
) { it.enabled.value = !it.enabled.value }
|
||||
}
|
||||
}
|
||||
Button(
|
||||
onClick = { onApply.invoke(filterItems) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp, horizontal = 64.dp)
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.apply))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewSensorFilterBottomSheet() {
|
||||
NarodMonTheme {
|
||||
FilterSensorsBottomSheet({}) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
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 = {}) {}
|
||||
}
|
||||
}
|
57
app/src/main/java/ru/nm17/narodmon/ui/elements/TileMap.kt
Normal file
57
app/src/main/java/ru/nm17/narodmon/ui/elements/TileMap.kt
Normal file
|
@ -0,0 +1,57 @@
|
|||
package ru.nm17.narodmon.ui.elements
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.okhttp.OkHttp
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import ovh.plrapps.mapcompose.api.addLayer
|
||||
import ovh.plrapps.mapcompose.api.scale
|
||||
import ovh.plrapps.mapcompose.api.setScroll
|
||||
import ovh.plrapps.mapcompose.core.TileStreamProvider
|
||||
import ovh.plrapps.mapcompose.ui.MapUI
|
||||
import ovh.plrapps.mapcompose.ui.state.MapState
|
||||
import java.io.InputStream
|
||||
|
||||
const val mapSize = 32768
|
||||
|
||||
val client = HttpClient(OkHttp)
|
||||
val tileStreamProvider = TileStreamProvider { row, col, zoom ->
|
||||
requestTile(row, col, zoom)
|
||||
}
|
||||
|
||||
suspend fun requestTile(row: Int, col: Int, zoom: Int): InputStream {
|
||||
val response = client.get("https://tile.openstreetmap.org/${zoom}/${col}/${row}.png")
|
||||
return response.bodyAsChannel().toInputStream()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TileMap(modifier: Modifier = Modifier) {
|
||||
val state by remember {
|
||||
mutableStateOf(
|
||||
MapState(
|
||||
levelCount = 8,
|
||||
fullWidth = mapSize,
|
||||
fullHeight = mapSize,
|
||||
workerCount = 16,
|
||||
).apply {
|
||||
addLayer(tileStreamProvider)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LaunchedEffect(state) {
|
||||
// TODO: Подгружать сохранённую позицию
|
||||
state.setScroll(Offset(28702.6F, 14787.6F))
|
||||
state.scale = 1.4658884F
|
||||
}
|
||||
|
||||
MapUI(modifier = modifier, state = state)
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ru.nm17.narodmon.ui.entities
|
||||
|
||||
import ru.nm17.narodmon.db.entities.SensorType
|
||||
|
||||
|
||||
data class SensorEntity(
|
||||
// TODO: Вынести в отдельный класс, и явно не в директорию `ui`
|
||||
val id: Int,
|
||||
val type: SensorType,
|
||||
val deviceName: String,
|
||||
val deviceOwner: Int,
|
||||
val name: String,
|
||||
val favorite: Boolean,
|
||||
val public: Boolean,
|
||||
val mine: Boolean,
|
||||
val location: String,
|
||||
val distance: Double, // километры
|
||||
val value: Double,
|
||||
val unit: String,
|
||||
val changed: Int,
|
||||
)
|
|
@ -0,0 +1,11 @@
|
|||
package ru.nm17.narodmon.ui.entities
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
data class SensorFilterUiEntity(
|
||||
// TODO: Можно попробовать объединить с db/SensorType.kt
|
||||
val stringRes: Int,
|
||||
val code: Int,
|
||||
var enabled: MutableState<Boolean> = mutableStateOf(false),
|
||||
)
|
|
@ -0,0 +1,7 @@
|
|||
package ru.nm17.narodmon.ui.entities
|
||||
|
||||
|
||||
data class SensorSortingUiEntity(
|
||||
val stringRes: Int,
|
||||
val sortingType: SortingTypes,
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
package ru.nm17.narodmon.ui.entities
|
|
@ -0,0 +1,13 @@
|
|||
package ru.nm17.narodmon.ui.entities
|
||||
|
||||
enum class SortingTypes {
|
||||
DISTANCE,
|
||||
DISTANCE_DESC,
|
||||
TYPE,
|
||||
TYPE_DESC,
|
||||
UPD_TIME,
|
||||
NAME,
|
||||
NAME_DESC,
|
||||
VALUE,
|
||||
VALUE_DESC
|
||||
}
|
|
@ -1,97 +1,247 @@
|
|||
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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.geometry.Offset
|
||||
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 ovh.plrapps.mapcompose.api.scale
|
||||
import ovh.plrapps.mapcompose.api.setScroll
|
||||
import ovh.plrapps.mapcompose.ui.MapUI
|
||||
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.viewmodel.MapViewModel
|
||||
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
|
||||
|
||||
enum class SensorsFilter {
|
||||
All, Thermometer, Camera,
|
||||
}
|
||||
|
||||
@ExperimentalMaterial3Api
|
||||
@Composable
|
||||
fun SensorsPage(navController: NavController) {
|
||||
val mapVM by remember { mutableStateOf(MapViewModel()) }
|
||||
var filter by remember { mutableStateOf(SensorsFilter.All) }
|
||||
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
|
||||
|
||||
LaunchedEffect(mapVM) {
|
||||
// TODO: Подгружать сохранённую позицию
|
||||
mapVM.state.setScroll(Offset(28702.6F, 14787.6F))
|
||||
mapVM.state.scale = 1.4658884F
|
||||
}
|
||||
|
||||
GenericNavScaffold(
|
||||
title = { Text(text = stringResource(R.string.sensors_page_title)) }
|
||||
) {
|
||||
Column(modifier = Modifier.padding(it)) {
|
||||
|
||||
MapUI(state = mapVM.state, modifier = Modifier.height(mapHeight.dp))
|
||||
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(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
) {
|
||||
SensorsFilterChip(
|
||||
name = stringResource(R.string.sensors_filter_all),
|
||||
checkFilter = { filter == SensorsFilter.All },
|
||||
updateFilter = { filter = SensorsFilter.All },
|
||||
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)) },
|
||||
)
|
||||
|
||||
SensorsFilterChip(
|
||||
name = stringResource(R.string.sensors_filter_temp),
|
||||
checkFilter = { filter == SensorsFilter.Thermometer },
|
||||
updateFilter = { filter = SensorsFilter.Thermometer },
|
||||
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)) },
|
||||
)
|
||||
|
||||
SensorsFilterChip(
|
||||
name = stringResource(R.string.sensors_filter_camera),
|
||||
checkFilter = { filter == SensorsFilter.Camera },
|
||||
updateFilter = { filter = SensorsFilter.Camera },
|
||||
)
|
||||
}
|
||||
|
||||
//Text(mapVM.state.scroll.toString())
|
||||
//Text(mapVM.state.scale.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SensorsFilterChip(
|
||||
name: String,
|
||||
checkFilter: () -> Boolean,
|
||||
updateFilter: () -> Unit,
|
||||
) {
|
||||
FilterChip(
|
||||
selected = checkFilter(),
|
||||
onClick = updateFilter,
|
||||
label = { Text(name) },
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package ru.nm17.narodmon.ui.viewmodel
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.statement.bodyAsChannel
|
||||
import io.ktor.utils.io.jvm.javaio.toInputStream
|
||||
import ovh.plrapps.mapcompose.api.addLayer
|
||||
import ovh.plrapps.mapcompose.core.TileStreamProvider
|
||||
import ovh.plrapps.mapcompose.ui.state.MapState
|
||||
import java.io.InputStream
|
||||
|
||||
class MapViewModel : ViewModel() {
|
||||
private val client = HttpClient(CIO)
|
||||
|
||||
private val tileStreamProvider = TileStreamProvider { row, col, zoom ->
|
||||
requestTile(row, col, zoom)
|
||||
}
|
||||
|
||||
private val mapSize = 32768
|
||||
val state: MapState by mutableStateOf(
|
||||
MapState(
|
||||
levelCount = 8,
|
||||
fullWidth = mapSize,
|
||||
fullHeight = mapSize,
|
||||
workerCount = 16,
|
||||
).apply {
|
||||
addLayer(tileStreamProvider)
|
||||
}
|
||||
)
|
||||
|
||||
private suspend fun requestTile(row: Int, col: Int, zoom: Int): InputStream {
|
||||
val response = client.get("https://tile.openstreetmap.org/${zoom}/${col}/${row}.png")
|
||||
return response.bodyAsChannel().toInputStream()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package ru.nm17.narodmon.ui.webCamsScreen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bumptech.glide.integration.compose.ExperimentalGlideComposeApi
|
||||
import com.bumptech.glide.integration.compose.GlideImage
|
||||
import ru.nm17.narodmon.ui.iosevkaFamily
|
||||
|
||||
@OptIn(ExperimentalGlideComposeApi::class)
|
||||
@Composable
|
||||
fun WebCamItem(webCamEntity: WebCamUiEntity) {
|
||||
Box(
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
contentAlignment = Alignment.BottomStart
|
||||
) {
|
||||
GlideImage(
|
||||
model = webCamEntity.imageUrl,
|
||||
contentDescription = webCamEntity.name,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.height(240.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(
|
||||
RoundedCornerShape(bottomEnd = 8.dp, bottomStart = 8.dp)
|
||||
)
|
||||
.background(Color(0f, 0f, 0f, 0.55f))
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp, top = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = webCamEntity.time,
|
||||
color = Color.White,
|
||||
fontFamily = iosevkaFamily
|
||||
)
|
||||
Text(
|
||||
text = webCamEntity.name,
|
||||
color = Color.White,
|
||||
maxLines = 1,
|
||||
fontFamily = iosevkaFamily
|
||||
)
|
||||
|
||||
}
|
||||
Text(
|
||||
text = "${webCamEntity.distance} км",
|
||||
color = Color.White,
|
||||
fontFamily = iosevkaFamily,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.End
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package ru.nm17.narodmon.ui.webCamsScreen
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
|
||||
data class WebCamUiEntity(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val distance: Int,
|
||||
val location: String,
|
||||
val time: String,
|
||||
val imageUrl: String
|
||||
)
|
|
@ -0,0 +1,63 @@
|
|||
package ru.nm17.narodmon.ui.webCamsScreen
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import ru.nm17.narodmon.ui.theme.NarodMonTheme
|
||||
|
||||
@Composable
|
||||
fun WebCamsScreen() {
|
||||
|
||||
var webCams by remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
WebCamUiEntity(
|
||||
1,
|
||||
"Крутая камера",
|
||||
1,
|
||||
"Улица Пушкина, дом Калатушкина, кватира под номером 5",
|
||||
"12:45",
|
||||
"https://images-webcams.windy.com/51/1559159251/current/preview/1559159251.jpg?1686320054"
|
||||
),
|
||||
WebCamUiEntity(
|
||||
2,
|
||||
"Крутая камера 2",
|
||||
2,
|
||||
"Улица Пушкина, дом Калатушкина, кватира под номером 5",
|
||||
"12:45",
|
||||
"https://images-webcams.windy.com/51/1559159251/current/preview/1559159251.jpg?1686320054"
|
||||
),
|
||||
WebCamUiEntity(
|
||||
3,
|
||||
"Крутая камера 3",
|
||||
3,
|
||||
"Улица Пушкина, дом Калатушкина, кватира под номером 5",
|
||||
"12:45",
|
||||
"https://images-webcams.windy.com/51/1559159251/current/preview/1559159251.jpg?1686320054"
|
||||
)
|
||||
)
|
||||
)
|
||||
} // TODO источник камер
|
||||
|
||||
LazyColumn() {
|
||||
items(webCams) {
|
||||
WebCamItem(webCamEntity = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewWebCams() {
|
||||
NarodMonTheme {
|
||||
WebCamsScreen()
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/ic_filter.xml
Normal file
5
app/src/main/res/drawable/ic_filter.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,6h10l-5.01,6.3L7,6zM4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_sort.xml
Normal file
5
app/src/main/res/drawable/ic_sort.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="24dp"
|
||||
android:tint="#000000" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
|
||||
</vector>
|
BIN
app/src/main/res/font/iosevka.ttf
Normal file
BIN
app/src/main/res/font/iosevka.ttf
Normal file
Binary file not shown.
|
@ -1,14 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".Sensors">
|
||||
|
||||
<!-- TODO: Update blank fragment layout -->
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:text="@string/hello_blank_fragment" />
|
||||
|
||||
</FrameLayout>
|
|
@ -1,7 +1,6 @@
|
|||
<resources>
|
||||
<string name="app_name">Народный Мониторинг</string>
|
||||
<!-- TODO: Remove or change this placeholder text -->
|
||||
<string name="hello_blank_fragment">Hello blank fragment</string>
|
||||
<string name="accept_agreements">Я принимаю соглашения</string>
|
||||
<string name="exit">Выйти</string>
|
||||
<string name="agreement_dialog_text">
|
||||
|
@ -15,7 +14,41 @@
|
|||
<string name="agreement_dialog_title">Примите необходимые соглашения</string>
|
||||
<string name="sensors_page_title">Сенсоры</string>
|
||||
<string name="waiting_for_user_agreement">Ожидаю соглашение пользователя</string>
|
||||
<string name="sensors_filter_all">Все</string>
|
||||
<string name="sensors_filter_temp">Термометры</string>
|
||||
<string name="sensors_filter_camera">Камеры</string>
|
||||
<string name="search">Поиск</string>
|
||||
<string name="sensors_filter">Фильтр</string>
|
||||
<string name="sensors_sorting">Сортировка</string>
|
||||
<string name="sensors_mine">Мои датчики</string>
|
||||
<string name="filter_temp_dew_point">Температура точки росы</string>
|
||||
<string name="filter_temp">Температура воздуха</string>
|
||||
<string name="filter_temp_water">Температура воды</string>
|
||||
<string name="filter_temp_ground">Температура почвы</string>
|
||||
<string name="filter_humidity">Влажность</string>
|
||||
<string name="filter_pressure">Давление</string>
|
||||
<string name="filter_lightness">Освещённость</string>
|
||||
<string name="filter_uv">УФ-индекс</string>
|
||||
<string name="filter_radiation">Радиация</string>
|
||||
<string name="filter_rainfall">Осадки</string>
|
||||
<string name="filter_dust">Запылённость</string>
|
||||
<string name="filter_wind_speed">Скорость ветра</string>
|
||||
<string name="filter_wind_direction">Направление ветра</string>
|
||||
<string name="filter_concentration">Концентрация</string>
|
||||
<string name="filter_power">Мощность</string>
|
||||
<string name="filter_voltage">Напряжение</string>
|
||||
<string name="filter_amperage">Сила тока</string>
|
||||
<string name="filter_energy">Энергия</string>
|
||||
<string name="filter_battery">% батареи</string>
|
||||
<string name="filter_rxtx">Rx/Tx трафик</string>
|
||||
<string name="filter_signal">Сигнал в dBm</string>
|
||||
<string name="filter_water_meter">Счётчик воды</string>
|
||||
<string name="filter_time">Время работы</string>
|
||||
<string name="sensors_filter_title">Тип датчиков</string>
|
||||
<string name="sort_by_distance">От ближних к дальним</string>
|
||||
<string name="sort_by_type">По типу (от А до Я)</string>
|
||||
<string name="sort_by_type_desc">По типу (от Я до А)</string>
|
||||
<string name="sort_by_name">По названию (от А до Я)</string>
|
||||
<string name="sort_by_name_desc">По названию (от Я до А)</string>
|
||||
<string name="sensors_sort_title">Сортировка датчиков</string>
|
||||
<string name="sort_update_time">По времени обновления</string>
|
||||
<string name="apply">Применить</string>
|
||||
<string name="sort_by_distance_desc">От дальних к ближним</string>
|
||||
</resources>
|
|
@ -1,5 +1,5 @@
|
|||
[versions]
|
||||
agp = "8.2.0-alpha06"
|
||||
agp = "8.2.0-alpha07"
|
||||
kotlin = "1.8.10"
|
||||
core-ktx = "1.9.0"
|
||||
junit = "4.13.2"
|
||||
|
|
Loading…
Reference in a new issue