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:
nm17 2023-06-18 13:15:52 +03:00
commit b1e5af538a
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"