Compare commits

..

29 commits

Author SHA1 Message Date
b1e5af538a Merge pull request 'dc09-sensors2' (#5) from dc09-sensors2 into master
Reviewed-on: nm17/narodmon#5
Reviewed-by: nm17 <nm17@riseup.net>
2023-06-18 13:15:52 +03:00
0bdf64b7ed Добавлен Ui для веб-камер 2023-06-09 19:31:08 +05:00
2a6bfb205c Улучшен UX фильтров 2023-06-08 19:26:56 +05:00
9cd0bcddc8 Радио кнопки теперь прокликиваются по всей ширине 2023-06-08 18:54:41 +05:00
45c9101f9d Убрал ненужное из MainActivity.kt + удалил ненужный .xml файл 2023-06-08 18:51:09 +05:00
1c8f993dcb Упс. Очепятка 2023-06-08 18:48:47 +05:00
937f4890e1 Добавил иконки к чипам в Sensors.kt 2023-06-08 18:48:34 +05:00
01b362ce4e Поменял strings.xml 2023-06-08 18:34:41 +05:00
d82a4b28b7 Вынес data классы в отдельные файлы. Так принято делать. По крайней мере у себя я так стараюсь делать :) 2023-06-08 18:33:57 +05:00
9d024a0c69 Bottom Sheet'ы теперь живут в отдельных файлах, чтобы Sensors.kt был читабельнее 2023-06-08 18:32:24 +05:00
c32ea30772 Отступы у searchbar'а 2023-06-08 17:16:30 +05:00
b17e853103 Чекбоксы фильтров прокликиваются по всей ширине 2023-06-08 17:15:35 +05:00
be9b491c44 Убрал лишние библиотеки. Мы же не будем RxJava использовать? 2023-06-08 17:14:04 +05:00
4c3584d63c Шрифт Iosevka для значений датчиков 2023-06-07 21:42:38 +04:00
9739d1cdfe Разработан UI для списка датчиков, добавлены тестовые данные 2023-06-07 21:07:46 +04:00
9c6be5d8c5 Пофикшена прокрутка в BottomSheet-ах 2023-06-07 16:45:32 +04:00
12db4ba8df Заголовок для BottomSheet с сортировкой 2023-06-07 16:07:45 +04:00
95e849e27a Чекбокс/радио вынесены в отдельный composable, добавлены опции сортировки 2023-06-07 16:02:42 +04:00
965533c8cc Названия перемнных: filter/sortingShown -> ...Show 2023-06-07 15:42:40 +04:00
ebd8dadf7f Добавлена сортировка 2023-06-07 15:41:53 +04:00
e6f7ff8238 Обновление Gradle 2023-06-07 15:41:44 +04:00
5639ebd4cd Исправлены чекбоксы (забыл MutableState в датаклассе), центрирован заголовок BottomSheet 2023-06-06 21:14:50 +04:00
d1e7b60378 Фильтр: Card-ы заменены на CheckBox+Text, создан датакласс для фильтров 2023-06-06 20:56:15 +04:00
2a318d551c Добавлено содержимое в BottomSheet фильтрации 2023-06-06 17:50:37 +04:00
3a873725f2 Добавлен BottomSheet для фильтров и строки 2023-06-06 17:12:20 +04:00
1334014c04 Небольшие изменения в UI экрана сенсоров 2023-06-06 15:50:55 +04:00
06bc8a839e Fix: java.security.cert.CertificateException 2023-06-06 15:50:31 +04:00
2b2a2c2f5a IDE не видит @file:OptIn 2023-06-06 15:33:43 +04:00
0935cd88d4 Перенёс карту в отдельный компонент вместе со state 2023-06-06 14:57:49 +04:00
22 changed files with 734 additions and 125 deletions

View file

@ -106,12 +106,6 @@ dependencies {
// optional - Kotlin Extensions and Coroutines support for Room // optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version") 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 // optional - Guava support for Room, including Optional and ListenableFuture
implementation("androidx.room:room-guava:$room_version") implementation("androidx.room:room-guava:$room_version")
@ -151,4 +145,8 @@ dependencies {
// Map Compose library // Map Compose library
implementation("ovh.plrapps:mapcompose:2.7.1") 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")
} }

View file

@ -41,6 +41,7 @@ import ru.nm17.narodmon.ui.pages.SensorsPage
import ru.nm17.narodmon.ui.theme.NarodMonTheme import ru.nm17.narodmon.ui.theme.NarodMonTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppNavHost() { fun AppNavHost() {
val navController = rememberNavController() 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 @Composable
fun NavHolderEl() { fun NavHolderEl() {
//NavHost(navController = NavHostController(N), graph =) //NavHost(navController = NavHostController(N), graph =)
} }
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
NarodMonTheme {
Greeting("Android")
}
}

View 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)
)

View file

@ -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({}) {
}
}
}

View file

@ -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 = {}) {}
}
}

View 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)
}

View file

@ -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,
)

View file

@ -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),
)

View file

@ -0,0 +1,7 @@
package ru.nm17.narodmon.ui.entities
data class SensorSortingUiEntity(
val stringRes: Int,
val sortingType: SortingTypes,
)

View file

@ -0,0 +1 @@
package ru.nm17.narodmon.ui.entities

View file

@ -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
}

View file

@ -1,97 +1,247 @@
package ru.nm17.narodmon.ui.pages package ru.nm17.narodmon.ui.pages
import androidx.compose.foundation.clickable
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.Row 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.height
import androidx.compose.foundation.layout.padding 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.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip 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.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController 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.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.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 @ExperimentalMaterial3Api
@Composable @Composable
fun SensorsPage(navController: NavController) { fun SensorsPage(navController: NavController) {
val mapVM by remember { mutableStateOf(MapViewModel()) } var searchQuery by remember { mutableStateOf("") }
var filter by remember { mutableStateOf(SensorsFilter.All) } 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 scrConfig = LocalConfiguration.current
val mapHeight = scrConfig.screenHeightDp / 3 val mapHeight = scrConfig.screenHeightDp / 3
LaunchedEffect(mapVM) {
// TODO: Подгружать сохранённую позицию
mapVM.state.setScroll(Offset(28702.6F, 14787.6F))
mapVM.state.scale = 1.4658884F
}
GenericNavScaffold( GenericNavScaffold(
title = { Text(text = stringResource(R.string.sensors_page_title)) } title = { Text(text = stringResource(R.string.sensors_page_title)) }
) { ) {
Column(modifier = Modifier.padding(it)) { 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( Row(
modifier = Modifier.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 8.dp),
) { ) {
SensorsFilterChip( AssistChip(
name = stringResource(R.string.sensors_filter_all), onClick = { filterShow = true },
checkFilter = { filter == SensorsFilter.All }, leadingIcon = {
updateFilter = { filter = SensorsFilter.All }, Icon(
painter = painterResource(id = R.drawable.ic_filter),
contentDescription = stringResource(id = R.string.sensors_filter)
)
},
label = { Text(text = stringResource(R.string.sensors_filter)) },
) )
SensorsFilterChip( AssistChip(
name = stringResource(R.string.sensors_filter_temp), onClick = { sortingShow = true },
checkFilter = { filter == SensorsFilter.Thermometer }, leadingIcon = {
updateFilter = { filter = SensorsFilter.Thermometer }, 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( FilterChip(
selected = checkFilter(), selected = filterMine,
onClick = updateFilter, onClick = { filterMine = !filterMine },
label = { Text(name) }, 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

@ -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()
}
}

View file

@ -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
)
}
}
}

View file

@ -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
)

View file

@ -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()
}
}

View 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>

View 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>

Binary file not shown.

View file

@ -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>

View file

@ -1,7 +1,6 @@
<resources> <resources>
<string name="app_name">Народный Мониторинг</string> <string name="app_name">Народный Мониторинг</string>
<!-- TODO: Remove or change this placeholder text --> <!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="accept_agreements">Я принимаю соглашения</string> <string name="accept_agreements">Я принимаю соглашения</string>
<string name="exit">Выйти</string> <string name="exit">Выйти</string>
<string name="agreement_dialog_text"> <string name="agreement_dialog_text">
@ -15,7 +14,41 @@
<string name="agreement_dialog_title">Примите необходимые соглашения</string> <string name="agreement_dialog_title">Примите необходимые соглашения</string>
<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="sensors_filter_all">Все</string> <string name="search">Поиск</string>
<string name="sensors_filter_temp">Термометры</string> <string name="sensors_filter">Фильтр</string>
<string name="sensors_filter_camera">Камеры</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> </resources>

View file

@ -1,5 +1,5 @@
[versions] [versions]
agp = "8.2.0-alpha06" agp = "8.2.0-alpha07"
kotlin = "1.8.10" kotlin = "1.8.10"
core-ktx = "1.9.0" core-ktx = "1.9.0"
junit = "4.13.2" junit = "4.13.2"