total refactor

This commit is contained in:
Vineyro 2025-06-05 14:50:14 +07:00
parent 20c8842f95
commit 435a4db2fb
369 changed files with 23610 additions and 8447 deletions

View File

@ -13,6 +13,28 @@
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="tester">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-27T09:31:03.968211200Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=S96PROEEA0000067711" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
<SelectionState runConfigName="vgate">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2025-05-23T08:00:06.927511100Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=S96PROEEA0000067711" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
</project>

View File

@ -11,9 +11,11 @@
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/common" />
<option value="$PROJECT_DIR$/tester" />
<option value="$PROJECT_DIR$/vgate" />
</set>
</option>
<option name="resolveExternalAnnotations" value="false" />
</GradleProjectSettings>
</option>
</component>

View File

@ -3,15 +3,19 @@
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
@ -27,21 +31,26 @@
</inspection_tool>
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
@ -52,8 +61,13 @@
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Kotlin2JsCompilerArguments">
<option name="moduleKind" value="plain" />
</component>
<component name="Kotlin2JvmCompilerArguments">
<option name="jvmTarget" value="1.8" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" />
<option name="version" value="2.1.20" />
</component>
</project>

View File

@ -1,4 +1,10 @@
<project version="4">
<component name="EntryPointsManager">
<list size="2">
<item index="0" class="java.lang.String" itemvalue="dagger.Binds" />
<item index="1" class="java.lang.String" itemvalue="dagger.Module" />
</list>
</component>
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />

View File

@ -1,117 +0,0 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id("org.jetbrains.kotlin.plugin.serialization")
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
id("kotlin-parcelize")
id("androidx.room")
}
android {
namespace 'llc.arma.ble'
compileSdk 34
defaultConfig {
applicationId "llc.arma.ble"
minSdk 26
targetSdk 34
versionCode 50
versionName "1.4.24"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.9'
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}
applicationVariants.configureEach { variant ->
variant.outputs.configureEach {
outputFileName = "Arma BLE v${defaultConfig.versionName}.apk"
}
}
room {
schemaDirectory("$projectDir/schemas")
}
}
dependencies {
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0-alpha01'
implementation 'androidx.activity:activity-compose:1.7.2'
implementation "androidx.compose.ui:ui:1.5.0-beta01"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.0-beta01"
implementation 'androidx.compose.material3:material3:1.2.0-alpha02'
implementation 'androidx.compose.material:material:1.5.0-beta01'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.5.0-beta01"
debugImplementation "androidx.compose.ui:ui-tooling:1.5.0-beta01"
debugImplementation "androidx.compose.ui:ui-test-manifest:1.5.0-beta01"
implementation "androidx.compose.material:material-icons-extended:1.5.0-beta01"
implementation 'androidx.core:core-splashscreen:1.0.1'
implementation 'androidx.navigation:navigation-compose:2.5.3'
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
implementation('com.google.dagger:hilt-android:2.46')
kapt('com.google.dagger:hilt-android-compiler:2.46')
kapt("androidx.hilt:hilt-compiler:1.2.0")
implementation 'no.nordicsemi.android.kotlin.ble:scanner:1.0.14'
implementation 'no.nordicsemi.android.kotlin.ble:client:1.0.14'
implementation "com.google.accompanist:accompanist-permissions:0.26.3-beta"
implementation "com.patrykandpatrick.vico:core:1.7.1"
implementation "com.patrykandpatrick.vico:compose:1.7.1"
implementation "com.patrykandpatrick.vico:compose-m3:1.7.1"
implementation "androidx.room:room-runtime:2.6.1"
kapt("androidx.room:room-compiler:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
implementation("androidx.datastore:datastore-preferences:1.1.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
implementation files('libs/poishadow-all.jar')
}

132
app/build.gradle.kts Normal file
View File

@ -0,0 +1,132 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.hilt)
alias(libs.plugins.room)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.parcelize)
}
android {
namespace = "llc.arma.ble"
compileSdk = 35
defaultConfig {
applicationId = "llc.arma.ble"
minSdk = 26
targetSdk = 35
versionCode = 50
versionName = "1.4.24"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
/*debug {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}*/
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
/*packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
}*/
applicationVariants.all {
outputs.all {
(this as BaseVariantOutputImpl).outputFileName = "Arma BLE v${defaultConfig.versionName}.apk"
}
}
room {
schemaDirectory("$projectDir/schemas")
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.ui)
implementation(libs.ui.tooling.preview)
implementation(libs.material3)
implementation(libs.androidx.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(libs.ui.test.junit4)
debugImplementation(libs.ui.tooling)
debugImplementation(libs.ui.test.manifest)
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.navigation.compose)
implementation("io.github.raamcosta.compose-destinations:core:2.1.1")
ksp("io.github.raamcosta.compose-destinations:ksp:2.1.1")
implementation("io.github.raamcosta.compose-destinations:bottom-sheet:2.1.1")
implementation(libs.androidx.hilt.navigation.compose)
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)
ksp(libs.androidx.hilt.compiler)
implementation(libs.scanner)
implementation(libs.client)
//implementation("no.nordicsemi.kotlin.ble:core:2.0.0-alpha02")
implementation("no.nordicsemi.kotlin.ble:client-android:2.0.0-alpha02")
implementation(libs.accompanist.permissions)
implementation(libs.core)
implementation(libs.compose)
implementation(libs.compose.m3)
implementation(libs.androidx.room.runtime)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.room.ktx)
implementation(libs.androidx.datastore.preferences)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlin.parcelize.runtime)
implementation(files("libs/poishadow-all.jar"))
}

View File

@ -1,6 +1,6 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

View File

@ -55,7 +55,7 @@
<activity
android:name=".app.ui.MainActivity"
android:exported="true"
android:theme="@style/Theme.App.Starting">
android:theme="@style/Theme.Ble">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -15,6 +15,7 @@ import android.view.SurfaceView
import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@ -24,8 +25,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
@ -33,6 +36,7 @@ import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@ -72,9 +76,9 @@ class MainActivity : ComponentActivity() {
val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
WindowCompat.setDecorFitsSystemWindows(window, false)
enableEdgeToEdge()
installSplashScreen()
//installSplashScreen()
setContent {
@ -103,7 +107,9 @@ class MainActivity : ComponentActivity() {
)
) {
BoxWithConstraints {
BoxWithConstraints(
modifier = Modifier.navigationBarsPadding()
) {
val maxHeight = with(LocalDensity.current) {
this@BoxWithConstraints.constraints.maxHeight.toDp()
@ -168,6 +174,7 @@ class MainActivity : ComponentActivity() {
},
content = {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Surface(
modifier = Modifier
.fillMaxSize()
@ -249,6 +256,7 @@ class MainActivity : ComponentActivity() {
}
}
}
}

View File

@ -17,19 +17,19 @@ import androidx.compose.ui.unit.dp
@Composable
fun SignalLevel(
modifier: Modifier = Modifier,
maxLevel: Int = 5,
maxLevel: Int = 4,
level: Int
){
val step = (16 - 4) / 4
val step = (16 - 4) / maxLevel
Row(
modifier = modifier.height(16.dp),
modifier = modifier.height(12.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.Bottom
) {
for(col in 0..4 step 1){
for(col in 0..<maxLevel step 1){
Surface(
color = LocalContentColor.current.copy(
alpha = if(col <= level + 1) ContentAlpha.high else ContentAlpha.disabled

View File

@ -1,68 +0,0 @@
package llc.arma.ble.app.ui.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
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.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.SelectorItem
@Composable
fun TxLevelSelector(
tx: BleView.BleState.TX,
onSelect: (tx: BleView.BleState.TX) -> Unit,
){
var value by remember(tx) {
mutableStateOf(tx)
}
Column(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Мощность",
style = MaterialTheme.typography.titleLarge
)
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
) {
BleView.BleState.TX.entries.forEach {
SelectorItem(
label = "${it.value} dBb (${it.powerPercentage} %)",
selected = it == value
){
value = it
}
}
}
PrimaryButton(
label = "Применить"
) {
onSelect(value)
}
}
}

View File

@ -49,16 +49,16 @@ class BleMapper @Inject constructor(
)
}
is Ble.Host -> {
BleView.Host(
is Ble.Gate -> {
BleView.Gate(
info = input.info,
state = BleView.BleState(
tx = txMapper.map(input.state.tx),
version = input.state.version
),
hostState = BleView.Host.HostState(
historyInterval = input.hostState.historyInterval,
readInterval = input.hostState.readInterval
hostState = BleView.Gate.HostState(
historyInterval = input.gateState.historyInterval,
readInterval = input.gateState.readInterval
)
)
}

View File

@ -49,14 +49,14 @@ class BleViewMapper @Inject constructor(
)
}
is BleView.Host -> {
Ble.Host(
is BleView.Gate -> {
Ble.Gate(
info = input.info,
state = Ble.BleState(
tx = txMapper.map(input.state.tx),
version = input.state.version
),
hostState = Ble.Host.HostState(
gateState = Ble.Gate.HostState(
historyInterval = input.hostState.historyInterval,
readInterval = input.hostState.readInterval
)

View File

@ -60,7 +60,7 @@ sealed class BleView(
}
class Host(
class Gate(
info: BleInfo,
val state: BleState,
val hostState: HostState

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -21,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
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.unit.dp
@ -35,19 +37,12 @@ fun BleInfoView(
version: BleRepositoryImpl.Version
) {
Surface(
modifier = Modifier.padding(bottom = 16.dp),
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceVariant
) {
Column(
modifier = Modifier.padding(8.dp)
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Column {
BleInfoItem(
shapeType = ShapeType.Start,
icon = {
Icon(
imageVector = bleInfo.type.icon,
@ -58,9 +53,8 @@ fun BleInfoView(
subtitle = "${bleInfo.type.localized} v${version}"
)
SpecDivider()
BleInfoItem(
shapeType = ShapeType.Middle,
icon = {
Icon(
imageVector = Icons.Rounded.Key,
@ -71,9 +65,8 @@ fun BleInfoView(
subtitle = bleInfo.serial
)
SpecDivider()
BleInfoItem(
shapeType = ShapeType.Middle,
icon = {
Icon(
imageVector = Icons.Rounded.BatteryFull,
@ -84,9 +77,8 @@ fun BleInfoView(
subtitle = "${bleInfo.batteryLevel} %"
)
SpecDivider()
BleInfoItem(
shapeType = ShapeType.End,
icon = {
Icon(
imageVector = Icons.Rounded.NetworkCell,
@ -94,15 +86,11 @@ fun BleInfoView(
)
},
title = "Мощность сигнала",
subtitle = if(bleInfo.rssi != null) "${bleInfo.rssi } dBm" else "Нет сигнала"
subtitle = if (bleInfo.rssi != null) "${bleInfo.rssi} dBm" else "Нет сигнала"
)
}
}
}
}
@Composable
@ -116,27 +104,65 @@ private fun SpecDivider(){
}
enum class ShapeType(
val shape: RoundedCornerShape
) {
Start(RoundedCornerShape(16.dp, 16.dp, 4.dp, 4.dp)),
Middle(RoundedCornerShape(4.dp)),
End(RoundedCornerShape(4.dp, 4.dp, 16.dp, 16.dp)),
Singleton(RoundedCornerShape(16.dp));
companion object {
fun <T> List<T>.takeShapeType(item: T): ShapeType{
return if(size == 1){
ShapeType.Singleton
} else {
if(indexOf(item) == 0){
ShapeType.Start
} else {
if(indexOf(item) == size - 1){
ShapeType.End
} else {
ShapeType.Middle
}
}
}
}
}
}
@Composable
private fun BleInfoItem(
shapeType: ShapeType,
icon: @Composable () -> Unit,
title: String,
subtitle: String
){
Surface(
shape = shapeType.shape,
color = MaterialTheme.colorScheme.surfaceContainer
) {
Row(
modifier = Modifier.padding(8.dp),
modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Surface(
modifier = Modifier.size(40.dp),
shape = CircleShape
shape = CircleShape,
color = MaterialTheme.colorScheme.surfaceContainerHighest,
modifier = Modifier.size(36.dp),
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
){
) {
icon()
@ -163,4 +189,6 @@ private fun BleInfoItem(
}
}
}

View File

@ -5,15 +5,12 @@ import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
class BleListContract {
sealed class Event : ViewEvent {
data object OnResetFilter : Event()
data object OnHideFilter : Event()
data object OnResetScanner : Event()
data object OnShowFilter : Event()
@ -21,73 +18,32 @@ class BleListContract {
val bleAddress: String
) : Event()
data class OnRssiRangeChanged(
val rssi: ClosedFloatingPointRange<Float>
) : Event()
data class OnBatteryRangeChanged(
val battery: ClosedFloatingPointRange<Float>
) : Event()
data class OnMacFilterChanged(
val mac: String
) : Event()
data class OnNameFilterChanged(
val name: String
) : Event()
data class OnTypeChanged(
val type: BleInfo.Type?
) : Event()
data class OnSortFieldChanged(
val field: BleFilter.Field
) : Event()
data class OnSortOrderChanged(
val order: BleFilter.Order
) : Event()
}
data class State(
val connectedBleList: List<ConnectedBleInfo>,
val bleList: List<BleInfo>,
val bleFilter: BleFilter
) : ViewState {
/*data class Filter(
val sortField: Field = Field.Name,
val sortOrder: Order = Order.Asc,
val name: String = "",
val mac: String = "",
val battery: ClosedFloatingPointRange<Float> = (0f)..(100f),
val rssi: ClosedFloatingPointRange<Float> = (-100f)..(-10f),
val bleType: BleInfo.Type? = null
){
enum class Field {
Name, Mac, Distance, Dbm, Battery
}
enum class Order {
Asc, Desc
}
}*/
}
) : ViewState
sealed class Effect : ViewSideEffect {
object ShowFilter : Effect()
object HideFilter : Effect()
sealed class Navigation : Effect() {
data class NavigateToBle(
data object BleFilter : Navigation()
data class Beacon(
val serial: String
) : Navigation()
data class Thermometer(
val serial: String
) : Navigation()
data class Accelerometer(
val serial: String
) : Navigation()
data class Gate(
val serial: String
) : Navigation()

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -23,7 +24,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt
import androidx.compose.material.icons.rounded.ArrowRightAlt
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.CompareArrows
import androidx.compose.material.icons.rounded.FilterAlt
@ -34,11 +34,14 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
@ -47,64 +50,82 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import com.ramcosta.composedestinations.generated.destinations.BleFilterScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.SignalLevel
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.locale.icon
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow
@Destination<RootGraph>(start = true)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BleListScreen(
onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
//onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BleListViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
val scrollState = rememberLazyListState()
val lifecycleOwner = LocalLifecycleOwner.current
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
LaunchedEffect(lifecycleState) {
if(lifecycleState == Lifecycle.State.RESUMED)
viewModel.setEvent(BleListContract.Event.OnResetScanner)
}
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is BleListContract.Effect.Navigation -> onNavigationEvent(it)
is BleListContract.Effect.HideFilter -> launch {
bottomDialog.hide()
}
is BleListContract.Effect.ShowFilter -> launch {
bottomDialog.show {
Filter(
filter = viewModel.viewState.value.bleFilter,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
is BleListContract.Effect.Navigation.Accelerometer ->
navigator.navigate(AccelerometerScreenDestination(it.serial))
is BleListContract.Effect.Navigation.Beacon ->
navigator.navigate(BeaconScreenDestination(it.serial))
BleListContract.Effect.Navigation.BleFilter ->
navigator.navigate(BleFilterScreenDestination)
is BleListContract.Effect.Navigation.Gate ->
navigator.navigate(GateScreenDestination(it.serial))
is BleListContract.Effect.Navigation.Thermometer ->
navigator.navigate(ThermometerScreenDestination(it.serial))
}
}.launchIn(this)
}
Column {
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier
.zIndex(1f)
.shadow(if (scrollState.canScrollBackward) 8.dp else 0.dp),
title = {
Text(text = "Arma BLE")
},
@ -155,6 +176,12 @@ fun BleListScreen(
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
val filteredData = remember(state.bleList, state.bleFilter) {
@ -211,18 +238,10 @@ fun BleListScreen(
LazyColumn(
state = scrollState,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxSize()
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
items(items = state.connectedBleList){
ConnectedBleItem(ble = it) {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
}
items(
items = filteredData,
key = { it.serial }
@ -230,6 +249,7 @@ fun BleListScreen(
BleItem(
ble = it,
shapeType = filteredData.takeShapeType(it),
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
@ -242,6 +262,10 @@ fun BleListScreen(
}
}
}
@Composable
@ -251,7 +275,7 @@ fun ItemIcon(
Surface(
modifier = Modifier.size(40.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
color = MaterialTheme.colorScheme.surfaceContainerHighest,
shape = CircleShape
) {
Box(modifier = Modifier.fillMaxSize()) {
@ -275,6 +299,7 @@ private fun Int.toSignalLevel(): Int {
@Composable
fun BleItem(
shapeType: ShapeType,
ble: BleInfo,
onClick: () -> Unit
){
@ -282,7 +307,7 @@ fun BleItem(
val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer
} else {
MaterialTheme.colorScheme.background
MaterialTheme.colorScheme.surfaceContainer
}
val highAlpha = ContentAlpha.high
@ -313,13 +338,10 @@ fun BleItem(
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clip(shapeType.shape)
.background(color)
.clickable(onClick = onClick)
.padding(
vertical = 8.dp,
horizontal = 16.dp
)
.padding(horizontal = 16.dp, vertical = 12.dp)
.alpha(alpha)
) {
@ -366,65 +388,16 @@ fun BleItem(
Text(text = ble.name)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.serial
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.CompareArrows,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
val distance = remember(ble.rssi, ble.tx) {
String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
}
Text(
style = MaterialTheme.typography.bodyMedium,
text = distance
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
Spacer(modifier = Modifier.width(4.dp))
Box {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "-999 dBm",
modifier = Modifier.alpha(0f)
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.rssi.toString() + " dBm"
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
@ -462,6 +435,64 @@ fun BleItem(
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = Icons.Rounded.CompareArrows,
contentDescription = null
)
Spacer(modifier = Modifier.width(4.dp))
val distance = remember(ble.rssi, ble.tx) {
String.format("%.3f", (10.0.pow((ble.tx.toDouble() - (ble.rssi?.toDouble() ?: 0.0) - 74) / 20))) + " м."
}
Text(
style = MaterialTheme.typography.bodyMedium,
text = distance
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
SignalLevel(level = ble.rssi?.toSignalLevel() ?: 0)
Spacer(modifier = Modifier.width(4.dp))
Box {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "-999 dBm",
modifier = Modifier.alpha(0f)
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.rssi.toString() + " dBm"
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
@ -509,61 +540,3 @@ fun BleItem(
}
}
@Composable
private fun ConnectedBleItem(
ble: ConnectedBleInfo,
onClick: () -> Unit
){
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onClick)
.background(MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = .99f))
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
ItemIcon {
Icon(
modifier = Modifier.align(Alignment.Center),
imageVector = Icons.Rounded.Link,
contentDescription = null
)
}
Column {
Text(text = ble.name)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.serial
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f)
) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = "Соединено"
)
}
}
}
}
}

View File

@ -8,40 +8,23 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.GetBleAroundFlow
import llc.arma.ble.domain.usecase.filter.GetFilterFlow
import llc.arma.ble.domain.usecase.filter.SaveFilter
import javax.inject.Inject
@HiltViewModel
class BleListViewModel @Inject constructor(
private val getFilterFlow: GetFilterFlow,
private val saveFilter: SaveFilter,
getBleAroundFlow: GetBleAroundFlow
private val getBleAroundFlow: GetBleAroundFlow
) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() {
private var scannerJob: Job? = null
init {
viewModelScope.launch {
var job: Job? = null
getBleAroundFlow().fold(
onSuccess = {
it.onEach {
setState {
copy(
connectedBleList = emptyList(),
bleList = it
)
}
}.launchIn(viewModelScope)
},
onFailure = {
throw IllegalStateException()
}
)
getFilterFlow.invoke().onEach {
setState {
copy(
@ -49,51 +32,18 @@ class BleListViewModel @Inject constructor(
)
}
}.launchIn(this)
/*while (true) {
job?.cancel()
job = getBleAroundFlow().onEach {
it.fold(
onSuccess = {
setState {
copy(
connectedBleList = emptyList(),
bleList = it
)
}
},
onFailure = {
}
)
}.launchIn(viewModelScope)
delay(30_000)
}*/
}
}
override fun setInitialState(): BleListContract.State =
BleListContract.State(emptyList(), emptyList(), BleFilter())
BleListContract.State(emptyList(), BleFilter())
override fun handleEvents(event: BleListContract.Event) {
when(event){
is BleListContract.Event.OnConnectToBle -> reduce(viewState.value, event)
is BleListContract.Event.OnHideFilter -> reduce(viewState.value, event)
is BleListContract.Event.OnMacFilterChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnNameFilterChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnResetFilter -> reduce(viewState.value, event)
is BleListContract.Event.OnRssiRangeChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnShowFilter -> reduce(viewState.value, event)
is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnBatteryRangeChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnSortFieldChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnSortOrderChanged -> reduce(viewState.value, event)
is BleListContract.Event.OnResetScanner -> reduce(viewState.value, event)
}
}
@ -101,150 +51,30 @@ class BleListViewModel @Inject constructor(
state: BleListContract.State,
event: BleListContract.Event.OnConnectToBle
) {
setEffect {
BleListContract.Effect.Navigation.NavigateToBle(serial = event.bleAddress)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnHideFilter
) {
setEffect {
BleListContract.Effect.HideFilter
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnMacFilterChanged
) {
setState {
copy(bleFilter = state.bleFilter.copy(mac = event.mac))
}
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(mac = event.mac)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnSortOrderChanged
) {
setState {
copy(bleFilter = state.bleFilter.copy(sortOrder = event.order))
}
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(sortOrder = event.order)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnSortFieldChanged
) {
setState {
copy(bleFilter = state.bleFilter.copy(sortField = event.field))
}
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(sortField = event.field)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnNameFilterChanged
) {
setState {
copy(bleFilter = state.bleFilter.copy(name = event.name))
}
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(name = event.name)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnResetFilter
) {
setState {
copy(bleFilter = BleFilter())
}
viewModelScope.launch {
saveFilter(BleFilter())
state.bleList.firstOrNull { it.serial == event.bleAddress }?.let { ble ->
setEffect {
BleListContract.Effect.HideFilter
when (ble.type) {
BleInfo.Type.HOST ->
BleListContract.Effect.Navigation.Gate(serial = event.bleAddress)
BleInfo.Type.BEACON ->
BleListContract.Effect.Navigation.Beacon(serial = event.bleAddress)
BleInfo.Type.THERMOMETER ->
BleListContract.Effect.Navigation.Thermometer(serial = event.bleAddress)
BleInfo.Type.ACCELEROMETER ->
BleListContract.Effect.Navigation.Accelerometer(serial = event.bleAddress)
}
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnRssiRangeChanged
) {
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(rssi = event.rssi)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnBatteryRangeChanged
) {
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(battery = event.battery)
)
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnTypeChanged
) {
setState {
copy(bleFilter = state.bleFilter.copy(bleType = event.type))
}
viewModelScope.launch {
saveFilter(
bleFilter = state.bleFilter.copy(bleType = event.type)
)
}
}
private fun reduce(
@ -252,8 +82,24 @@ class BleListViewModel @Inject constructor(
event: BleListContract.Event.OnShowFilter
) {
setEffect {
BleListContract.Effect.ShowFilter
BleListContract.Effect.Navigation.BleFilter
}
}
private fun reduce(
state: BleListContract.State,
event: BleListContract.Event.OnResetScanner
) {
scannerJob?.cancel()
scannerJob = getBleAroundFlow().onEach {
setState {
copy(
bleList = it
)
}
}.launchIn(viewModelScope)
}
}

View File

@ -1,472 +0,0 @@
package llc.arma.ble.app.ui.screen.ble
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.ShortText
import androidx.compose.material.icons.rounded.SignalCellularAlt
import androidx.compose.material.icons.rounded.Sort
import androidx.compose.material.icons.rounded.SortByAlpha
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.SliderDefaults
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Filter(
filter: BleFilter,
onEvent: (BleListContract.Event) -> Unit
) {
Column {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Фильтр",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.Sort,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded,
onExpandedChange = {
expanded = it
}
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
readOnly = true,
value = filter.sortField.localized,
onValueChange = { },
label = { Text("Сортировка") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
BleFilter.Field.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onEvent(
BleListContract.Event.OnSortFieldChanged(
selectionOption
)
)
expanded = false
},
text = {
Text(text = selectionOption.localized)
}
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.SortByAlpha,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded,
onExpandedChange = {
expanded = it
}
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
readOnly = true,
value = filter.sortOrder.localized,
onValueChange = { },
label = { Text("Напрвление сортировки") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
BleFilter.Order.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onEvent(
BleListContract.Event.OnSortOrderChanged(
selectionOption
)
)
expanded = false
},
text = {
Text(text = selectionOption.localized)
}
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.Bluetooth,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded,
onExpandedChange = {
expanded = it
}
) {
OutlinedTextField(
modifier = Modifier
.menuAnchor()
.fillMaxWidth(),
readOnly = true,
value = filter.bleType.localized,
onValueChange = { },
label = { Text("Тип") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
}
)
ExposedDropdownMenu(
modifier = Modifier
.background(MaterialTheme.colorScheme.background)
.fillMaxWidth(),
expanded = expanded,
onDismissRequest = {
expanded = false
}
) {
mutableListOf<BleInfo.Type?>(null).apply {
addAll(BleInfo.Type.entries.toTypedArray())
}.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
onEvent(
BleListContract.Event.OnTypeChanged(
selectionOption
)
)
expanded = false
},
text = {
Text(text = selectionOption.localized)
}
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
){
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
OutlinedTextField(
value = filter.name,
singleLine = true,
onValueChange = {
onEvent(BleListContract.Event.OnNameFilterChanged(it))
},
label = {
Text(text = "Имя")
},
trailingIcon = {
if(filter.name.isNotEmpty()) {
IconButton(
onClick = { onEvent(BleListContract.Event.OnNameFilterChanged("")) }
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null
)
}
}
},
modifier = Modifier
.padding(end = 8.dp)
.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.ShortText,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
OutlinedTextField(
value = filter.mac,
singleLine = true,
onValueChange = {
onEvent(BleListContract.Event.OnMacFilterChanged(it))
},
label = {
Text(text = "Mac")
},
trailingIcon = {
if (filter.mac.isNotEmpty()) {
IconButton(
onClick = { onEvent(BleListContract.Event.OnMacFilterChanged("")) }
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null
)
}
}
},
modifier = Modifier
.padding(end = 8.dp)
.fillMaxWidth()
)
}
Spacer(modifier = Modifier.height(12.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(end = 8.dp)
) {
Icon(
imageVector = Icons.Rounded.SignalCellularAlt,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Column() {
RangeSlider(
value = filter.rssi,
onValueChange = {
onEvent(BleListContract.Event.OnRssiRangeChanged(it))
},
valueRange = (-100f)..(-10f),
steps = 89,
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.primary,
inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
)
)
Row() {
Text(text = filter.rssi.start.toInt().toString() + " dBm")
Spacer(modifier = Modifier.weight(1f))
Text(text = filter.rssi.endInclusive.toInt().toString() + " dBm")
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(end = 8.dp)
) {
Icon(
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Column() {
RangeSlider(
value = filter.battery,
onValueChange = {
onEvent(BleListContract.Event.OnBatteryRangeChanged(it))
},
valueRange = 0f..100f,
steps = 99,
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.primary,
inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
)
)
Row {
Text(text = "0 %")
Spacer(modifier = Modifier.weight(1f))
Text(text = "100 %")
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
PrimaryButton(
label = "Применить"
) {
onEvent(BleListContract.Event.OnHideFilter)
}
SecondaryButton(
label = "Сбросить"
) {
onEvent(BleListContract.Event.OnResetFilter)
}
}
}

View File

@ -5,10 +5,10 @@ import kotlinx.parcelize.Parcelize
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract
import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
@ -31,7 +31,7 @@ class ConnectionContract {
) : Event()
data class OnHostNavigationEvent(
val event: HostContract.Effect.Navigation
val event: GateContract.Effect.Navigation
) : Event()
data class OnThermometerNavigationEvent(
@ -50,6 +50,7 @@ class ConnectionContract {
data object Loading : State()
data class DisplayException(
val tries: Long,
val exception: BleException
) : State()
@ -73,6 +74,10 @@ class ConnectionContract {
val serial: String
) : Navigation()
data class NavigateToThermometerHistory(
val bleSerial: String
) : Navigation()
}
sealed class InnerNavigation : Effect() {

View File

@ -2,31 +2,28 @@ package llc.arma.ble.app.ui.screen.connection
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.animation.with
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -35,24 +32,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.SmallPrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerScreen
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerHistory
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerRealtime
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelerometerSpectre
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconScreen
import llc.arma.ble.app.ui.screen.inspection.host.HostScreen
import llc.arma.ble.app.ui.screen.inspection.host.view.HostHistory
import llc.arma.ble.app.ui.screen.inspection.host.view.table.BleTableEditContract
import llc.arma.ble.app.ui.screen.inspection.host.view.table.BleTableEditScreen
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerScreen
import llc.arma.ble.domain.model.Ble
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -86,7 +71,7 @@ fun ConnectionScreen(
Column {
CenterAlignedTopAppBar(
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
@ -127,18 +112,17 @@ fun ConnectionScreen(
}
)
when (state) {
/*when (state) {
is ConnectionContract.State.DisplayException -> DisplayException(
onEvent = {
viewModel.setEvent(it)
}
viewState = state,
onEvent = viewModel::setEvent
)
is ConnectionContract.State.Loading -> LoadingState()
is ConnectionContract.State.Display -> {
when (state.ble) {
is Ble.Beacon -> BeaconScreen(
ble = state.ble,
null,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnBeaconNavigationEvent(
@ -151,7 +135,8 @@ fun ConnectionScreen(
is Ble.Thermometer -> {
ThermometerScreen(
ble = state.ble,
txSelectResult = null,
//ble = state.ble,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnThermometerNavigationEvent(
@ -164,16 +149,17 @@ fun ConnectionScreen(
}
is Ble.Accelerometer -> {
AccelerometerScreen(ble = state.ble) {
/*AccelerometerScreen {
viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it)
)
}
}*/
}
is Ble.Host -> {
HostScreen(
ble = state.ble,
is Ble.Gate -> {
GateScreen(
null,
null,
onNavigationEvent = {
viewModel.setEvent(
ConnectionContract.Event.OnHostNavigationEvent(it)
@ -186,10 +172,12 @@ fun ConnectionScreen(
}
}
}*/
}
/*
innerScreen?.let {
Surface(
@ -245,20 +233,20 @@ fun ConnectionScreen(
}
is ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory -> {
HostHistory(
/*GateHistoryScreen(
ble = it.ble,
onDismiss = {
innerScreen = null
}
)
)*/
}
is ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable -> {
BleTableEditScreen(it.serial){
GateBleTableScreen {
when(it){
BleTableEditContract.Effect.Navigation.NavigateUp -> {
GateBleTableContract.Effect.Navigation.Up -> {
innerScreen = null
}
}
@ -272,26 +260,28 @@ fun ConnectionScreen(
}
*/
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
Column {
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center
)
)
}
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
ContainedLoadingIndicator()
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun DisplayException(
viewState: ConnectionContract.State.DisplayException,
onEvent: (ConnectionContract.Event) -> Unit
){
@ -301,9 +291,20 @@ private fun DisplayException(
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.align(Alignment.Center)
modifier = Modifier.align(Alignment.Center).widthIn(max = 270.dp)
) {
ContainedLoadingIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
text = "Повторная попытка ${viewState.tries}"
)
Text(
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium,
@ -313,9 +314,9 @@ private fun DisplayException(
Spacer(modifier = Modifier.height(18.dp))
SmallPrimaryButton(
label = "Повторить"
label = "Отмена"
) {
onEvent(ConnectionContract.Event.RefreshBle)
onEvent(ConnectionContract.Event.OnNavigateUp)
}
}

View File

@ -7,10 +7,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.usecase.GetBleBySerial
import javax.inject.Inject
@ -41,19 +38,19 @@ class ConnectionViewModel @Inject constructor(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnHostNavigationEvent
) {
when(event.event){
HostContract.Effect.Navigation.NavigateUp -> {
/*when(event.event){
GateContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
HostContract.Effect.Navigation.NavigateToChangePassword -> {
is GateContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is HostContract.Effect.Navigation.NavigateToHostHistory -> {
is GateContract.Effect.Navigation.GateHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory(
event.event.ble
@ -61,14 +58,17 @@ class ConnectionViewModel @Inject constructor(
}
}
is HostContract.Effect.Navigation.NavigateToBleTable -> {
is GateContract.Effect.Navigation.BleTable -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable(
event.event.serial
)
}
}
}
is GateContract.Effect.Navigation.TxSelector -> TODO()
is GateContract.Effect.Navigation.ReadIntervalSelector -> TODO()
}*/
}
private fun reduce(
@ -76,16 +76,18 @@ class ConnectionViewModel @Inject constructor(
event: ConnectionContract.Event.OnBeaconNavigationEvent
) {
when(event.event){
BeaconContract.Effect.Navigation.NavigateUp -> {
BeaconContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
BeaconContract.Effect.Navigation.NavigateToChangePassword -> {
is BeaconContract.Effect.Navigation.PasswordForm -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is BeaconContract.Effect.Navigation.TxSelector -> TODO()
}
}
@ -93,32 +95,40 @@ class ConnectionViewModel @Inject constructor(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnThermometerNavigationEvent
) {
when(event.event){
ThermometerContract.Effect.Navigation.NavigateUp -> {
/*(event.event){
ThermometerContract.Effect.Navigation.Up -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateUp
}
}
ThermometerContract.Effect.Navigation.NavigateToChangePassword -> {
ThermometerContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is ThermometerContract.Effect.Navigation.ThermometerHistory -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToThermometerHistory(event.event.bleSerial)
}
}
is ThermometerContract.Effect.Navigation.TxSelector -> TODO()
}*/
}
private fun reduce(
state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent
) {
when(event.event){
AccelerometerContract.Effect.Navigation.NavigateToChangePassword -> {
/*when(event.event){
is AccelerometerContract.Effect.Navigation.ChangePassword -> {
setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
}
}
is AccelerometerContract.Effect.Navigation.NavigateToAccelHistory -> {
is AccelerometerContract.Effect.Navigation.AccelHistory -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory(
event.event.ble,
@ -131,7 +141,7 @@ class ConnectionViewModel @Inject constructor(
}
}
is AccelerometerContract.Effect.Navigation.NavigateToAccelRealtime -> {
is AccelerometerContract.Effect.Navigation.AccelRealtime -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime(
event.event.ble,
@ -143,7 +153,7 @@ class ConnectionViewModel @Inject constructor(
)
}
}
is AccelerometerContract.Effect.Navigation.NavigateToAccelSpectre -> {
is AccelerometerContract.Effect.Navigation.AccelSpectre -> {
setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre(
event.event.ble,
@ -155,7 +165,12 @@ class ConnectionViewModel @Inject constructor(
)
}
}
is AccelerometerContract.Effect.Navigation.TxPowerSelector -> TODO()
is AccelerometerContract.Effect.Navigation.ReadIntervalSelector -> TODO()
is AccelerometerContract.Effect.Navigation.SaveIntervalSelector -> TODO()
}
*/
}
private fun reduce(
@ -179,7 +194,7 @@ class ConnectionViewModel @Inject constructor(
}
private fun refreshBle(){
val serial = savedStateHandle.get<String>("serial")
/*val serial = savedStateHandle.get<String>("serial")
if(serial != null){
@ -189,7 +204,11 @@ class ConnectionViewModel @Inject constructor(
ConnectionContract.State.Loading
}
getBleBySerial(serial).fold(
var tries = 0L
while (true) {
getBleBySerial(serial, this).fold(
onSuccess = {
it.onEach {
@ -200,17 +219,22 @@ class ConnectionViewModel @Inject constructor(
}
}.launchIn(viewModelScope)
return@launch
},
onFailure = {
setState {
ConnectionContract.State.DisplayException(it)
tries += 1
ConnectionContract.State.DisplayException(tries, it)
}
}
)
}
}
} else {
throw IllegalArgumentException("serial arg must not be null")
}
}*/
}
}

View File

@ -0,0 +1,44 @@
package llc.arma.ble.app.ui.screen.filter
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleFilter
class BleFilterContract {
sealed class Event : ViewEvent {
data class OnFilterChanged(
val filter: BleFilter
) : Event()
data object OnSave : Event()
data object OnResetFilter : Event()
data object OnNavigateUp : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class Display(
val filter: BleFilter
) : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect(){
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,574 @@
package llc.arma.ble.app.ui.screen.filter
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.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.automirrored.rounded.ShortText
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material.icons.rounded.BatteryFull
import androidx.compose.material.icons.rounded.Bluetooth
import androidx.compose.material.icons.rounded.Close
import androidx.compose.material.icons.rounded.Restore
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.SignalCellularAlt
import androidx.compose.material.icons.rounded.SortByAlpha
import androidx.compose.material3.Button
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BleFilterScreen(
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BleFilterViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
BleFilterContract.Effect.Navigation.Up -> navigator.popBackStack()
}
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text("Фильтр BLE")
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnResetFilter)
}
) {
Icon(
imageVector = Icons.Rounded.Restore,
contentDescription = null
)
}
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when(state){
is BleFilterContract.State.Display -> DisplayState(viewModel, state)
is BleFilterContract.State.Loading -> LoadingState()
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
ContainedLoadingIndicator()
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DisplayState(
viewModel: BleFilterViewModel,
state: BleFilterContract.State.Display
){
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(16.dp)
.verticalScroll(rememberScrollState())
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(16.dp)
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
state.filter.sortField.localized,
TextRange(state.filter.sortField.localized.length)
)
ExposedDropdownMenuBox(
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded,
onExpandedChange = {
expanded = it
}
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Сортировка") },
leadingIcon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.Sort,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
BleFilter.Field.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(
sortField = selectionOption
)
))
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFieldState = TextFieldState(
state.filter.sortOrder.localized,
TextRange(state.filter.sortOrder.localized.length)
)
ExposedDropdownMenuBox(
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded,
onExpandedChange = {
expanded = it
}
) {
OutlinedTextField(
state = sortTextFieldState,
readOnly = true,
label = { Text("Напрвление сортировки") },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.SortByAlpha,
contentDescription = null,
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
BleFilter.Order.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(sortOrder = selectionOption)
))
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val typeTextFiledState = TextFieldState(
state.filter.bleType.localized,
TextRange(state.filter.bleType.localized.length)
)
ExposedDropdownMenuBox(
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
expanded = expanded,
onExpandedChange = {
expanded = it
}
) {
OutlinedTextField(
state = typeTextFiledState,
lineLimits = TextFieldLineLimits.SingleLine,
readOnly = true,
label = { Text("Тип") },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Bluetooth,
contentDescription = null
)
},
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
mutableListOf<BleInfo.Type?>(null).apply {
addAll(BleInfo.Type.entries.toTypedArray())
}.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(bleType = selectionOption)
))
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
OutlinedTextField(
value = state.filter.name,
singleLine = true,
onValueChange = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(name = it)
))
},
label = { Text(text = "Имя") },
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null
)
},
trailingIcon = {
if(state.filter.name.isNotEmpty()) {
IconButton(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(name = "")
))
}
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null
)
}
}
},
modifier = Modifier
.padding(end = 8.dp)
.fillMaxWidth()
)
OutlinedTextField(
value = state.filter.mac,
singleLine = true,
onValueChange = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(mac = it)
))
},
label = { Text(text = "Mac") },
leadingIcon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ShortText,
contentDescription = null,
)
},
trailingIcon = {
if (state.filter.mac.isNotEmpty()) {
IconButton(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnFilterChanged(
state.filter.copy(mac = "")
))
}
) {
Icon(
imageVector = Icons.Rounded.Close,
contentDescription = null
)
}
}
},
modifier = Modifier
.padding(end = 8.dp)
.fillMaxWidth()
)
}
}
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = RoundedCornerShape(16.dp)
) {
Column(
modifier = Modifier
.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.SignalCellularAlt,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Column {
RangeSlider(
value = state.filter.rssi,
onValueChange = {
viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(rssi = it)
)
)
},
valueRange = (-100f)..(-10f),
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.primary,
inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
)
)
Row {
Text(text = state.filter.rssi.start.toInt().toString() + " dBm")
Spacer(modifier = Modifier.weight(1f))
Text(text = state.filter.rssi.endInclusive.toInt().toString() + " dBm")
}
}
}
Spacer(modifier = Modifier.height(12.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.BatteryFull,
contentDescription = null,
modifier = Modifier.padding(12.dp)
)
Column(
verticalArrangement = Arrangement.Center
) {
RangeSlider(
value = state.filter.battery,
onValueChange = {
viewModel.setEvent(
BleFilterContract.Event.OnFilterChanged(
state.filter.copy(battery = it)
)
)
},
valueRange = 0f..100f,
colors = SliderDefaults.colors(
activeTickColor = MaterialTheme.colorScheme.primary,
inactiveTickColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.38f)
)
)
Row {
Text(text = state.filter.battery.start.toInt().toString() + " %")
Spacer(modifier = Modifier.weight(1f))
Text(text = state.filter.battery.endInclusive.toInt().toString() + " %")
}
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = {
viewModel.setEvent(BleFilterContract.Event.OnSave)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Применить"
)
}
}
}

View File

@ -0,0 +1,98 @@
package llc.arma.ble.app.ui.screen.filter
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.usecase.filter.GetFilterFlow
import llc.arma.ble.domain.usecase.filter.SaveFilter
import javax.inject.Inject
@HiltViewModel
class BleFilterViewModel @Inject constructor(
private val getFilterFlow: GetFilterFlow,
private val saveFilter: SaveFilter
) : BaseViewModel<BleFilterContract.State, BleFilterContract.Event, BleFilterContract.Effect>() {
init {
viewModelScope.launch {
val filter = getFilterFlow.invoke().firstOrNull() ?: BleFilter()
setState { BleFilterContract.State.Display(filter) }
}
}
override fun setInitialState() = BleFilterContract.State.Loading
override fun handleEvents(event: BleFilterContract.Event) {
when(event){
is BleFilterContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is BleFilterContract.Event.OnSave -> reduce(viewState.value, event)
is BleFilterContract.Event.OnFilterChanged -> reduce(viewState.value, event)
is BleFilterContract.Event.OnResetFilter -> reduce(viewState.value, event)
}
}
private fun reduce(
state: BleFilterContract.State,
event: BleFilterContract.Event.OnNavigateUp,
) {
setEffect {
BleFilterContract.Effect.Navigation.Up
}
}
private fun reduce(
state: BleFilterContract.State,
event: BleFilterContract.Event.OnSave,
) {
if(state is BleFilterContract.State.Display) {
viewModelScope.launch {
saveFilter(state.filter)
setEffect {
BleFilterContract.Effect.Navigation.Up
}
}
}
}
private fun reduce(
state: BleFilterContract.State,
event: BleFilterContract.Event.OnFilterChanged,
) {
setState { BleFilterContract.State.Display(event.filter) }
}
private fun reduce(
state: BleFilterContract.State,
event: BleFilterContract.Event.OnResetFilter,
) {
viewModelScope.launch {
saveFilter(BleFilter())
setEffect {
BleFilterContract.Effect.Navigation.Up
}
}
}
}

View File

@ -1,223 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerContract {
sealed class Event : ViewEvent {
object OnShowAccelerometerAccel : Event()
object OnHideAccelerometerAccel : Event()
object OnHideAccelerometerSpectre : Event()
object OnShowAccelerometerHistory : Event()
object OnHideAccelerometerHistory : Event()
object OnRealtimeViewModeEdit : Event()
data class OnAccelViewModeEdit(
val next: Next
) : Event()
enum class Next {
ACCEL, HISTORY
}
data class OnAccelScaleEdit(
val next: Next
) : Event()
object OnAccelEdit : Event()
object OnHistoryEdit : Event()
object OnFftFrequencyEdit : Event()
object OnFftAxisEdit : Event()
object OnFftModeEdit : Event()
object OnPowerEdit : Event()
object OnShowWriteBlePreview : Event()
object OnWriteBle : Event()
object OnHideWriteBlePreview : Event()
object OnChangePassword : Event()
object OnSaveIntervalEdit : Event()
object OnReadIntervalEdit : Event()
object OnHideHistoryEdit : Event()
data class OnRealtimeViewModeChanged(
val mode: RealtimeViewMode
) : Event()
data class OnBleChanged(
val ble: Ble.Accelerometer,
): Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
data class OnAccelViewModelChanged(
val mode: AccelViewMode
) : Event()
data class OnHistoryViewModeChanged(
val mode: AccelViewMode
) : Event()
data class OnFftFrequencyChanged(
val frequency: FftFrequency
) : Event()
data class OnFftAxisChanged(
val axis: FftAxis
) : Event()
data class OnFftModeChanged(
val mode: FftViewMode
) : Event()
data class OnAccelScaleChanged(
val scale: AccelScale
) : Event()
data class OnHistoryScaleChanged(
val scale: AccelScale
) : Event()
data class OnSaveHistoryChanged(
val save: Boolean
) : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
}
sealed class State : ViewState {
object Loading : State()
data class Display(
val origin: Ble.Accelerometer,
val accelerometer: BleView.Accelerometer,
val writeState: WriteState?,
val accelViewMode: AccelViewMode,
val accelRealtimeViewMode: RealtimeViewMode,
val accelScale: AccelScale,
val fftViewMode: FftViewMode,
val fftAxis: FftAxis,
val fftFrequency: FftFrequency,
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
object Success : WriteState()
object Failure : WriteState()
}
}
}
sealed class Effect : ViewSideEffect {
object ShowRealtimeViewModeEdit : Effect()
object ShowPowerPicker : Effect()
object HidePowerPicker : Effect()
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
object HideHistoryEdit : Effect()
data class ShowAccelViewEdit(
val next: Event.Next
) : Effect()
data class ShowAccelScaleEdit(
val next: Event.Next
) : Effect()
object ShowAccelEdit : Effect()
object HideAccelEdit : Effect()
object ShowFftFrequencyEdit : Effect()
object ShowFftAxisEdit : Effect()
object ShowFftModeEdit : Effect()
object HideIntervalPicker : Effect()
object ShowIntervalPicker : Effect()
object HideReadIntervalPicker : Effect()
object ShowReadIntervalPicker : Effect()
object ShowHistoryEdit : Effect()
sealed class Navigation : Effect() {
object NavigateToChangePassword : Navigation()
data class NavigateToAccelHistory(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Navigation()
data class NavigateToAccelRealtime(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Navigation()
data class NavigateToAccelSpectre(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Navigation()
}
}
}

View File

@ -1,415 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import androidx.compose.foundation.layout.Column
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.TxLevelSelector
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFftAxisEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFftModeEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelFrequencyEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelRealtimeViewEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelScaleEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.AccelViewEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.HistoryEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.IntervalEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.ReadIntervalEdit
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.Write
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
ACCEL_SCALE, SPECTRE_SCALE, HISTORY_MODE_EDIT, HISTORY_SCALE, HISTORY_EDIT, ACCEL_EDIT, ACCEL_REALTIME_EDIT, POWER, WRITE, ACCEL_MODE_EDIT, SPECTRE_MODE_EDIT, FREQUENCY_EDIT, AXIS_EDIT, FFT_MODE_EDIT, INTERVAL_EDIT, READ_INTERVAL_EDIT
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun AccelerometerScreen(
ble: Ble.Accelerometer,
onEvent: (AccelerometerContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
LaunchedEffect(ble){
viewModel.setEvent(AccelerometerContract.Event.OnBleChanged(ble))
}
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
LaunchedEffect(
key1 = bottomDialog.sheetState?.currentValue,
block = {
if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) {
bottomDialog.setContent {}
sheetPage = null
}
}
)
val scope = rememberCoroutineScope()
LaunchedEffect(sheetPage) {
when (sheetPage) {
SheetPage.POWER -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
TxLevelSelector(
tx = currentState.accelerometer.state.tx,
onSelect = {
viewModel.setEvent(AccelerometerContract.Event.OnPowerChanged(it))
}
)
}
}
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = viewModel::setEvent
)
}
}
}
SheetPage.ACCEL_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.SPECTRE_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.HISTORY_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelViewEdit(
next = AccelerometerContract.Event.Next.HISTORY,
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.FREQUENCY_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFrequencyEdit(
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.AXIS_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFftAxisEdit(
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.FFT_MODE_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelFftModeEdit(
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
IntervalEdit(
state = currentState.accelerometer,
onEvent = viewModel::setEvent
)
}
}
SheetPage.ACCEL_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.SPECTRE_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.ACCEL,
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.HISTORY_SCALE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelScaleEdit(
next = AccelerometerContract.Event.Next.HISTORY,
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.ACCEL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelEdit(
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.HISTORY_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
HistoryEdit(
state = currentState,
onEvent = viewModel::setEvent
)
}
}
null -> {
bottomDialog.hide()
}
SheetPage.ACCEL_REALTIME_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
AccelRealtimeViewEdit(
state = currentState,
onEvent = viewModel::setEvent
)
}
}
SheetPage.READ_INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is AccelerometerContract.State.Display) {
ReadIntervalEdit(
state = currentState.accelerometer,
onEvent = viewModel::setEvent
)
}
}
}
}
DisposableEffect(key1 = Unit, effect = {
onDispose {
scope.launch {
bottomDialog.hide()
}
}
})
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is AccelerometerContract.Effect.HidePowerPicker -> launch {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER
}
is AccelerometerContract.Effect.HideWriteBle -> launch {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowWriteBle -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is AccelerometerContract.Effect.ShowAccelViewEdit -> launch {
sheetPage = null
delay(100)
sheetPage = when(it.next){
AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_MODE_EDIT
AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_MODE_EDIT
}
}
is AccelerometerContract.Effect.ShowFftFrequencyEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.FREQUENCY_EDIT
}
is AccelerometerContract.Effect.ShowFftAxisEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.AXIS_EDIT
}
is AccelerometerContract.Effect.ShowFftModeEdit -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.FFT_MODE_EDIT
}
is AccelerometerContract.Effect.HideIntervalPicker -> {
sheetPage = null
delay(100)
}
is AccelerometerContract.Effect.ShowIntervalPicker -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.INTERVAL_EDIT
}
is AccelerometerContract.Effect.ShowAccelScaleEdit -> {
sheetPage = null
delay(100)
sheetPage = when(it.next){
AccelerometerContract.Event.Next.ACCEL -> SheetPage.ACCEL_SCALE
AccelerometerContract.Event.Next.HISTORY -> SheetPage.HISTORY_SCALE
}
}
is AccelerometerContract.Effect.ShowAccelEdit -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.ACCEL_EDIT
}
is AccelerometerContract.Effect.Navigation -> {
onEvent(it)
}
is AccelerometerContract.Effect.ShowHistoryEdit -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.HISTORY_EDIT
}
is AccelerometerContract.Effect.HideHistoryEdit -> {
sheetPage = null
delay(100)
}
AccelerometerContract.Effect.ShowRealtimeViewModeEdit -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.ACCEL_REALTIME_EDIT
}
AccelerometerContract.Effect.HideAccelEdit -> {
sheetPage = null
delay(100)
}
AccelerometerContract.Effect.HideReadIntervalPicker -> {
sheetPage = null
delay(100)
}
AccelerometerContract.Effect.ShowReadIntervalPicker -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.READ_INTERVAL_EDIT
}
}
}.launchIn(this)
}
Column {
when(state){
is AccelerometerContract.State.Display -> {
DisplayState(
origin = state.origin,
ble = state.accelerometer,
onEvent = viewModel::setEvent
)
}
is AccelerometerContract.State.Loading -> LoadingState()
}
}
}

View File

@ -1,703 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class AccelerometerViewModel @Inject constructor(
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
override fun setInitialState() = AccelerometerContract.State.Loading
override fun handleEvents(event: AccelerometerContract.Event) {
when(event){
is AccelerometerContract.Event.OnBleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerAccel -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerAccel -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerSpectre -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelViewModeEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelViewModelChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftFrequencyEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftAxisChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftFrequencyChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftModeChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftAxisEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnFftModeEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelScaleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelScaleEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnAccelEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnRealtimeViewModeEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnRealtimeViewModeChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHistoryEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHistoryScaleChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHistoryViewModeChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnHideHistoryEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnReadIntervalChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnReadIntervalChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.readInterval = event.interval
}
setEffect {
AccelerometerContract.Effect.HideReadIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnReadIntervalEdit
) {
setEffect {
AccelerometerContract.Effect.ShowReadIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideHistoryEdit
) {
setEffect {
AccelerometerContract.Effect.HideHistoryEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHistoryViewModeChanged
) {
if(state is AccelerometerContract.State.Display){
var saveHistory = state.accelerometer.accelerometerState.saveHistory
if(saveHistory is Ble.Accelerometer.HistorySettings.Enabled){
saveHistory = Ble.Accelerometer.HistorySettings.Enabled(
mode = event.mode,
scale = saveHistory.scale,
detailed = saveHistory.detailed
)
}
state.accelerometer.accelerometerState.saveHistory = saveHistory
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHistoryScaleChanged
) {
if(state is AccelerometerContract.State.Display){
var saveHistory = state.accelerometer.accelerometerState.saveHistory
if(saveHistory is Ble.Accelerometer.HistorySettings.Enabled){
saveHistory = saveHistory.copy(scale = event.scale)
}
state.accelerometer.accelerometerState.saveHistory = saveHistory
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHistoryEdit
) {
setEffect {
AccelerometerContract.Effect.ShowHistoryEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelScaleChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
accelScale = event.scale
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelScaleEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelScaleEdit(
event.next
)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalEdit
) {
setEffect {
AccelerometerContract.Effect.ShowIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.historyInterval = event.interval
}
setEffect {
AccelerometerContract.Effect.HideIntervalPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnChangePassword
) {
setEffect {
AccelerometerContract.Effect.Navigation.NavigateToChangePassword
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveHistoryChanged
) {
if(state is AccelerometerContract.State.Display) {
if(event.save){
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Enabled(
scale = AccelScale.S_2,
mode = AccelViewMode.ACCELERATION,
detailed = true
)
setEffect {
AccelerometerContract.Effect.ShowHistoryEdit
}
} else {
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Disabled
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftModeEdit
) {
setEffect {
AccelerometerContract.Effect.ShowFftModeEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftAxisEdit
) {
setEffect {
AccelerometerContract.Effect.ShowFftAxisEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftAxisChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
fftAxis = event.axis
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftModeChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
fftViewMode = event.mode
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftFrequencyChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
fftFrequency = event.frequency
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnFftFrequencyEdit
) {
setEffect {
AccelerometerContract.Effect.ShowFftFrequencyEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnRealtimeViewModeEdit
) {
setEffect {
AccelerometerContract.Effect.ShowRealtimeViewModeEdit
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnRealtimeViewModeChanged
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
accelRealtimeViewMode = event.mode
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerSpectre
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerHistory
) {
/*setEffect {
AccelerometerContract.Effect.ShowAccelerometerHistory
}*/
if (state is AccelerometerContract.State.Display &&
state.origin.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled
) {
setEffect {
AccelerometerContract.Effect.Navigation.NavigateToAccelHistory(
ble = state.accelerometer.info,
accelMode = state.origin.accelerometerState.saveHistorySettings.mode,
fftAxis = state.fftAxis,
fftMode = state.fftViewMode,
frequency = state.fftFrequency,
accelScale = state.accelScale
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerHistory
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
setState {
state.copy(
writeState = null
)
}
}
setEffect {
AccelerometerContract.Effect.HideWriteBle
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer
val writeRequest = Ble.Accelerometer.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
saveHistorySettings = if(newBle.accelerometerState.saveHistorySettings == state.origin.accelerometerState.saveHistorySettings) null else newBle.accelerometerState.saveHistorySettings,
historyInterval = if(newBle.accelerometerState.historyInterval == state.origin.accelerometerState.historyInterval) null else newBle.accelerometerState.historyInterval,
readInterval = if(newBle.accelerometerState.readInterval == state.origin.accelerometerState.readInterval) null else newBle.accelerometerState.readInterval,
)
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
AccelerometerContract.Effect.ShowWriteBle
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.state.tx = event.tx
}
setEffect {
AccelerometerContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelViewModelChanged
) {
if(state is AccelerometerContract.State.Display) {
setState {
state.copy(
accelViewMode = event.mode
)
}
}
setEffect {
AccelerometerContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerEdit
) {
setEffect {
AccelerometerContract.Effect.ShowPowerPicker
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnAccelViewModeEdit
) {
setEffect {
AccelerometerContract.Effect.ShowAccelViewEdit(event.next)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnHideAccelerometerAccel
) {
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerAccel
) {
viewModelScope.launch {
if(state is AccelerometerContract.State.Display){
/*setEffect {
AccelerometerContract.Effect.ShowAccelerometerAccel
}*/
setEffect {
AccelerometerContract.Effect.HideAccelEdit
}
setEffect {
when (state.accelRealtimeViewMode) {
is RealtimeViewMode.Accel -> {
AccelerometerContract.Effect.Navigation.NavigateToAccelRealtime(
ble = state.accelerometer.info,
accelMode = state.accelRealtimeViewMode.accelViewMode,
fftAxis = state.fftAxis,
fftMode = state.fftViewMode,
frequency = state.fftFrequency,
accelScale = state.accelScale
)
}
is RealtimeViewMode.Spectre -> {
AccelerometerContract.Effect.Navigation.NavigateToAccelSpectre(
ble = state.accelerometer.info,
accelMode = state.accelViewMode,
fftAxis = state.fftAxis,
fftMode = state.fftViewMode,
frequency = state.fftFrequency,
accelScale = state.accelScale
)
}
}
}
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnBleChanged
) {
when (state) {
is AccelerometerContract.State.Display -> setState {
state.copy(
origin = Ble.Accelerometer(
info = event.ble.info,
state = event.ble.state,
accelerometerState = state.origin.accelerometerState
)
)
}
is AccelerometerContract.State.Loading -> setState {
AccelerometerContract.State.Display(
origin = event.ble,
accelerometer = bleMapper.map(event.ble) as BleView.Accelerometer,
writeState = null,
accelViewMode = AccelViewMode.ACCELERATION,
accelRealtimeViewMode = RealtimeViewMode.Accel(AccelViewMode.ACCELERATION),
fftAxis = FftAxis.AUTO,
fftFrequency = FftFrequency.F_400,
fftViewMode = FftViewMode.SPECTRE,
accelScale = AccelScale.S_2
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle
) {
if(state is AccelerometerContract.State.Display){
state.writeState?.let { request ->
if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Writing(
request.writeRequest
)
)
}
writeBle(state.accelerometer.info.serial, request.writeRequest).fold(
onSuccess = {
val currentState = viewState.value
if(currentState is AccelerometerContract.State.Display) {
val newBleObject = Ble.Accelerometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
accelerometerState = currentState.origin.accelerometerState.copy(
saveHistorySettings = request.writeRequest.saveHistorySettings
?: currentState.origin.accelerometerState.saveHistorySettings
)
)
setState {
currentState.copy(
origin = newBleObject,
writeState = AccelerometerContract.State.Display.WriteState.Success
)
}
}
},
onFailure = {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}

View File

@ -0,0 +1,219 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
@Serializable
data class AccelerometerHistoryFormData(
val mode: AccelViewMode,
val scale: AccelScale
)
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerHistoryForm(
resultNavigator: ResultBackNavigator<AccelerometerHistoryFormData>
) {
var mode by remember { mutableStateOf(AccelViewMode.entries.first()) }
var scale by remember { mutableStateOf(AccelScale.entries.first()) }
Surface(
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(20.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
mode.localized,
TextRange(mode.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Accel view mode") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
AccelViewMode.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
mode = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
scale.localized,
TextRange(scale.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp)
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Accel scale") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
AccelScale.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
scale = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
OutlinedButton(
onClick = {
resultNavigator.navigateBack()
}
) {
Text(
text = "Отмена"
)
}
Button(
onClick = {
resultNavigator.navigateBack(AccelerometerHistoryFormData(mode, scale))
}
) {
Text(
text = "Сохранить"
)
}
}
}
}
}

View File

@ -0,0 +1,42 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.main
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
class AccelerometerHistoryContract {
sealed class Event : ViewEvent {
data object StopMeasure : Event()
data object OnExport : Event()
data class OnStart(
val serial: String,
) : Event()
data class OnRefreshHistory(
val serial: String
) : Event()
}
sealed class State : ViewState {
data class Display(
val bleName: String,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}

View File

@ -0,0 +1,143 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.main
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.usecase.ExportToXlsx
import llc.arma.ble.domain.usecase.GetAccelerometerHistoryBySerial
import javax.inject.Inject
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
private val exportToXlsx: ExportToXlsx,
) : BaseViewModel<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
private var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerHistoryContract.State.Display(
"",
ProgressState.Indeterminate
)
override fun handleEvents(event: AccelerometerHistoryContract.Event) {
when(event){
is AccelerometerHistoryContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnExport -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnExport
) {
if(state is AccelerometerHistoryContract.State.Display){
if(state.loadingHistoryState is ProgressState.Finished){
exportToXlsx.invoke(state.bleName, state.loadingHistoryState.data)
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerHistoryContract.State.Exception
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerHistoryContract.State.Display) {
if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerHistoryContract.State.Display(
"event.bleName",
ProgressState.Indeterminate
)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display("event.bleName", it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display("", ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display("event.bleName", it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
package llc.arma.ble.app.ui.screen.inspection.accelerometer.history.main
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.CloudUpload
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.TableView
@ -31,7 +30,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
@ -50,30 +48,14 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.screen.locale.localized
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.ExportToXlsx
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerHistoryBySerial
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class AccelEntry(
val localDate: Long,
@ -85,43 +67,43 @@ class AccelEntry(
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerHistory(
ble: BleInfo,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
onDismiss: (() -> Unit)? = null,
onShowStatistic: () -> Unit,
bleSerial: String,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
LaunchedEffect(bleSerial) {
viewModel.setEvent(
AccelerometerHistoryContract.Event.OnStart(bleSerial)
)
}
Column {
TopAppBar(
navigationIcon = {
onDismiss?.let {
IconButton(onClick = it) {
IconButton(
onClick = {
navigator.popBackStack()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
},
title = {
val title = when(state){
val title = /*when(state){
is AccelerometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})"
@ -130,7 +112,7 @@ fun AccelerometerHistory(
}
}
AccelerometerHistoryContract.State.Exception -> accelMode.localized
}
}*/ ""
Text(
modifier = Modifier.weight(1f),
@ -141,7 +123,7 @@ fun AccelerometerHistory(
actions = {
IconButton(
onClick = onShowStatistic,
onClick = {} ,
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> false
@ -170,7 +152,9 @@ fun AccelerometerHistory(
IconButton(
onClick = {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
viewModel.setEvent(
AccelerometerHistoryContract.Event.OnRefreshHistory(bleSerial)
)
},
enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
@ -190,8 +174,8 @@ fun AccelerometerHistory(
Box(modifier = Modifier) {
when (state) {
is AccelerometerHistoryContract.State.Display -> Display(state = state)
is AccelerometerHistoryContract.State.Exception -> Exception()
is AccelerometerHistoryContract.State.Display -> DisplayState(state = state)
is AccelerometerHistoryContract.State.Exception -> ErrorState()
}
}
@ -206,11 +190,12 @@ val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
@Composable
fun Display(
private fun DisplayState(
state: AccelerometerHistoryContract.State.Display
) {
Box(modifier = Modifier
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
@ -514,7 +499,7 @@ fun Display(
}
@Composable
private fun Exception() {
private fun ErrorState() {
Box(
modifier = Modifier
.padding(8.dp)
@ -532,178 +517,5 @@ private fun Exception() {
}
class AccelerometerHistoryContract {
sealed class Event : ViewEvent {
data object StopMeasure : Event()
data object OnExport : Event()
data class OnStart(
val bleName: String,
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
data class OnRefreshHistory(
val bleName: String,
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
}
sealed class State : ViewState {
data class Display(
val bleName: String,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerHistoryViewModel @Inject constructor(
private val getAccelerometerHistoryBySerial: GetAccelerometerHistoryBySerial,
private val exportToXlsx: ExportToXlsx,
) : BaseViewModel<AccelerometerHistoryContract.State, AccelerometerHistoryContract.Event, AccelerometerHistoryContract.Effect>() {
private var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerHistoryContract.State.Display(
"",
ProgressState.Indeterminate
)
override fun handleEvents(event: AccelerometerHistoryContract.Event) {
when(event){
is AccelerometerHistoryContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
is AccelerometerHistoryContract.Event.OnExport -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnExport
) {
if(state is AccelerometerHistoryContract.State.Display){
if(state.loadingHistoryState is ProgressState.Finished){
exportToXlsx.invoke(state.bleName, state.loadingHistoryState.data)
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerHistoryContract.State.Exception
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerHistoryContract.State.Display) {
if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
AccelerometerHistoryContract.State.Display(event.bleName, ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(event.bleName, it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}
private fun reduce(
state: AccelerometerHistoryContract.State,
event: AccelerometerHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display("", ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
measureJob = getAccelerometerHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(event.bleName, it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

@ -0,0 +1,137 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerContract {
sealed class Event : ViewEvent {
data object OnShowChart : Event()
data object OnShowAccelerometerHistory : Event()
data object OnShowRealtimeForm : Event()
data object OnPowerEdit : Event()
data object OnShowWriteBlePreview : Event()
data object OnWriteBle : Event()
data object OnChangePassword : Event()
data object OnSaveIntervalEdit : Event()
data object OnReadIntervalEdit : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
data object OnShowHistoryForm : Event()
data object OnDisableSaveHistory : Event()
data class OnEnableSaveHistory(
val mode: AccelViewMode,
val scale: AccelScale
) : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data class OnReadIntervalChanged(
val interval: Long
) : Event()
}
sealed class State : ViewState {
data object Loading : State()
data class Display(
val origin: Ble.Accelerometer,
val accelerometer: BleView.Accelerometer,
val writeState: WriteState?,
val accelViewMode: AccelViewMode,
val accelRealtimeViewMode: RealtimeViewMode,
val accelScale: AccelScale,
val fftViewMode: FftViewMode,
val fftAxis: FftAxis,
val fftFrequency: FftFrequency,
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Accelerometer.WriteRequest
) : WriteState()
data object Success : WriteState()
data object Failure : WriteState()
}
}
}
sealed class Effect : ViewSideEffect {
object ShowWriteBle : Effect()
sealed class Navigation : Effect() {
data object ShowRealtimeForm : Effect()
data object ShowHistoryForm : Effect()
data class SaveIntervalSelector(
val interval: Int
) : Navigation()
data class ReadIntervalSelector(
val interval: Int
) : Navigation()
data class TxPowerSelector(
val tx: BleView.BleState.TX
) : Navigation()
data class ChangePassword(
val serial: String
) : Navigation()
data class AccelHistory(
val ble: BleInfo,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Navigation()
}
}
}

View File

@ -0,0 +1,221 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerHistoryFormDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeFormDestination
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.history.form.AccelerometerHistoryFormData
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.Write
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerScreen(
navigator: DestinationsNavigator,
bleSerial: String,
historyFormResult: ResultRecipient<AccelerometerHistoryFormDestination, AccelerometerHistoryFormData>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
) {
val viewModel = hiltViewModel<AccelerometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
historyFormResult.onResult {
viewModel.setEvent(AccelerometerContract.Event.OnEnableSaveHistory(it.mode, it.scale))
}
txSelectResult.onResult {
viewModel.setEvent(AccelerometerContract.Event.OnPowerChanged(it))
}
readDurationSelectResult.onResult {
if(it.qualifier == "ReadIntervalSelector"){
viewModel.setEvent(AccelerometerContract.Event.OnReadIntervalChanged(it.duration.toLong()))
}
if(it.qualifier == "SaveIntervalSelector"){
viewModel.setEvent(AccelerometerContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
}
}
LaunchedEffect(
key1 = bottomDialog.sheetState?.currentValue,
block = {
if(bottomDialog.sheetState?.currentValue == ModalBottomSheetValue.Hidden) {
bottomDialog.setContent {}
sheetPage = null
}
}
)
val scope = rememberCoroutineScope()
LaunchedEffect(sheetPage) {
when (sheetPage) {
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is AccelerometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = viewModel::setEvent
)
}
}
}
null -> {
bottomDialog.hide()
}
}
}
DisposableEffect(Unit){
onDispose {
scope.launch {
bottomDialog.hide()
}
}
}
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is AccelerometerContract.Effect.ShowWriteBle -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is AccelerometerContract.Effect.Navigation.AccelHistory ->
navigator.navigate(AccelerometerHistoryDestination(it.ble.serial))
is AccelerometerContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.serial))
is AccelerometerContract.Effect.Navigation.ReadIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "ReadIntervalSelector",
duration = it.interval
))
is AccelerometerContract.Effect.Navigation.SaveIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "SaveIntervalSelector",
duration = it.interval
))
is AccelerometerContract.Effect.Navigation.TxPowerSelector ->
navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
AccelerometerContract.Effect.Navigation.ShowHistoryForm ->
navigator.navigate(AccelerometerHistoryFormDestination())
AccelerometerContract.Effect.Navigation.ShowRealtimeForm ->
navigator.navigate(AccelerometerRealtimeFormDestination(bleSerial))
}
}.launchIn(this)
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
navigator.popBackStack()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(text = BleInfo.Type.ACCELEROMETER.localized)
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when(state){
is AccelerometerContract.State.Display -> {
DisplayState(
origin = state.origin,
ble = state.accelerometer,
onEvent = viewModel::setEvent
)
}
is AccelerometerContract.State.Loading -> LoadingState()
}
}
}
}

View File

@ -0,0 +1,389 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.AccelerometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class AccelerometerViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<AccelerometerContract.State, AccelerometerContract.Event, AccelerometerContract.Effect>() {
init {
val params = AccelerometerScreenDestination.argsFrom(savedStateHandle)
viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { it },
onFailure = { null }
)
if(ble != null && ble is Ble.Accelerometer){
setState {
when(this){
is AccelerometerContract.State.Display -> {
copy(
origin = Ble.Accelerometer(
info = ble.info,
state = origin.state,
accelerometerState = origin.accelerometerState
)
)
}
AccelerometerContract.State.Loading -> {
AccelerometerContract.State.Display(
origin = ble,
accelerometer = bleMapper.map(ble) as BleView.Accelerometer,
writeState = null,
accelViewMode = AccelViewMode.ACCELERATION,
accelRealtimeViewMode = RealtimeViewMode.Accel(
AccelViewMode.ACCELERATION
),
fftAxis = FftAxis.AUTO,
fftFrequency = FftFrequency.F_400,
fftViewMode = FftViewMode.SPECTRE,
accelScale = AccelScale.S_2
)
}
}
}
}
}
}
override fun setInitialState() = AccelerometerContract.State.Loading
override fun handleEvents(event: AccelerometerContract.Event) {
when(event){
is AccelerometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnDisableSaveHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowAccelerometerHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnReadIntervalChanged -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnReadIntervalEdit -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnEnableSaveHistory -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowHistoryForm -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowChart -> reduce(viewState.value, event)
is AccelerometerContract.Event.OnShowRealtimeForm -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowChart
) {
setEffect {
AccelerometerContract.Effect.Navigation.ShowHistoryForm
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowRealtimeForm
) {
setEffect {
AccelerometerContract.Effect.Navigation.ShowRealtimeForm
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowHistoryForm
) {
setEffect {
AccelerometerContract.Effect.Navigation.ShowHistoryForm
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnReadIntervalChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.readInterval = event.interval
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnReadIntervalEdit
) {
if(state is AccelerometerContract.State.Display) {
setEffect {
AccelerometerContract.Effect.Navigation.ReadIntervalSelector(
state.accelerometer.accelerometerState.readInterval.toInt()
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalEdit
) {
if(state is AccelerometerContract.State.Display) {
setEffect {
AccelerometerContract.Effect.Navigation.SaveIntervalSelector(
state.accelerometer.accelerometerState.historyInterval.toInt()
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnSaveIntervalChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.historyInterval = event.interval
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnChangePassword
) {
if(state is AccelerometerContract.State.Display) {
setEffect {
AccelerometerContract.Effect.Navigation.ChangePassword(state.accelerometer.info.serial)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnDisableSaveHistory
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Disabled
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnEnableSaveHistory
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.accelerometerState.saveHistory = Ble.Accelerometer.HistorySettings.Enabled(
scale = event.scale,
mode = event.mode,
detailed = true
)
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowAccelerometerHistory
) {
if (state is AccelerometerContract.State.Display &&
state.origin.accelerometerState.saveHistorySettings is Ble.Accelerometer.HistorySettings.Enabled
) {
setEffect {
AccelerometerContract.Effect.Navigation.AccelHistory(
ble = state.accelerometer.info,
accelMode = state.origin.accelerometerState.saveHistorySettings.mode,
fftAxis = state.fftAxis,
fftMode = state.fftViewMode,
frequency = state.fftFrequency,
accelScale = state.accelScale
)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnShowWriteBlePreview
) {
if(state is AccelerometerContract.State.Display){
val newBle = bleViewMapper.map(state.accelerometer) as Ble.Accelerometer
val writeRequest = Ble.Accelerometer.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
saveHistorySettings = if(newBle.accelerometerState.saveHistorySettings == state.origin.accelerometerState.saveHistorySettings) null else newBle.accelerometerState.saveHistorySettings,
historyInterval = if(newBle.accelerometerState.historyInterval == state.origin.accelerometerState.historyInterval) null else newBle.accelerometerState.historyInterval,
readInterval = if(newBle.accelerometerState.readInterval == state.origin.accelerometerState.readInterval) null else newBle.accelerometerState.readInterval,
)
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
AccelerometerContract.Effect.ShowWriteBle
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerChanged
) {
if(state is AccelerometerContract.State.Display) {
state.accelerometer.state.tx = event.tx
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnPowerEdit
) {
if(state is AccelerometerContract.State.Display) {
setEffect {
AccelerometerContract.Effect.Navigation.TxPowerSelector(state.accelerometer.state.tx)
}
}
}
private fun reduce(
state: AccelerometerContract.State,
event: AccelerometerContract.Event.OnWriteBle
) {
if(state is AccelerometerContract.State.Display){
state.writeState?.let { request ->
if(request is AccelerometerContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Writing(
request.writeRequest
)
)
}
writeBle(state.accelerometer.info.serial, request.writeRequest).fold(
onSuccess = {
val currentState = viewState.value
if(currentState is AccelerometerContract.State.Display) {
val newBleObject = Ble.Accelerometer(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
accelerometerState = currentState.origin.accelerometerState.copy(
saveHistorySettings = request.writeRequest.saveHistorySettings
?: currentState.origin.accelerometerState.saveHistorySettings
)
)
setState {
currentState.copy(
origin = newBleObject,
writeState = AccelerometerContract.State.Display.WriteState.Success
)
}
}
},
onFailure = {
setState {
state.copy(
writeState = AccelerometerContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -17,7 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.FftAxis
@ -45,44 +45,3 @@ fun SelectorItem(
}
}
@Composable
fun AccelFftAxisEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft axis",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftAxis.entries.forEach {
SelectorItem(
label = it.localized,
selected = it == state.fftAxis
){
onEvent(AccelerometerContract.Event.OnFftAxisChanged(it))
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
}
}

View File

@ -0,0 +1,49 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.AccelViewMode
@Serializable
sealed class RealtimeViewMode {
companion object {
val entries: List<RealtimeViewMode>
get() {
return AccelViewMode.entries.map {
Accel(it)
} + Spectre
}
}
data class Accel(
val accelViewMode: AccelViewMode
): RealtimeViewMode()
data object Spectre : RealtimeViewMode()
}

View File

@ -0,0 +1,201 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.domain.model.Ble
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@Composable
fun DisplayState(
onEvent: (AccelerometerContract.Event) -> Unit,
origin: Ble.Accelerometer,
ble: BleView.Accelerometer
) {
val scrollState = rememberScrollState()
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.padding(horizontal = 16.dp)
.verticalScroll(scrollState)
.weight(1f)
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
onEvent(AccelerometerContract.Event.OnPowerEdit)
}
val history = ble.accelerometerState.saveHistory
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Сохранять измерения",
subtitle = if (history is Ble.Accelerometer.HistorySettings.Enabled) {
"View mode ${history.mode.localized} Scale: ${history.scale.localized}"
} else {
""
},
icon = {
Switch(
checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled,
onCheckedChange = {
if(it){
onEvent(AccelerometerContract.Event.OnShowHistoryForm)
} else {
onEvent(AccelerometerContract.Event.OnDisableSaveHistory)
}
}
)
}
)
if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал измерений",
subtitle = ble.accelerometerState.historyInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
onEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
}
}
if (ble.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) {
if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал чтения",
subtitle = ble.accelerometerState.readInterval
.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
onEvent(AccelerometerContract.Event.OnReadIntervalEdit)
}
}
}
BleMenuItem(
shapeType = ShapeType.Middle,
title = "График измерений",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
when (origin.accelerometerState.saveHistorySettings) {
is Ble.Accelerometer.HistorySettings.Disabled ->
onEvent(AccelerometerContract.Event.OnShowRealtimeForm)
is Ble.Accelerometer.HistorySettings.Enabled ->
onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
}
}
BleMenuItem(
shapeType = ShapeType.End,
title = "Изменить пароль",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
onEvent(AccelerometerContract.Event.OnChangePassword)
}
}
}
PrimaryButton(
modifier = Modifier.shadow(
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
) {
onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
}
}
}

View File

@ -0,0 +1,25 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LoadingState(){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
ContainedLoadingIndicator()
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
package llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@ -21,8 +21,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localizedName
import llc.arma.ble.domain.model.Ble
@ -57,6 +61,7 @@ fun Write(
state.writeRequest.tx?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Мощность",
subtitle = "${it.localizedName} db",
)
@ -66,6 +71,7 @@ fun Write(
state.writeRequest.saveHistorySettings?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений",
subtitle = when(it){
Ble.Accelerometer.HistorySettings.Disabled -> "Выключено"
@ -82,6 +88,7 @@ fun Write(
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
@ -95,6 +102,7 @@ fun Write(
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек."
)
@ -112,7 +120,7 @@ fun Write(
SecondaryButton(
label = "Отменить"
) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
} else {
@ -130,7 +138,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
@ -155,7 +163,7 @@ fun Write(
SecondaryButton(
label = "Отменить"
) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
@ -197,7 +205,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}
@ -239,7 +247,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
//onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
}
}

View File

@ -0,0 +1,36 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
data object OnRefresh : Event()
}
sealed class State : ViewState {
data class Display(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}

View File

@ -0,0 +1,114 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
import javax.inject.Inject
@HiltViewModel
class AccelerometerAccelViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerAccelContract.State, AccelerometerAccelContract.Event, AccelerometerAccelContract.Effect>() {
private var measureJob: Job? = null
init {
startReadMeasure(false)
}
override fun setInitialState() = AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){
is AccelerometerAccelContract.Event.OnRefresh -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefresh
) {
startReadMeasure(true)
}
private fun startReadMeasure(
restartJob: Boolean
){
val params = AccelerometerRealtimeDestination.argsFrom(savedStateHandle)
if(restartJob || measureJob == null) {
measureJob?.cancel()
measureJob = null
measureJob = viewModelScope.launch {
setState {
AccelerometerAccelContract.State.Display(
mode = AccelViewMode.ACCELERATION,
measureHistory = emptyList()
)
}
getAccelerometerMeasureBySerialFlow(
params.bleSerial,
params.accelScale,
params.accelMode,
params.fftAxis,
params.fftMode,
params.frequency
).onEach {
it.fold(
onSuccess = {
setState {
when (this) {
is AccelerometerAccelContract.State.Display -> {
var dataList = this.measureHistory.toMutableList().apply {
add(it)
}
if(params.accelMode != AccelViewMode.ANGLE) {
dataList = dataList.takeLast(100).toMutableList()
}
AccelerometerAccelContract.State.Display(
params.accelMode,
dataList
)
}
AccelerometerAccelContract.State.Exception -> {
AccelerometerAccelContract.State.Display(
params.accelMode,
listOf(it)
)
}
}
}
},
onFailure = {
setState {
AccelerometerAccelContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}

View File

@ -0,0 +1,443 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Sort
import androidx.compose.material3.Button
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AccelerometerRealtimeDestination
import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.filter.BleFilterContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
@Serializable
data class AccelerometerRealtimeFormData(
val mode: RealtimeViewMode,
val fftViewMode: FftViewMode,
val fftAxis: FftAxis,
val fftFrequency: FftFrequency,
val scale: AccelScale
)
@Destination<RootGraph>(style = DestinationStyle.Dialog::class)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AccelerometerRealtimeForm(
navigator: DestinationsNavigator,
bleSerial: String,
) {
var mode by remember { mutableStateOf<RealtimeViewMode>(RealtimeViewMode.Accel(AccelViewMode.ACCELERATION)) }
var fftMode by remember { mutableStateOf(FftViewMode.entries.first()) }
var fftAxis by remember { mutableStateOf(FftAxis.entries.first()) }
var fftFrequency by remember { mutableStateOf(FftFrequency.entries.first()) }
var scale by remember { mutableStateOf(AccelScale.entries.first()) }
Surface(
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(20.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
mode.localized,
TextRange(mode.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Измеряемый параметр") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
RealtimeViewMode.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
mode = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
if(mode is RealtimeViewMode.Spectre) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
fftMode.localized,
TextRange(fftMode.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Режим просмотра") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
FftViewMode.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
fftMode = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
fftAxis.localized,
TextRange(fftAxis.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Ось") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
FftAxis.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
fftAxis = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
fftFrequency.localized,
TextRange(fftFrequency.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp),
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Частота дискретизации") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
FftFrequency.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
fftFrequency = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
}
Row(
verticalAlignment = Alignment.CenterVertically
) {
var expanded by remember { mutableStateOf(false) }
val sortTextFileState = TextFieldState(
scale.localized,
TextRange(scale.localized.length)
)
ExposedDropdownMenuBox(
expanded = expanded,
onExpandedChange = {
expanded = it
},
modifier = Modifier
.fillMaxWidth()
.padding(end = 8.dp)
) {
OutlinedTextField(
state = sortTextFileState,
readOnly = true,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text("Масштаб") },
trailingIcon = {
ExposedDropdownMenuDefaults.TrailingIcon(
expanded = expanded
)
},
modifier = Modifier
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
.fillMaxWidth(),
)
ExposedDropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false },
) {
AccelScale.entries.forEach { selectionOption ->
DropdownMenuItem(
onClick = {
scale = selectionOption
expanded = false
},
text = {
Text(text = selectionOption.localized)
},
contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding,
)
}
}
}
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.align(Alignment.End)
) {
OutlinedButton(
onClick = {
navigator.popBackStack()
}
) {
Text(
text = "Отмена"
)
}
Button(
onClick = {
when (mode) {
is RealtimeViewMode.Accel -> {
navigator.navigate(AccelerometerRealtimeDestination(
bleSerial = bleSerial,
accelMode = (mode as RealtimeViewMode.Accel).accelViewMode,
fftAxis = fftAxis,
fftMode = fftMode,
frequency = fftFrequency,
accelScale = scale
))
}
is RealtimeViewMode.Spectre -> {
navigator.navigate(AccelerometerSpectreDestination(
bleSerial = bleSerial,
accelMode = AccelViewMode.ACCELERATION,
fftAxis = fftAxis,
fftMode = fftMode,
frequency = fftFrequency,
accelScale = scale
))
}
}
}
) {
Text(
text = "Продолжить"
)
}
}
}
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
package llc.arma.ble.app.ui.screen.inspection.accelerometer.rt
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
@ -15,15 +15,17 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -32,7 +34,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
@ -47,81 +48,57 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.AccelViewMode.ACCELERATION
import llc.arma.ble.domain.usecase.AccelViewMode.ANGLE
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
import javax.inject.Inject
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun AccelerometerRealtime(
ble: BleInfo,
bleSerial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
onDismiss: (() -> Unit)? = null
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value
viewModel.setEvent(AccelerometerAccelContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
DisposableEffect(key1 = "ble", effect = {
onDispose {
viewModel.setEvent(AccelerometerAccelContract.Event.StopMeasure)
}
})
Column(
modifier = Modifier.fillMaxHeight(0.9f)
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = navigator::popBackStack
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
onDismiss?.let {
IconButton(onClick = it) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
},
title = {
Text(
modifier = Modifier.weight(1f),
text = accelMode.localized,
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
},
enabled = true
) {
@ -132,25 +109,24 @@ fun AccelerometerRealtime(
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
)
}
) {
Box(modifier = Modifier.padding(it)) {
when (state) {
is AccelerometerAccelContract.State.Display -> Display(state = state)
is AccelerometerAccelContract.State.Exception -> Exception()
is AccelerometerAccelContract.State.Display -> DisplayState(state = state)
is AccelerometerAccelContract.State.Exception -> ExceptionState()
}
}
}
}
@Composable
fun Display(
private fun DisplayState(
state: AccelerometerAccelContract.State.Display
) {
@ -528,7 +504,7 @@ fun Angle(
}
@Composable
private fun Exception(
private fun ExceptionState(
) {
Box(
@ -547,160 +523,3 @@ private fun Exception(
}
}
class AccelerometerAccelContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelScale: AccelScale,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency
) : Event()
}
sealed class State : ViewState {
data class Display(
val mode: AccelViewMode,
val measureHistory : List<Ble.Accelerometer.RealtimePoint>
) : State()
object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerAccelViewModel @Inject constructor(
private val getAccelerometerMeasureBySerialFlow: GetAccelerometerMeasureBySerialFlow,
) : BaseViewModel<AccelerometerAccelContract.State, AccelerometerAccelContract.Event, AccelerometerAccelContract.Effect>() {
private var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerAccelContract.State.Display(
mode = ACCELERATION,
measureHistory = emptyList()
)
override fun handleEvents(event: AccelerometerAccelContract.Event) {
when(event){
is AccelerometerAccelContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerAccelContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
AccelerometerAccelContract.State.Display(
mode = ACCELERATION,
measureHistory = emptyList()
)
}
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnStart
) {
startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, false)
}
private fun reduce(
state: AccelerometerAccelContract.State,
event: AccelerometerAccelContract.Event.OnRefreshHistory
) {
startReadMeasure(event.serial, event.accelScale, event.accelMode, event.fftAxis, event.fftMode, event.frequency, true)
}
private fun startReadMeasure(
serial: String,
accelScale: AccelScale,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
restartJob: Boolean
){
if(restartJob || measureJob == null) {
measureJob?.cancel()
measureJob = null
measureJob = viewModelScope.launch {
setState {
AccelerometerAccelContract.State.Display(
mode = ACCELERATION,
measureHistory = emptyList()
)
}
getAccelerometerMeasureBySerialFlow(serial, accelScale, accelMode, fftAxis, fftMode, frequency).onEach {
it.fold(
onSuccess = {
setState {
when (this) {
is AccelerometerAccelContract.State.Display -> {
var dataList = this.measureHistory.toMutableList().apply {
add(it)
}
if(accelMode != ANGLE) {
dataList = dataList.takeLast(100).toMutableList()
}
AccelerometerAccelContract.State.Display(accelMode, dataList)
}
AccelerometerAccelContract.State.Exception -> {
AccelerometerAccelContract.State.Display(accelMode, listOf(it))
}
}
}
},
onFailure = {
setState {
AccelerometerAccelContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}

View File

@ -0,0 +1,336 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.spectre
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
class AccelerometerEntry(
val frequency: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = AccelerometerEntry(frequency, x, y)
}
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable
fun AccelerometerSpectre(
bleSerial: String,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
accelScale: AccelScale,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<AccelerometerSpectreViewModel>()
val state = viewModel.viewState.value
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
navigator.popBackStack()
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
val title = when(state){
is AccelerometerSpectreContract.State.Display -> {
if (state.previousHistory !== null) {
"${fftMode.localized} (${state.previousHistory.size})"
}else {
fftMode.localized
}
}
AccelerometerSpectreContract.State.Exception -> fftMode.localized
}
Text(
modifier = Modifier.padding(end = 16.dp),
text = title,
style = MaterialTheme.typography.titleLarge
)
if(state is AccelerometerSpectreContract.State.Display) {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
progress = { progressAnimation },
)
}
else -> {}
}
}
}
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnRefresh)
},
enabled = when(state){
is AccelerometerSpectreContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerSpectreContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
}
) {
Box(
modifier = Modifier
.padding(it)
) {
when (state) {
is AccelerometerSpectreContract.State.Display -> DisplayState(state = state)
AccelerometerSpectreContract.State.Exception -> ExceptionState()
}
}
}
}
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun DisplayState(
state: AccelerometerSpectreContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
val data = if(state.loadingHistoryState is ProgressState.Finished){
state.loadingHistoryState.data
} else {
state.previousHistory
}
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
if(data != null){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp,
persistentMarkers = mapOf(producer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val scrollState = rememberChartScrollState()
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Chart(
marker = marker,
diffAnimationSpec = tween(0),
isZoomEnabled = true,
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize()
)
}
} else {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)
}
else -> {}
}
}
}
}
@Composable
private fun ExceptionState(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}

View File

@ -0,0 +1,32 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.spectre
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
class AccelerometerSpectreContract {
sealed class Event : ViewEvent {
data object OnRefresh : Event()
}
sealed class State : ViewState {
data class Display(
val previousHistory : List<Ble.Accelerometer.SpectrePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.SpectrePoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}

View File

@ -0,0 +1,128 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.spectre
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.AccelerometerSpectreDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.usecase.GetAccelerometerSpectreBySerial
import javax.inject.Inject
@HiltViewModel
class AccelerometerSpectreViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
) : BaseViewModel<AccelerometerSpectreContract.State, AccelerometerSpectreContract.Event, AccelerometerSpectreContract.Effect>() {
private var job: Job? = null
private var lastSerial: String? = null
init {
loadData()
}
override fun setInitialState() = AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
override fun handleEvents(event: AccelerometerSpectreContract.Event) {
when(event){
is AccelerometerSpectreContract.Event.OnRefresh -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnRefresh
) {
loadData()
}
private fun loadData(){
val params = AccelerometerSpectreDestination.argsFrom(savedStateHandle)
val state = viewState.value
viewModelScope.launch {
if(state is AccelerometerSpectreContract.State.Display) {
lastSerial = params.bleSerial
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = when (state.loadingHistoryState) {
is ProgressState.Finished -> state.loadingHistoryState.data
is ProgressState.Indeterminate -> null
is ProgressState.Progress -> null
}
)
}
} else {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
}
}
job?.cancel()
job = getAccelerometerSpectreBySerial(
serial = params.bleSerial,
accelMode = params.accelMode,
fftAxis = params.fftAxis,
fftMode = params.fftMode,
frequency = params.frequency,
accelScale = params.accelScale
).onEach {
val currentState = viewState.value
if(currentState is AccelerometerSpectreContract.State.Display) {
it.fold(
onSuccess = {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = it,
previousHistory = when (it) {
is ProgressState.Finished -> {
it.data
}
is ProgressState.Indeterminate -> currentState.previousHistory
is ProgressState.Progress -> currentState.previousHistory
}
)
}
},
onFailure = {
setState {
AccelerometerSpectreContract.State.Exception
}
}
)
}
}.launchIn(this)
}
}
}

View File

@ -1,272 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
@Composable
fun AccelEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
val accelMode = state.accelRealtimeViewMode
val fftMode = state.fftViewMode
val fftAxis = state.fftAxis
val fftFrequency = state.fftFrequency
val accelScale = state.accelScale
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Параметры",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnRealtimeViewModeEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Измеряемый параметр"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelMode.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
if(accelMode is RealtimeViewMode.Spectre){
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnFftModeEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Режим просмотра"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = fftMode.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { onEvent(AccelerometerContract.Event.OnFftAxisEdit) }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Ось"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = fftAxis.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnFftFrequencyEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Частота дистретизации"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = fftFrequency.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.ACCEL))
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Масштаб"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelScale.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Продолжить"
) {
onEvent(AccelerometerContract.Event.OnShowAccelerometerAccel)
}
}
}

View File

@ -1,56 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.FftViewMode
@Composable
fun AccelFftModeEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftViewMode.entries.forEach {
SelectorItem(
label = it.localized,
selected = it == state.fftViewMode
){
onEvent(AccelerometerContract.Event.OnFftModeChanged(it))
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
}
}

View File

@ -1,56 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.FftFrequency
@Composable
fun AccelFrequencyEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Fft frequency",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
FftFrequency.entries.forEach {
SelectorItem(
label = it.localized,
selected = it == state.fftFrequency
){
onEvent(AccelerometerContract.Event.OnFftFrequencyChanged(it))
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
}
}

View File

@ -1,102 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.AccelViewMode
sealed class RealtimeViewMode {
data class Accel(
val accelViewMode: AccelViewMode
): RealtimeViewMode()
object Spectre : RealtimeViewMode()
}
@Composable
fun AccelRealtimeViewEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelRealtimeViewMode) {
mutableStateOf(state.accelRealtimeViewMode)
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Accel view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
AccelViewMode.entries.forEach {
SelectorItem(
label = it.localized,
selected = value is RealtimeViewMode.Accel && it == (value as RealtimeViewMode.Accel).accelViewMode
){
value = RealtimeViewMode.Accel(it)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { value = RealtimeViewMode.Spectre }
.padding(4.dp)
) {
RadioButton(
selected = value is RealtimeViewMode.Spectre,
onClick = {
value = RealtimeViewMode.Spectre
}
)
Text(text = RealtimeViewMode.Spectre.localized)
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnRealtimeViewModeChanged(value))
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
}
}

View File

@ -1,81 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
@Composable
fun AccelScaleEdit(
next: AccelerometerContract.Event.Next,
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
val fftMode = when(next){
AccelerometerContract.Event.Next.ACCEL ->
state.accelScale
AccelerometerContract.Event.Next.HISTORY -> {
val history = state.accelerometer.accelerometerState.saveHistory
if (history is Ble.Accelerometer.HistorySettings.Enabled)
history.scale
else {
state.accelScale
}
}
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Accel scale",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
AccelScale.entries.forEach {
SelectorItem(
label = it.localized,
selected = it == fftMode
){
when(next){
AccelerometerContract.Event.Next.ACCEL ->
onEvent(AccelerometerContract.Event.OnAccelScaleChanged(it))
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryScaleChanged(it))
}
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
when(next){
AccelerometerContract.Event.Next.ACCEL ->
onEvent(AccelerometerContract.Event.OnAccelEdit)
AccelerometerContract.Event.Next.HISTORY ->
onEvent(AccelerometerContract.Event.OnHistoryEdit)
}
}
}
}

View File

@ -1,76 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
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.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.AccelViewMode
@Composable
fun AccelViewEdit(
next: AccelerometerContract.Event.Next,
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelViewMode) {
mutableStateOf(state.accelViewMode)
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Accel view mode",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
AccelViewMode.entries.forEach {
SelectorItem(
label = it.localized,
selected = it == value
){
value = it
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
when(next){
AccelerometerContract.Event.Next.ACCEL -> {
onEvent(AccelerometerContract.Event.OnAccelViewModelChanged(value))
onEvent(AccelerometerContract.Event.OnAccelEdit)
}
AccelerometerContract.Event.Next.HISTORY -> {
onEvent(AccelerometerContract.Event.OnHistoryViewModeChanged(value))
onEvent(AccelerometerContract.Event.OnHistoryEdit)
}
}
}
}
}

View File

@ -1,540 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerSpectreBySerial
import javax.inject.Inject
class AccelerometerEntry(
val frequency: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = AccelerometerEntry(frequency, x, y)
}
@Composable
fun AccelerometerSpectre(
ble: BleInfo,
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
accelScale: AccelScale,
onDismiss: (() -> Unit)? = null
) {
val viewModel = hiltViewModel<AccelerometerSpectreViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial, accelMode, fftAxis, fftMode, frequency) {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
}
DisposableEffect(key1 = "ble", effect = {
onDispose {
viewModel.setEvent(AccelerometerSpectreContract.Event.StopMeasure)
}
})
Column(
modifier = Modifier.fillMaxHeight(0.9f)
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
onDismiss?.let {
IconButton(onClick = it) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
val title = when(state){
is AccelerometerSpectreContract.State.Display -> {
if (state.previousHistory !== null) {
"${fftMode.localized} (${state.previousHistory.size})"
}else {
fftMode.localized
}
}
AccelerometerSpectreContract.State.Exception -> fftMode.localized
}
Row(
modifier = Modifier.weight(1f),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = title,
style = MaterialTheme.typography.titleLarge
)
if(state is AccelerometerSpectreContract.State.Display) {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
modifier = Modifier.size(16.dp),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
progress = { progressAnimation },
)
}
else -> {}
}
}
}
IconButton(
onClick = {
viewModel.setEvent(AccelerometerSpectreContract.Event.OnStart(ble.serial, accelMode, fftAxis, fftMode, frequency, accelScale))
},
enabled = when(state){
is AccelerometerSpectreContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerSpectreContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Box(modifier = Modifier) {
when (state) {
is AccelerometerSpectreContract.State.Display -> Display(state = state)
AccelerometerSpectreContract.State.Exception -> Exception()
}
}
}
}
val axisValueFormatter = AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? AccelerometerEntry)
?.frequency?.let { String.format("%.1f", (it.toFloat() / 256f)) }
.orEmpty()
}
@Composable
fun Display(
state: AccelerometerSpectreContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
val data = if(state.loadingHistoryState is ProgressState.Finished){
state.loadingHistoryState.data
} else {
state.previousHistory
}
val producer = remember {
ChartEntryModelProducer(listOf<FloatEntry>())
}
if(data != null){
if(data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
LaunchedEffect(data){
producer.setEntries(
data.mapIndexed { index, measurePoint ->
AccelerometerEntry(measurePoint.frequency, index.toFloat(), measurePoint.value)
}
)
}
val lineChart = columnChart(
spacing = 1.5.dp,
persistentMarkers = mapOf(producer.getModel().maxX to MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)),
)
val scrollState = rememberChartScrollState()
val marker = MarkerComponent(
label = textComponent(),
indicator = null,
guideline = axisGuidelineComponent()
)
Chart(
marker = marker,
diffAnimationSpec = tween(0),
isZoomEnabled = true,
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize()
)
}
} else {
when (state.loadingHistoryState) {
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
)
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)
}
else -> {}
}
}
}
}
@Composable
private fun Exception(
) {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class AccelerometerSpectreContract {
sealed class Event : ViewEvent {
object StopMeasure : Event()
data class OnStart(
val serial: String,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
) : Event()
data class OnRefreshHistory(
val serial: String,
val accelMode: AccelViewMode,
val fftAxis: FftAxis,
val fftMode: FftViewMode,
val frequency: FftFrequency,
val accelScale: AccelScale
) : Event()
}
sealed class State : ViewState {
data class Display(
val previousHistory : List<Ble.Accelerometer.SpectrePoint>?,
val loadingHistoryState : ProgressState<List<Ble.Accelerometer.SpectrePoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class AccelerometerSpectreViewModel @Inject constructor(
private val getAccelerometerSpectreBySerial: GetAccelerometerSpectreBySerial
) : BaseViewModel<AccelerometerSpectreContract.State, AccelerometerSpectreContract.Event, AccelerometerSpectreContract.Effect>() {
private var job: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
override fun handleEvents(event: AccelerometerSpectreContract.Event) {
when(event){
is AccelerometerSpectreContract.Event.OnStart -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is AccelerometerSpectreContract.Event.StopMeasure -> reduce(viewState.value, event)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnStart
) {
viewModelScope.launch {
if(state is AccelerometerSpectreContract.State.Display) {
lastSerial = event.serial
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = when(state.loadingHistoryState){
is ProgressState.Finished -> state.loadingHistoryState.data
is ProgressState.Indeterminate -> null
is ProgressState.Progress -> null
}
)
}
} else {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = ProgressState.Indeterminate,
previousHistory = null
)
}
}
job?.cancel()
job = getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
val currentState = viewState.value
if(currentState is AccelerometerSpectreContract.State.Display) {
it.fold(
onSuccess = {
setState {
AccelerometerSpectreContract.State.Display(
loadingHistoryState = it,
previousHistory = when (it) {
is ProgressState.Finished -> {
it.data
}
is ProgressState.Indeterminate -> currentState.previousHistory
is ProgressState.Progress -> currentState.previousHistory
}
)
}
},
onFailure = {
setState {
AccelerometerSpectreContract.State.Exception
}
}
)
}
}.launchIn(this)
}
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.OnRefreshHistory
) {
/*viewModelScope.launch {
setState {
AccelerometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getAccelerometerSpectreBySerial(
serial = event.serial,
accelMode = event.accelMode,
fftAxis = event.fftAxis,
fftMode = event.fftMode,
frequency = event.frequency,
accelScale = event.accelScale
).onEach {
it.fold(
onSuccess = {
setState {
AccelerometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
AccelerometerHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}*/
}
private fun reduce(
state: AccelerometerSpectreContract.State,
event: AccelerometerSpectreContract.Event.StopMeasure
) {
job?.cancel()
}
}

View File

@ -1,218 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
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.draw.shadow
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.data.repository.BleRepositoryImpl
import llc.arma.ble.domain.model.Ble
@Composable
fun DisplayState(
onEvent: (AccelerometerContract.Event) -> Unit,
origin: Ble.Accelerometer,
ble: BleView.Accelerometer
) {
val scrollState = rememberScrollState()
Column {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
)
}
Column(
modifier = Modifier,
content = {
BleMenuItem(
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
){
onEvent(AccelerometerContract.Event.OnPowerEdit)
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
val history = ble.accelerometerState.saveHistory
if(history is Ble.Accelerometer.HistorySettings.Enabled){
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "View mode ${history.mode.localized}"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "Scale: ${history.scale.localized}"
)
}
}
Switch(
checked = ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled,
onCheckedChange = {
onEvent(AccelerometerContract.Event.OnSaveHistoryChanged(it))
}
)
}
}
if(ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
val hours =
ble.accelerometerState.historyInterval / millisInHour
val minutes =
(ble.accelerometerState.historyInterval - (hours * millisInHour)) / millisInMinute
val seconds =
(ble.accelerometerState.historyInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек.",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(AccelerometerContract.Event.OnSaveIntervalEdit)
}
}
if(ble.state.version > BleRepositoryImpl.Version.fromString("0.0.0-0")) {
if (ble.accelerometerState.saveHistory is Ble.Accelerometer.HistorySettings.Enabled) {
val hours =
ble.accelerometerState.readInterval / millisInHour
val minutes =
(ble.accelerometerState.readInterval - (hours * millisInHour)) / millisInMinute
val seconds =
(ble.accelerometerState.readInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек.",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(AccelerometerContract.Event.OnReadIntervalEdit)
}
}
}
BleMenuItem(
title = "График измерений",
icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
) {
when(origin.accelerometerState.saveHistorySettings){
is Ble.Accelerometer.HistorySettings.Disabled ->
onEvent(AccelerometerContract.Event.OnAccelEdit)
is Ble.Accelerometer.HistorySettings.Enabled ->
onEvent(AccelerometerContract.Event.OnShowAccelerometerHistory)
}
}
BleMenuItem(
title = "Изменить пароль",
icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
) {
onEvent(AccelerometerContract.Event.OnChangePassword)
}
}
)
}
PrimaryButton(
modifier = Modifier.shadow(
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
) {
onEvent(AccelerometerContract.Event.OnShowWriteBlePreview)
}
}
}

View File

@ -1,152 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble
@Composable
fun HistoryEdit(
state: AccelerometerContract.State.Display,
onEvent: (AccelerometerContract.Event) -> Unit,
){
val history = state.accelerometer.accelerometerState.saveHistory
val detailed = if (history is Ble.Accelerometer.HistorySettings.Enabled) history.detailed else false
val accelMode = if (history is Ble.Accelerometer.HistorySettings.Enabled) history.mode else state.accelViewMode
val accelScale = if (history is Ble.Accelerometer.HistorySettings.Enabled) history.scale else state.accelScale
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "История измерений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Column(
modifier = Modifier
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(
AccelerometerContract.Event.OnAccelViewModeEdit(
next = AccelerometerContract.Event.Next.HISTORY
)
)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel view mode"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelMode.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(AccelerometerContract.Event.OnAccelScaleEdit(next = AccelerometerContract.Event.Next.HISTORY))
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Accel scale"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = accelScale.localized
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Ок"
) {
onEvent(AccelerometerContract.Event.OnHideHistoryEdit)
}
}
}

View File

@ -1,242 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
@Composable
fun IntervalEdit(
state: BleView.Accelerometer,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelerometerState.historyInterval) {
mutableIntStateOf((state.accelerometerState.historyInterval).toInt())
}
val maxInterval = 10 * 24 * 60 * 60 * 1000
val minInterval = 1_000
if(value > maxInterval){
value = maxInterval
}
if(value < minInterval){
value = minInterval
}
val maxSeconds = maxInterval / millisInSecond
val maxMinutes = maxInterval / millisInMinute
val maxHours = maxInterval / millisInHour
val maxDays = maxInterval / millisInDay
val dayValue = value / millisInDay
val hourValue = (value - (dayValue * millisInDay)) / millisInHour
val minutesValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour)) / millisInMinute
val secondsValue = (value - (dayValue * millisInDay) - (hourValue * millisInHour) - (minutesValue * millisInMinute)) / millisInSecond
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Интервал измерений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
NumberPicker(
range = -1..maxDays,
value = dayValue,
onValueChanged = {
value = (it * millisInDay) + (hourValue * millisInHour) + (minutesValue * millisInMinute) + (secondsValue * millisInSecond)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Д.")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxHours,
value = hourValue,
onValueChanged = {
value = (it * millisInHour) + (dayValue * millisInDay) + (minutesValue * millisInMinute) + (secondsValue * millisInSecond)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Ч.")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxMinutes,
value = minutesValue,
onValueChanged = {
value = (secondsValue * millisInSecond) + (it * millisInMinute) + (dayValue * millisInDay) + (hourValue * millisInHour)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "М.")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxSeconds,
value = secondsValue,
onValueChanged = {
value = (it * millisInSecond) + (minutesValue * millisInMinute) + (dayValue * millisInDay) + (hourValue * millisInHour)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "С.")
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Применить"
) {
onEvent(
AccelerometerContract.Event.OnSaveIntervalChanged(
value.toLong()
)
)
}
}
}
const val millisInSecond = 1000
const val millisInMinute = millisInSecond * 60
const val millisInHour = millisInMinute * 60
const val millisInDay = millisInHour * 24
@Composable
fun NumberPicker(
modifier: Modifier = Modifier,
range: IntRange,
value: Int,
onValueChanged: (Int) -> Unit
) {
LaunchedEffect(range){
if(value > range.last){
onValueChanged(range.last)
}
if(value < range.first){
onValueChanged(range.first)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
){
FilledIconButton(
onClick = {
if(value < range.last) onValueChanged(value + 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowUp,
contentDescription = null
)
}
Spacer(modifier = Modifier.height(36.dp))
AnimatedContent(
targetState = value,
transitionSpec = {
if (targetState > initialState) {
(slideInVertically { height -> height } + fadeIn()).togetherWith(
slideOutVertically { height -> -height } + fadeOut())
} else {
(slideInVertically { height -> -height } + fadeIn()).togetherWith(
slideOutVertically { height -> height } + fadeOut())
}.using(
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(
style = MaterialTheme.typography.displaySmall,
text = "$targetCount"
)
}
Spacer(modifier = Modifier.height(36.dp))
FilledIconButton(
onClick = {
if(value > range.first) onValueChanged(value - 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}

View File

@ -1,19 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun LoadingState(){
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}

View File

@ -1,65 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.accelerometer.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.host.view.DurationPicker
@Composable
fun ReadIntervalEdit(
state: BleView.Accelerometer,
onEvent: (AccelerometerContract.Event) -> Unit,
){
var value by remember(state.accelerometerState.readInterval) {
mutableIntStateOf((state.accelerometerState.readInterval).toInt())
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Интервал чтения",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
DurationPicker(
modifier = Modifier.align(Alignment.CenterHorizontally),
value = value
) {
value = it
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Применить"
) {
onEvent(
AccelerometerContract.Event.OnReadIntervalChanged(
value.toLong()
)
)
}
}
}

View File

@ -4,19 +4,22 @@ import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract.Effect.Navigation
import llc.arma.ble.domain.model.Ble
class BeaconContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
object OnWriteBle : Event()
object OnHideWriteBlePreview : Event()
object OnShowWriteBlePreview : Event()
object OnPowerEdit : Event()
data object OnPowerEdit : Event()
data class OnBleChanged(
val ble: Ble.Beacon
@ -26,7 +29,7 @@ class BeaconContract {
val tx: BleView.BleState.TX
) : Event()
data class OnTxChanged(val tx: Int) : Event()
data class OnTxChanged(val tx: BleView.BleState.TX) : Event()
object OnNavigateUpClicked : Event()
@ -36,7 +39,7 @@ class BeaconContract {
sealed class State : ViewState {
object Loading : State()
data object Loading : State()
data class Display(
val origin: Ble.Beacon,
@ -54,9 +57,9 @@ class BeaconContract {
val writeRequest: Ble.Beacon.WriteRequest
) : WriteState()
object Success : WriteState()
data object Success : WriteState()
object Failure : WriteState()
data object Failure : WriteState()
}
@ -66,19 +69,21 @@ class BeaconContract {
sealed class Effect : ViewSideEffect {
object ShowPowerPicker : Effect()
data object HideWriteBlePreview : Effect()
object HidePowerPicker : Effect()
object HideWriteBlePreview : Effect()
object ShowWriteBlePreview : Effect()
data object ShowWriteBlePreview : Effect()
sealed class Navigation : Effect() {
object NavigateToChangePassword : Navigation()
data object Up : Navigation()
object NavigateUp : Navigation()
data class PasswordForm(
val bleSerial: String
) : Navigation()
data class TxSelector(
val tx: BleView.BleState.TX?
) : Navigation()
}

View File

@ -3,7 +3,18 @@ package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -13,24 +24,35 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.TxLevelSelector
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.beacon.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.beacon.view.Write
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE, POWER_EDIT
WRITE
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BeaconScreen(
ble: Ble.Beacon,
onNavigationEvent: (BeaconContract.Effect.Navigation) -> Unit
bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BeaconViewModel>()
@ -42,10 +64,14 @@ fun BeaconScreen(
val bottomDialog = rememberBottomDialogState()
LaunchedEffect("effect"){
txSelectResult.onResult {
viewModel.setEvent(BeaconContract.Event.OnTxChanged(it))
}
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
is BeaconContract.Effect.Navigation -> onNavigationEvent(it)
BeaconContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
@ -54,22 +80,19 @@ fun BeaconScreen(
delay(100)
sheetPage = SheetPage.WRITE
}
BeaconContract.Effect.HidePowerPicker -> launch {
sheetPage = null
}
BeaconContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER_EDIT
}
is BeaconContract.Effect.Navigation.PasswordForm ->
navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
is BeaconContract.Effect.Navigation.TxSelector ->
navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
BeaconContract.Effect.Navigation.Up ->
navigator.popBackStack()
}
}.launchIn(this)
}
LaunchedEffect(ble){
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
}
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
@ -88,26 +111,37 @@ fun BeaconScreen(
}
}
SheetPage.POWER_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is BeaconContract.State.Display) {
TxLevelSelector(
tx = currentState.beacon.state.tx
) {
viewModel.setEvent(BeaconContract.Event.OnPowerChanged(it))
}
}
}
else -> {
bottomDialog.hide()
}
}
}
Column {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(BeaconContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(text = BleInfo.Type.BEACON.localized)
}
)
}
) {
Box(
modifier = Modifier.padding(it)
) {
when(state){
is BeaconContract.State.Display -> DisplayState(
@ -122,14 +156,22 @@ fun BeaconScreen(
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(){
Box(modifier = Modifier.fillMaxSize()){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
ContainedLoadingIndicator()
}

View File

@ -1,23 +1,74 @@
package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.AccelerometerContract
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.RealtimeViewMode
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode
import llc.arma.ble.domain.usecase.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class BeaconViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() {
init {
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { it },
onFailure = { null }
)
if(ble != null && ble is Ble.Beacon){
setState {
when(this){
is BeaconContract.State.Display -> {
copy(
origin = Ble.Beacon(
info = ble.info,
state = origin.state
)
)
}
BeaconContract.State.Loading -> {
BeaconContract.State.Display(
origin = ble,
beacon = bleMapper.map(ble) as BleView.Beacon,
writeState = null
)
}
}
}
}
}
}
override fun setInitialState() = BeaconContract.State.Loading
override fun handleEvents(event: BeaconContract.Event) {
@ -31,6 +82,7 @@ class BeaconViewModel @Inject constructor(
is BeaconContract.Event.OnWriteBle -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is BeaconContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
@ -45,8 +97,15 @@ class BeaconViewModel @Inject constructor(
}
}
private fun reduce(
state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUp
) {
setEffect {
BeaconContract.Effect.HidePowerPicker
BeaconContract.Effect.Navigation.Up
}
}
@ -56,7 +115,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnPowerEdit
) {
setEffect { BeaconContract.Effect.ShowPowerPicker }
if(state is BeaconContract.State.Display) {
setEffect { BeaconContract.Effect.Navigation.TxSelector(state.beacon.state.tx) }
}
}
@ -64,7 +129,7 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUpClicked
) {
setEffect { BeaconContract.Effect.Navigation.NavigateUp }
setEffect { BeaconContract.Effect.Navigation.Up }
}
private fun reduce(
@ -107,9 +172,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State,
event: BeaconContract.Event.OnChangePassword
) {
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
setEffect {
BeaconContract.Effect.Navigation.NavigateToChangePassword
BeaconContract.Effect.Navigation.PasswordForm(params.bleSerial)
}
}
private fun reduce(

View File

@ -1,8 +1,9 @@
package llc.arma.ble.app.ui.screen.inspection.beacon.view
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -10,17 +11,20 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.domain.model.Ble
@Composable
@ -30,66 +34,65 @@ fun DisplayState(
ble: BleView.Beacon
) {
val scrollState = rememberScrollState()
Column {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f)
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
)
}
Column(
modifier = Modifier,
content = {
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
onEvent(BeaconContract.Event.OnPowerEdit)
}
BleMenuItem(
shapeType = ShapeType.End,
title = "Изменить пароль",
icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
onEvent(BeaconContract.Event.OnChangePassword)
}
}
Button (
onClick = {
onEvent(BeaconContract.Event.OnShowWriteBlePreview)
},
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Сохранить"
)
}
PrimaryButton(
modifier = Modifier.shadow(
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
) {
onEvent(BeaconContract.Event.OnShowWriteBlePreview)
}
}
}

View File

@ -0,0 +1,48 @@
package llc.arma.ble.app.ui.screen.inspection.gate.history
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleName
class GateHistoryContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data object StopMeasure : Event()
data object OnRefreshHistory : Event()
data object OnExportHistory : Event()
}
sealed class State : ViewState {
data class Loading(
val progress: Float?
) : State()
data class Display(
val bleNames: List<BleName>,
val loadingHistoryState: List<Ble.Gate.HistoryPoint>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,522 @@
package llc.arma.ble.app.ui.screen.inspection.gate.history
import android.content.res.Configuration
import androidx.compose.animation.core.tween
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowColumn
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.patrykandpatrick.vico.compose.axis.axisLineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.compose.component.shapeComponent
import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.component.shape.LineComponent
import com.patrykandpatrick.vico.core.component.shape.Shapes.pillShape
import com.patrykandpatrick.vico.core.dimensions.MutableDimensions
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class HostEntry(
val localDate: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = HostEntry(localDate, x, y)
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GateHistoryScreen(
bleSerial: String,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<GateHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
GateHistoryContract.Effect.Navigation.Up ->
navigator.popBackStack()
}
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(GateHistoryContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
val title = when(state){
is GateHistoryContract.State.Exception,
is GateHistoryContract.State.Loading -> "Таблица"
is GateHistoryContract.State.Display -> "Таблица (${state.loadingHistoryState.size})"
}
Text(
text = title,
)
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(GateHistoryContract.Event.OnExportHistory)
},
enabled = when(state){
is GateHistoryContract.State.Display,
GateHistoryContract.State.Exception -> true
is GateHistoryContract.State.Loading -> false
}
) {
Icon(
imageVector = Icons.Rounded.Save,
contentDescription = null
)
}
IconButton(
onClick = {
viewModel.setEvent(GateHistoryContract.Event.OnRefreshHistory)
},
enabled = when(state){
is GateHistoryContract.State.Display,
GateHistoryContract.State.Exception -> true
is GateHistoryContract.State.Loading -> false
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
}
) {
Box(modifier = Modifier.padding(it)) {
when (state) {
is GateHistoryContract.State.Display -> DisplayState(state = state)
is GateHistoryContract.State.Exception -> ErrorState()
is GateHistoryContract.State.Loading -> LoadingState(state = state)
}
}
}
}
val dayFormatter = SimpleDateFormat("dd", Locale.getDefault())
val dateFormatter = SimpleDateFormat("dd.MM", Locale.getDefault())
val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
val colorsStack = listOf(
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
)
val startAxisValueFormatter =
AxisValueFormatter<AxisPosition.Vertical.Start> { value, chartValues ->
" "
}
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
val first = (chartValues.chartEntryModel.entries.firstOrNull()?.firstOrNull() as? HostEntry)
val last = (chartValues.chartEntryModel.entries.firstOrNull()?.lastOrNull() as? HostEntry)
val previous = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt() - 1) as? HostEntry)
val current = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt()) as? HostEntry)
if(current != null) {
if (first == current || last == current) {
dateFormatter.format(Date(current.localDate))
} else {
if(previous != null && dayFormatter.format(previous.localDate) != dayFormatter.format(current.localDate)){
dateFormatter.format(Date(current.localDate))
}else{
timeFormatter.format(Date(current.localDate))
}
}.orEmpty()
} else {
" "
}
}
@Composable
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
private fun LoadingState(
state: GateHistoryContract.State.Loading
){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
if(state.progress == null){
ContainedLoadingIndicator()
}else{
ContainedLoadingIndicator(
progress = { state.progress }
)
}
}
}
@Composable
private fun DisplayState(
state: GateHistoryContract.State.Display
) {
if (state.loadingHistoryState.isEmpty()) {
EmptyState()
} else {
DataState(state)
}
}
@Composable
private fun EmptyState(){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
}
}
@Composable
private fun DataState(
state: GateHistoryContract.State.Display
){
val configuration = LocalConfiguration.current
val allSerials =
remember(state) { state.loadingHistoryState.flatMap { it.value }.distinct() }
val colors = remember(allSerials) {
allSerials.mapIndexed { index, s ->
Pair(s, colorsStack[index])
}.toMap()
}
var selectedSerials by rememberSaveable {
mutableStateOf(allSerials.take(1))
}
val serials =
remember(selectedSerials) { allSerials.filter { selectedSerials.contains(it) } }
val entries = remember(serials, state) {
serials.map { serial ->
ChartEntryModelProducer(
state.loadingHistoryState.mapIndexed { index, historyPoint ->
if (historyPoint.value.contains(serial)) {
HostEntry(historyPoint.date, index.toFloat(), 1f)
} else {
HostEntry(historyPoint.date, index.toFloat(), 0.0001f)
}
}
)
}
}
val producer = remember(entries) { ComposedChartEntryModelProducer(entries) }
val chart = columnChart(
persistentMarkers = state.loadingHistoryState.mapIndexedNotNull { index, historyPoint ->
if (historyPoint.hit) {
Pair(
index.toFloat(),
MarkerComponent(
label = textComponent(
textSize = 0.sp,
padding = MutableDimensions(10f, 10f, 10f, 10f),
margins = MutableDimensions(10f, 10f, 10f, 10f),
color = Color.Red,
background = shapeComponent(
color = Color.Red,
shape = pillShape
)
),
indicator = null,
guideline = axisLineComponent(
thickness = 0.dp
)
)
)
} else {
null
}
}.toMap(),
innerSpacing = 2.dp,
columns = serials.map {
LineComponent(
color = colors[it]!!.toArgb(),
thicknessDp = 7f,
shape = pillShape
)
},
spacing = 8.dp,
)
@Composable
fun LegendItem(s: String) {
FilterChip(
selected = selectedSerials.contains(s),
onClick = {
selectedSerials = if (selectedSerials.contains(s)) {
selectedSerials.toMutableList().apply {
remove(s)
}
} else {
selectedSerials.toMutableList().apply {
add(s)
}
}
},
leadingIcon = {
Surface(
shape = CircleShape,
color = colors[s]!!,
modifier = Modifier.size(28.dp)
) {}
},
label = {
Column {
Text(text = state.bleNames.firstOrNull { it.serial == s }?.name ?: s)
Text(
style = MaterialTheme.typography.bodySmall.copy(
color = LocalTextStyle.current.color.copy(
alpha = ContentAlpha.medium
)
),
text = s
)
}
}
)
}
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Chart(
chart = chart,
chartModelProducer = producer,
bottomAxis = bottomAxis(
labelRotationDegrees = -90f,
valueFormatter = axisValueFormatter,
tickLength = 0.dp,
),
startAxis = startAxis(
valueFormatter = startAxisValueFormatter
),
modifier = Modifier
.fillMaxHeight()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
VerticalDivider()
FlowRow(
maxItemsInEachRow = 1,
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.verticalScroll(rememberScrollState())
) {
allSerials.mapIndexed { _, s ->
LegendItem(s = s)
}
}
}
}
else -> {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
FlowColumn(
maxItemsInEachColumn = 2,
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
allSerials.mapIndexed { _, s ->
LegendItem(s = s)
}
}
HorizontalDivider()
Chart(
chart = chart,
chartModelProducer = producer,
bottomAxis = bottomAxis(
labelRotationDegrees = -90f,
valueFormatter = axisValueFormatter,
tickLength = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
@Composable
private fun ErrorState() {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}

View File

@ -0,0 +1,152 @@
package llc.arma.ble.app.ui.screen.inspection.gate.history
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.ExportToXlsx
import llc.arma.ble.domain.usecase.GetBleNamesFlow
import llc.arma.ble.domain.usecase.GetHostHistoryBySerial
import javax.inject.Inject
@HiltViewModel
class GateHistoryViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getHostHistoryBySerial: GetHostHistoryBySerial,
private val getBleNamesFlow: GetBleNamesFlow,
private val exportToXlsx: ExportToXlsx
) : BaseViewModel<GateHistoryContract.State, GateHistoryContract.Event, GateHistoryContract.Effect>() {
private var measureJob: Job? = null
init {
loadData()
}
override fun setInitialState() = GateHistoryContract.State.Loading(null)
override fun handleEvents(event: GateHistoryContract.Event) {
when(event){
is GateHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is GateHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
is GateHistoryContract.Event.OnExportHistory -> reduce(viewState.value, event)
is GateHistoryContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: GateHistoryContract.State,
event: GateHistoryContract.Event.OnNavigateUp
) {
setEffect {
GateHistoryContract.Effect.Navigation.Up
}
}
private fun reduce(
state: GateHistoryContract.State,
event: GateHistoryContract.Event.OnExportHistory
) {
val params = GateHistoryScreenDestination.argsFrom(savedStateHandle)
if(state is GateHistoryContract.State.Display) {
exportToXlsx.invoke(params.bleSerial, state.loadingHistoryState)
}
}
private fun reduce(
state: GateHistoryContract.State,
event: GateHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
GateHistoryContract.State.Exception
}
}
private fun reduce(
state: GateHistoryContract.State,
event: GateHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch { loadData() }
}
private fun loadData(){
val state = viewState.value
val params = GateHistoryScreenDestination.argsFrom(savedStateHandle)
viewModelScope.launch {
setState { GateHistoryContract.State.Loading(null) }
measureJob?.cancel()
measureJob = null
val names = getBleNamesFlow().first()
measureJob = getHostHistoryBySerial(params.bleSerial).onEach {
it.fold(
onSuccess = {
when (it) {
is ProgressState.Finished<List<Ble.Gate.HistoryPoint>> -> {
setState {
GateHistoryContract.State.Display(
names,
it.data
)
}
}
is ProgressState.Indeterminate -> {
setState {
GateHistoryContract.State.Loading(null)
}
}
is ProgressState.Progress -> {
setState {
GateHistoryContract.State.Loading(it.value)
}
}
}
},
onFailure = {
setState {
GateHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.host
package llc.arma.ble.app.ui.screen.inspection.gate.main
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
@ -7,7 +7,7 @@ import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
class HostContract {
class GateContract {
sealed class Event : ViewEvent {
@ -17,19 +17,13 @@ class HostContract {
data object OnShowWriteBlePreview : Event()
data object OnPowerEdit : Event()
data class OnBleChanged(
val ble: Ble.Host
) : Event()
data object OnTxSelect : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
data class OnTxChanged(val tx: Int) : Event()
data object OnShowIntervalEdit : Event()
data object OnHistoryIntervalSelect : Event()
data class OnSaveIntervalChanged(
val interval: Long
@ -41,7 +35,7 @@ class HostContract {
val interval: Long
) : Event()
data object OnNavigateUpClicked : Event()
data object OnNavigateUp : Event()
data object OnChangePassword : Event()
@ -53,22 +47,24 @@ class HostContract {
sealed class State : ViewState {
data object Loading : State()
data class Loading(
val attempt: Int?
) : State()
data class Display(
val origin: Ble.Host,
val host: BleView.Host,
val origin: Ble.Gate,
val gate: BleView.Gate,
val writeState: WriteState?
) : State() {
sealed class WriteState {
data class DisplayPreview(
val writeRequest: Ble.Host.WriteRequest
val writeRequest: Ble.Gate.WriteRequest
) : WriteState()
data class Writing(
val writeRequest: Ble.Host.WriteRequest
val writeRequest: Ble.Gate.WriteRequest
) : WriteState()
data object Success : WriteState()
@ -83,36 +79,36 @@ class HostContract {
sealed class Effect : ViewSideEffect {
data object ShowPowerPicker : Effect()
data object HidePowerPicker : Effect()
data object HideWriteBlePreview : Effect()
data object ShowWriteBlePreview : Effect()
data object HideIntervalPicker : Effect()
data object ShowIntervalPicker : Effect()
data object HideReadIntervalPicker : Effect()
data object ShowReadIntervalPicker : Effect()
sealed class Navigation : Effect() {
data object NavigateToChangePassword : Navigation()
data class ChangePassword(
val serial: String,
) : Navigation()
data object NavigateUp : Navigation()
data object Up : Navigation()
data class NavigateToHostHistory(
data class GateHistory(
val ble: BleInfo,
) : Navigation()
data class NavigateToBleTable(
data class BleTable(
val serial: String,
) : Navigation()
data class TxSelector(
val tx: BleView.BleState.TX?
) : Navigation()
data class ReadIntervalSelector(
val interval: Int
) : Navigation()
data class HistoryIntervalSelector(
val interval: Int
) : Navigation()
}
}

View File

@ -0,0 +1,255 @@
package llc.arma.ble.app.ui.screen.inspection.gate.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import com.ramcosta.composedestinations.generated.destinations.GateHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.gate.main.view.Write
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleInfo
enum class SheetPage {
WRITE
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GateScreen(
bleSerial: String,
readDurationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<GateViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult {
viewModel.setEvent(GateContract.Event.OnPowerChanged(it))
}
readDurationSelectResult.onResult {
if(it.qualifier == "ReadIntervalSelector"){
viewModel.setEvent(GateContract.Event.OnSaveReadIntervalChanged(it.duration.toLong()))
}
if(it.qualifier == "HistoryIntervalSelector"){
viewModel.setEvent(GateContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
}
}
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
GateContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is GateContract.Effect.Navigation.BleTable ->
navigator.navigate(GateBleTableScreenDestination(it.serial))
is GateContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.serial))
is GateContract.Effect.Navigation.GateHistory ->
navigator.navigate(GateHistoryScreenDestination(it.ble.serial))
is GateContract.Effect.Navigation.ReadIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "ReadIntervalSelector",
duration = it.interval,
maximum = 10 * 24 * 60 * 60 * 1000
))
is GateContract.Effect.Navigation.TxSelector ->
navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
GateContract.Effect.Navigation.Up ->
navigator.navigateUp()
is GateContract.Effect.Navigation.HistoryIntervalSelector ->
navigator.navigate(DurationSelectorScreenDestination(
qualifier = "HistoryIntervalSelector",
duration = it.interval,
maximum = 10 * 24 * 60 * 60 * 1000
))
}
}.launchIn(this)
}
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is GateContract.State.Display && currentState.writeState != null) {
Write(
state = currentState.writeState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
else -> {
bottomDialog.hide()
}
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(GateContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(
text = BleInfo.Type.HOST.localized
)
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when (state) {
is GateContract.State.Display -> DisplayState(viewModel, state)
is GateContract.State.Loading -> LoadingState(viewModel, state)
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun LoadingState(
viewModel: GateViewModel,
state: GateContract.State.Loading
){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.widthIn(max = 230.dp)
) {
ContainedLoadingIndicator()
state.attempt?.let {
Spacer(Modifier.height(16.dp))
Text(
text = "Повторная попытка ${it}"
)
Text(
text = "Во время загрузки произошла ошибка",
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodySmall
)
Spacer(Modifier.height(8.dp))
TextButton(
onClick = {
viewModel.setEvent(GateContract.Event.OnNavigateUp)
}
) {
Text(
text = "Отмена"
)
}
}
}
}
}

View File

@ -0,0 +1,349 @@
package llc.arma.ble.app.ui.screen.inspection.gate.main
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.ramcosta.composedestinations.generated.destinations.GateScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class GateViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<GateContract.State, GateContract.Event, GateContract.Effect>() {
init {
val params = GateScreenDestination.argsFrom(savedStateHandle)
viewModelScope.launch {
var attempt = 0
var ble: Ble? = null
while (ble == null) {
ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { return@fold it },
onFailure = { return@fold null }
)
if(ble == null) {
setState {
attempt++
GateContract.State.Loading(attempt)
}
}
}
if (ble is Ble.Gate) {
setState {
when (this) {
is GateContract.State.Display -> {
copy(
origin = Ble.Gate(
info = ble.info,
state = origin.state,
gateState = origin.gateState
)
)
}
is GateContract.State.Loading -> {
GateContract.State.Display(
origin = ble,
gate = bleMapper.map(ble) as BleView.Gate,
writeState = null
)
}
}
}
}
}
}
override fun setInitialState() = GateContract.State.Loading(null)
override fun handleEvents(event: GateContract.Event) {
when(event){
is GateContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is GateContract.Event.OnChangePassword -> reduce(viewState.value, event)
is GateContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is GateContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is GateContract.Event.OnWriteBle -> reduce(viewState.value, event)
is GateContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is GateContract.Event.OnTxSelect -> reduce(viewState.value, event)
is GateContract.Event.OnShowHostHistory -> reduce(viewState.value, event)
is GateContract.Event.OnShowHostBleTable -> reduce(viewState.value, event)
is GateContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is GateContract.Event.OnHistoryIntervalSelect -> reduce(viewState.value, event)
is GateContract.Event.OnSaveReadIntervalChanged -> reduce(viewState.value, event)
is GateContract.Event.OnShowReadIntervalEdit -> reduce(viewState.value, event)
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnSaveReadIntervalChanged
) {
if(state is GateContract.State.Display) {
state.gate.hostState.readInterval = event.interval
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnShowReadIntervalEdit
) {
if(state is GateContract.State.Display) {
setEffect {
GateContract.Effect.Navigation.ReadIntervalSelector(state.gate.hostState.readInterval.toInt())
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnSaveIntervalChanged
) {
if(state is GateContract.State.Display) {
state.gate.hostState.historyInterval = event.interval
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnHistoryIntervalSelect
) {
if(state is GateContract.State.Display) {
setEffect {
GateContract.Effect.Navigation.HistoryIntervalSelector(state.gate.hostState.historyInterval.toInt())
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnShowHostBleTable
) {
if(state is GateContract.State.Display) {
setEffect {
GateContract.Effect.Navigation.BleTable(state.gate.info.serial)
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnShowHostHistory
) {
if(state is GateContract.State.Display) {
setEffect {
GateContract.Effect.Navigation.GateHistory(state.gate.info)
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnPowerChanged
) {
if(state is GateContract.State.Display) {
state.gate.state.tx = event.tx
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnTxSelect
) {
if(state is GateContract.State.Display) {
setEffect { GateContract.Effect.Navigation.TxSelector(state.gate.state.tx) }
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnNavigateUp
) {
setEffect { GateContract.Effect.Navigation.Up }
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnChangePassword
) {
if(state is GateContract.State.Display) {
setEffect {
GateContract.Effect.Navigation.ChangePassword(state.gate.info.serial)
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnHideWriteBlePreview
) {
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnShowWriteBlePreview
) {
if(state is GateContract.State.Display){
val newBle = bleViewMapper.map(state.gate) as Ble.Gate
val writeRequest = Ble.Gate.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
interval = if(newBle.gateState.historyInterval == state.origin.gateState.historyInterval) null else newBle.gateState.historyInterval,
readInterval = if(newBle.gateState.readInterval == state.origin.gateState.readInterval) null else newBle.gateState.readInterval,
)
setState {
state.copy(
writeState = GateContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
GateContract.Effect.ShowWriteBlePreview
}
}
}
private fun reduce(
state: GateContract.State,
event: GateContract.Event.OnWriteBle
) {
if(state is GateContract.State.Display){
state.writeState?.let { request ->
if(request is GateContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = GateContract.State.Display.WriteState.Writing(request.writeRequest)
)
}
val currentState = viewState.value
if(currentState is GateContract.State.Display) {
val newBleObject = Ble.Gate(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
gateState = currentState.origin.gateState.copy(
historyInterval = request.writeRequest.interval
?: currentState.origin.gateState.historyInterval
)
)
writeBle(state.gate.info.serial, request.writeRequest).fold(
onSuccess = {
setState {
currentState.copy(
origin = newBleObject,
gate = bleMapper.map(newBleObject) as BleView.Gate,
writeState = GateContract.State.Display.WriteState.Success
)
}
},
onFailure = {
setState {
state.copy(
writeState = GateContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}
}

View File

@ -0,0 +1,158 @@
package llc.arma.ble.app.ui.screen.inspection.gate.main.view
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateViewModel
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.inspection.thermometer.main.BleMenuItem
import llc.arma.ble.domain.model.Ble
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@Composable
fun DisplayState(
viewModel: GateViewModel,
state: GateContract.State.Display
) {
val scrollState = rememberScrollState()
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.verticalScroll(scrollState)
.padding(horizontal = 16.dp)
) {
BleInfoView(
bleInfo = state.origin.info,
version = state.origin.state.version
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${state.gate.state.tx.value} db",
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
viewModel.setEvent(GateContract.Event.OnTxSelect)
}
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал измерений",
subtitle = state.gate.hostState.historyInterval
.toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
viewModel.setEvent(GateContract.Event.OnHistoryIntervalSelect)
}
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал чтения",
subtitle = state.gate.hostState.readInterval
.toDuration(DurationUnit.MILLISECONDS)
.toComponents { hours, minutes, seconds, _ ->
"$hours ч. $minutes мин. $seconds сек." },
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
) {
viewModel.setEvent(GateContract.Event.OnShowReadIntervalEdit)
}
BleMenuItem(
shapeType = ShapeType.Middle,
title = "График измерений",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
viewModel.setEvent(GateContract.Event.OnShowHostHistory)
}
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Таблица BLE ID",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
viewModel.setEvent(GateContract.Event.OnShowHostBleTable)
}
BleMenuItem(
shapeType = ShapeType.End,
title = "Изменить пароль",
icon = {
Icon(
imageVector = Icons.AutoMirrored.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
) {
viewModel.setEvent(GateContract.Event.OnChangePassword)
}
}
Button(
onClick = {
viewModel.setEvent(GateContract.Event.OnShowWriteBlePreview)
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(text = "Сохранить")
}
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.host.view
package llc.arma.ble.app.ui.screen.inspection.gate.main.view
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@ -24,13 +24,16 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInHour
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInMinute
import llc.arma.ble.app.ui.screen.inspection.selector.duration.millisInSecond
import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable
fun Write(
state: HostContract.State.Display.WriteState,
onEvent: (HostContract.Event) -> Unit
state: GateContract.State.Display.WriteState,
onEvent: (GateContract.Event) -> Unit
) {
Column(
@ -46,7 +49,7 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is HostContract.State.Display.WriteState.DisplayPreview -> {
is GateContract.State.Display.WriteState.DisplayPreview -> {
if(state.writeRequest.tx != null || state.writeRequest.interval != null || state.writeRequest.readInterval !== null) {
@ -174,13 +177,13 @@ fun Write(
PrimaryButton(
label = "Записать"
) {
onEvent(HostContract.Event.OnWriteBle)
onEvent(GateContract.Event.OnWriteBle)
}
SecondaryButton(
label = "Отменить"
) {
onEvent(HostContract.Event.OnHideWriteBlePreview)
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
} else {
@ -198,14 +201,14 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
onEvent(HostContract.Event.OnHideWriteBlePreview)
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
}
is HostContract.State.Display.WriteState.Writing -> {
is GateContract.State.Display.WriteState.Writing -> {
Box {
@ -224,7 +227,7 @@ fun Write(
SecondaryButton(
label = "Отменить"
) {
onEvent(HostContract.Event.OnHideWriteBlePreview)
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
@ -232,7 +235,7 @@ fun Write(
}
}
HostContract.State.Display.WriteState.Success -> {
GateContract.State.Display.WriteState.Success -> {
Box {
@ -266,7 +269,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
onEvent(HostContract.Event.OnHideWriteBlePreview)
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}
@ -274,7 +277,7 @@ fun Write(
}
}
HostContract.State.Display.WriteState.Failure -> {
GateContract.State.Display.WriteState.Failure -> {
Box {
@ -308,7 +311,7 @@ fun Write(
PrimaryButton(
label = "Ок"
) {
onEvent(HostContract.Event.OnHideWriteBlePreview)
onEvent(GateContract.Event.OnHideWriteBlePreview)
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.host.view.table
package llc.arma.ble.app.ui.screen.inspection.gate.table
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
@ -6,7 +6,7 @@ import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
class BleTableEditContract {
class GateBleTableContract {
sealed class Event : ViewEvent {
@ -16,9 +16,7 @@ class BleTableEditContract {
data object OnWrite: Event()
data class OnStart(
val serial: String
) : Event()
data object OnRestart : Event()
data class OnAddBle(
val ble: BleName
@ -63,7 +61,7 @@ class BleTableEditContract {
sealed class Navigation : Effect() {
data object NavigateUp : Navigation()
data object Up : Navigation()
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.host.view.table
package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable
@ -13,15 +13,14 @@ import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -38,40 +37,42 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.ShapeType.Companion.takeShapeType
import llc.arma.ble.app.ui.screen.ble.BleItem
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BleTableEditScreen(
serial: String,
onEvent: (event: BleTableEditContract.Effect.Navigation) -> Unit
fun GateBleTableScreen(
bleSerial: String,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<BleTableEditViewModel>()
val viewModel = hiltViewModel<GateBleTableViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.onEach {
viewModel.effect.collect {
when(it){
is BleTableEditContract.Effect.Navigation -> onEvent(it)
GateBleTableContract.Effect.Navigation.Up ->
navigator.navigateUp()
}
}.launchIn(this)
}
LaunchedEffect(key1 = serial) {
viewModel.setEvent(BleTableEditContract.Event.OnStart(serial))
}
var showSelector by remember {
@ -102,7 +103,7 @@ fun BleTableEditScreen(
if(showSelector){
showSelector = false
} else {
onEvent(BleTableEditContract.Effect.Navigation.NavigateUp)
navigator.popBackStack()
}
}) {
Icon(
@ -125,7 +126,7 @@ fun BleTableEditScreen(
actions = {
if(showSelector.not()){
IconButton(
enabled = state is BleTableEditContract.State.Display,
enabled = state is GateBleTableContract.State.Display,
onClick = { showSelector=true }
) {
Icon(
@ -138,20 +139,20 @@ fun BleTableEditScreen(
)
if(state is BleTableEditContract.State.Loading){
if(state is GateBleTableContract.State.Loading){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
){
CircularProgressIndicator()
ContainedLoadingIndicator()
}
}
if(state is BleTableEditContract.State.Error){
if(state is GateBleTableContract.State.Error){
Box(
contentAlignment = Alignment.Center,
@ -172,7 +173,7 @@ fun BleTableEditScreen(
PrimaryButton(
label = "Повторить"
) {
viewModel.setEvent(BleTableEditContract.Event.OnStart(serial))
viewModel.setEvent(GateBleTableContract.Event.OnRestart)
}
}
@ -181,7 +182,7 @@ fun BleTableEditScreen(
}
if (state is BleTableEditContract.State.Display) {
if (state is GateBleTableContract.State.Display) {
if(showSelector) {
@ -194,7 +195,7 @@ fun BleTableEditScreen(
}
) {
viewModel.setEvent(
BleTableEditContract.Event.OnAddBle(
GateBleTableContract.Event.OnAddBle(
BleName(
serial = it.serial,
name = it.name
@ -210,9 +211,10 @@ fun BleTableEditScreen(
}
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.weight(1f)
.padding(horizontal = 12.dp)
.padding(horizontal = 16.dp)
) {
val savedBleSerials = state.savedBleTable.map { it.serial }
@ -222,60 +224,91 @@ fun BleTableEditScreen(
item {
Text(
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
text = "Новые BLE",
modifier = Modifier
.padding(
horizontal = 12.dp,
vertical = 8.dp
)
)
}
items(items = newBle) {
SelectBleItem(
ble = it,
onClick = {
editBle = it
viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
},
shapeType = if(newBle.size == 1){
ShapeType.Singleton
} else {
if(newBle.indexOf(it) == 0){
ShapeType.Start
} else {
if(newBle.indexOf(it) == newBle.size - 1){
ShapeType.End
} else {
ShapeType.Middle
}
}
}
) {
viewModel.setEvent(BleTableEditContract.Event.OnAddBle(it))
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(it))
}
}
}
if (state.savedBleTable.isNotEmpty()) {
item {
Text(
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
text = "Сохраненные BLE",
modifier = Modifier
.padding(
horizontal = 12.dp,
vertical = 8.dp
)
)
}
items(items = state.savedBleTable) { ble ->
SavedBleItem(
checked = state.newTable.any { it.serial == ble.serial},
ble = ble
ble = ble,
shapeType = if(state.savedBleTable.size == 1){
ShapeType.Singleton
} else {
if(state.savedBleTable.indexOf(ble) == 0){
ShapeType.Start
} else {
if(state.savedBleTable.indexOf(ble) == state.savedBleTable.size - 1){
ShapeType.End
} else {
ShapeType.Middle
}
}
}
){
viewModel.setEvent(BleTableEditContract.Event.OnAddBle(ble))
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(ble))
}
}
}
}
PrimaryButton(
label = "Записать"
) {
viewModel.setEvent(BleTableEditContract.Event.OnWritePreview)
viewModel.setEvent(GateBleTableContract.Event.OnWritePreview)
}
if(editBle != null){
Dialog(
onDismissRequest = {
viewModel.setEvent(BleTableEditContract.Event.OnAddBle(ble = editBle!!.copy()))
viewModel.setEvent(GateBleTableContract.Event.OnAddBle(ble = editBle!!.copy()))
editBle = null
}
) {
@ -310,7 +343,7 @@ fun BleTableEditScreen(
label = "Сохранить"
) {
viewModel.setEvent(
BleTableEditContract.Event.OnAddBle(
GateBleTableContract.Event.OnAddBle(
ble = editBle!!.copy(name = name)
)
)
@ -327,7 +360,7 @@ fun BleTableEditScreen(
LaunchedEffect(key1 = bottomDialog.sheetState?.isVisible) {
if (bottomDialog.sheetState?.isVisible?.not() == true) {
viewModel.setEvent(BleTableEditContract.Event.OnHideWritePreview)
viewModel.setEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
@ -356,6 +389,11 @@ fun BleTableEditScreen(
}
@Composable
private fun DisplayState(){
}
@Composable
fun BleSelectorScreen(
saved: List<String>,
@ -370,6 +408,7 @@ fun BleSelectorScreen(
) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f)
) {
@ -390,7 +429,10 @@ fun BleSelectorScreen(
onCheckedChange = null
)
BleItem(ble = ble) {
BleItem(
shapeType = bleList.filterNot { saved.contains(it.serial) }.takeShapeType(ble),
ble = ble
) {
onAddBle(ble)
}
@ -412,19 +454,27 @@ fun BleSelectorScreen(
@Composable
fun SelectBleItem(
shapeType: ShapeType,
ble: BleName,
onClick: (() -> Unit)? = null,
onRemove: (() -> Unit)? = null,
){
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = shapeType.shape,
onClick = onClick ?: {}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable { onClick?.invoke() }
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(
vertical = 8.dp,
horizontal = 16.dp
)
) {
@ -456,23 +506,30 @@ fun SelectBleItem(
}
}
}
@Composable
fun SavedBleItem(
shapeType: ShapeType,
checked: Boolean,
ble: BleName,
onClick: () -> Unit
){
Surface(
color = MaterialTheme.colorScheme.surfaceContainer,
shape = shapeType.shape,
onClick = onClick
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable { onClick() }
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(vertical = 12.dp, horizontal = 16.dp)
.padding(end = 12.dp)
) {
@ -487,4 +544,6 @@ fun SavedBleItem(
}
}
}

View File

@ -1,6 +1,9 @@
package llc.arma.ble.app.ui.screen.inspection.host.view.table
package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first
@ -14,82 +17,85 @@ import llc.arma.ble.domain.usecase.GetHostBleTableBySerial
import javax.inject.Inject
@HiltViewModel
class BleTableEditViewModel @Inject constructor(
getFoundBle: GetFoundBle,
class GateBleTableViewModel @Inject constructor(
private val getFoundBle: GetFoundBle,
private val savedStateHandle: SavedStateHandle,
private val getBleNamesFlow: GetBleNamesFlow,
private val addBleToHostTable: AddBleToHostTable,
private val getHostBleTableBySerial: GetHostBleTableBySerial
) : BaseViewModel<BleTableEditContract.State, BleTableEditContract.Event, BleTableEditContract.Effect>() {
private var lastSerial: String = ""
) : BaseViewModel<GateBleTableContract.State, GateBleTableContract.Event, GateBleTableContract.Effect>() {
init {
setEvent(GateBleTableContract.Event.OnRestart)
viewModelScope.launch {
while (true){
val state = viewState.value
if(state is BleTableEditContract.State.Display) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(bleAround = getFoundBle())
}
}
delay(1_000)
}
}
}
override fun setInitialState() = BleTableEditContract.State.Loading
override fun setInitialState() = GateBleTableContract.State.Loading
override fun handleEvents(event: BleTableEditContract.Event) {
override fun handleEvents(event: GateBleTableContract.Event) {
when(event){
is BleTableEditContract.Event.OnStart -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnAddBle -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnWritePreview -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnWrite -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWritePreview -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
is GateBleTableContract.Event.OnWrite -> reduce(viewState.value, event)
}
}
private fun reduce(
state: BleTableEditContract.State,
event: BleTableEditContract.Event.OnWrite
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWrite
) {
if(state is BleTableEditContract.State.Display) {
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
if(state is GateBleTableContract.State.Display) {
viewModelScope.launch {
setState {
state.copy(
writeState = BleTableEditContract.State.Display.WriteState.Writing(state.newTable)
writeState = GateBleTableContract.State.Display.WriteState.Writing(state.newTable)
)
}
addBleToHostTable.invoke(
serial = lastSerial,
serial = params.bleSerial,
ble = state.newTable
).fold(
onSuccess = {
setState {
state.copy(
writeState = BleTableEditContract.State.Display.WriteState.Success
writeState = GateBleTableContract.State.Display.WriteState.Success
)
}
setEvent(BleTableEditContract.Event.OnStart(lastSerial))
setEvent(GateBleTableContract.Event.OnRestart)
},
onFailure = {
setState {
state.copy(
writeState = BleTableEditContract.State.Display.WriteState.Failure
writeState = GateBleTableContract.State.Display.WriteState.Failure
)
}
}
@ -102,11 +108,11 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
state: BleTableEditContract.State,
event: BleTableEditContract.Event.OnHideWritePreview
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnHideWritePreview
) {
if(state is BleTableEditContract.State.Display) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(writeState = null)
@ -117,14 +123,17 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
state: BleTableEditContract.State,
event: BleTableEditContract.Event.OnWritePreview
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnWritePreview
) {
if(state is BleTableEditContract.State.Display) {
if(state is GateBleTableContract.State.Display) {
setState {
state.copy(writeState = BleTableEditContract.State.Display.WriteState.DisplayPreview(state.newTable))
state.copy(writeState = GateBleTableContract.State.Display.WriteState.DisplayPreview(
state.newTable
)
)
}
}
@ -132,11 +141,11 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
state: BleTableEditContract.State,
event: BleTableEditContract.Event.OnAddBle
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnAddBle
) {
if(state is BleTableEditContract.State.Display) {
if(state is GateBleTableContract.State.Display) {
if(state.newTable.any { it.serial == event.ble.serial}){
@ -157,21 +166,22 @@ class BleTableEditViewModel @Inject constructor(
}
private fun reduce(
state: BleTableEditContract.State,
event: BleTableEditContract.Event.OnStart
state: GateBleTableContract.State,
event: GateBleTableContract.Event.OnRestart
) {
lastSerial = event.serial
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
setState {
BleTableEditContract.State.Loading
GateBleTableContract.State.Loading
}
viewModelScope.launch {
val names = getBleNamesFlow.invoke().first()
getHostBleTableBySerial(event.serial).fold(
getHostBleTableBySerial(params.bleSerial).fold(
onSuccess = {
val savedBle = it.map { ble -> BleName(
@ -179,17 +189,18 @@ class BleTableEditViewModel @Inject constructor(
serial = ble) }
setState {
BleTableEditContract.State.Display(
GateBleTableContract.State.Display(
bleAround = emptyList(),
newTable = savedBle,
savedBleTable = savedBle,
writeState = null)
writeState = null
)
}
},
onFailure = {
setState {
BleTableEditContract.State.Error
GateBleTableContract.State.Error
}
}
)

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.host.view.table
package llc.arma.ble.app.ui.screen.inspection.gate.table
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@ -24,11 +24,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.ShapeType
@Composable
fun Write(
state: BleTableEditContract.State.Display.WriteState,
onEvent: (BleTableEditContract.Event) -> Unit
state: GateBleTableContract.State.Display.WriteState,
onEvent: (GateBleTableContract.Event) -> Unit
) {
Column(
@ -44,7 +45,7 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp))
when (state) {
is BleTableEditContract.State.Display.WriteState.DisplayPreview -> {
is GateBleTableContract.State.Display.WriteState.DisplayPreview -> {
Box(
modifier = Modifier
@ -66,7 +67,7 @@ fun Write(
}
items(items = state.writeRequest) {
SelectBleItem(it)
SelectBleItem(ShapeType.Singleton, it)
}
if(state.writeRequest.isEmpty()){
@ -87,18 +88,18 @@ fun Write(
PrimaryButton(
label = "Записать"
) {
onEvent(BleTableEditContract.Event.OnWrite)
onEvent(GateBleTableContract.Event.OnWrite)
}
SecondaryButton (
label = "Отменить"
) {
onEvent(BleTableEditContract.Event.OnHideWritePreview)
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
is BleTableEditContract.State.Display.WriteState.Writing -> {
is GateBleTableContract.State.Display.WriteState.Writing -> {
Column {
@ -115,13 +116,13 @@ fun Write(
SecondaryButton (
label = "Отменить"
) {
onEvent(BleTableEditContract.Event.OnHideWritePreview)
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
BleTableEditContract.State.Display.WriteState.Success -> {
GateBleTableContract.State.Display.WriteState.Success -> {
Column {
@ -153,13 +154,13 @@ fun Write(
PrimaryButton(
label = "Ok"
) {
onEvent(BleTableEditContract.Event.OnHideWritePreview)
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}
}
BleTableEditContract.State.Display.WriteState.Failure -> {
GateBleTableContract.State.Display.WriteState.Failure -> {
Column {
@ -191,7 +192,7 @@ fun Write(
PrimaryButton(
label = "Ok"
) {
onEvent(BleTableEditContract.Event.OnHideWritePreview)
onEvent(GateBleTableContract.Event.OnHideWritePreview)
}
}

View File

@ -1,178 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.TxLevelSelector
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.inspection.host.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.host.view.IntervalEdit
import llc.arma.ble.app.ui.screen.inspection.host.view.ReadIntervalEdit
import llc.arma.ble.app.ui.screen.inspection.host.view.Write
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
WRITE, POWER_EDIT, INTERVAL_EDIT, READ_INTERVAL_EDIT
}
@Composable
fun HostScreen(
ble: Ble.Host,
onNavigationEvent: (HostContract.Effect.Navigation) -> Unit
) {
val viewModel = hiltViewModel<HostViewModel>()
val state = viewModel.viewState.value
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val bottomDialog = rememberBottomDialogState()
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is HostContract.Effect.Navigation -> onNavigationEvent(it)
HostContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null
}
HostContract.Effect.ShowWriteBlePreview -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
HostContract.Effect.HidePowerPicker -> launch {
sheetPage = null
}
HostContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER_EDIT
}
HostContract.Effect.HideIntervalPicker -> launch {
sheetPage = null
}
HostContract.Effect.ShowIntervalPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.INTERVAL_EDIT
}
HostContract.Effect.HideReadIntervalPicker -> launch {
sheetPage = null
}
HostContract.Effect.ShowReadIntervalPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.READ_INTERVAL_EDIT
}
}
}.launchIn(this)
}
LaunchedEffect(ble){
viewModel.setEvent(HostContract.Event.OnBleChanged(ble))
}
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display && currentState.writeState != null) {
Write(
state = currentState.writeState,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.POWER_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display) {
TxLevelSelector(
tx = currentState.host.state.tx
) {
viewModel.setEvent(HostContract.Event.OnPowerChanged(it))
}
}
}
SheetPage.INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display) {
IntervalEdit(
state = currentState.host,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
SheetPage.READ_INTERVAL_EDIT -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is HostContract.State.Display) {
ReadIntervalEdit(
state = currentState.host,
onEvent = {
viewModel.setEvent(it)
}
)
}
}
else -> {
bottomDialog.hide()
}
}
}
Column {
when(state){
is HostContract.State.Display -> DisplayState(
onEvent = {
viewModel.setEvent(it)
},
ble = state.host,
origin = state.origin
)
is HostContract.State.Loading -> LoadingState()
}
}
}
@Composable
private fun LoadingState(){
Box(modifier = Modifier.fillMaxSize()){
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}

View File

@ -1,313 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class HostViewModel @Inject constructor(
private val bleMapper: BleMapper,
private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper
) : BaseViewModel<HostContract.State, HostContract.Event, HostContract.Effect>() {
override fun setInitialState() = HostContract.State.Loading
override fun handleEvents(event: HostContract.Event) {
when(event){
is HostContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is HostContract.Event.OnTxChanged -> reduce(viewState.value, event)
is HostContract.Event.OnBleChanged -> reduce(viewState.value, event)
is HostContract.Event.OnChangePassword -> reduce(viewState.value, event)
is HostContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is HostContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is HostContract.Event.OnWriteBle -> reduce(viewState.value, event)
is HostContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is HostContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is HostContract.Event.OnShowHostHistory -> reduce(viewState.value, event)
is HostContract.Event.OnShowHostBleTable -> reduce(viewState.value, event)
is HostContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is HostContract.Event.OnShowIntervalEdit -> reduce(viewState.value, event)
is HostContract.Event.OnSaveReadIntervalChanged -> reduce(viewState.value, event)
is HostContract.Event.OnShowReadIntervalEdit -> reduce(viewState.value, event)
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnSaveReadIntervalChanged
) {
if(state is HostContract.State.Display) {
state.host.hostState.readInterval = event.interval
}
setEffect {
HostContract.Effect.HideReadIntervalPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowReadIntervalEdit
) {
setEffect {
HostContract.Effect.ShowReadIntervalPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnSaveIntervalChanged
) {
if(state is HostContract.State.Display) {
state.host.hostState.historyInterval = event.interval
}
setEffect {
HostContract.Effect.HideIntervalPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowIntervalEdit
) {
setEffect {
HostContract.Effect.ShowIntervalPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowHostBleTable
) {
if(state is HostContract.State.Display) {
setEffect {
HostContract.Effect.Navigation.NavigateToBleTable(state.host.info.serial)
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowHostHistory
) {
if(state is HostContract.State.Display) {
setEffect {
HostContract.Effect.Navigation.NavigateToHostHistory(state.host.info)
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnPowerChanged
) {
if(state is HostContract.State.Display) {
state.host.state.tx = event.tx
}
setEffect {
HostContract.Effect.HidePowerPicker
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnPowerEdit
) {
setEffect { HostContract.Effect.ShowPowerPicker }
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnNavigateUpClicked
) {
setEffect { HostContract.Effect.Navigation.NavigateUp }
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnTxChanged
) {
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnBleChanged
) {
when(state){
is HostContract.State.Display -> {
setState {
state.copy(
origin = Ble.Host(
info = event.ble.info,
state = state.origin.state,
hostState = state.origin.hostState
)
)
}
}
is HostContract.State.Loading -> {
setState {
HostContract.State.Display(
origin = event.ble,
host = bleMapper.map(event.ble) as BleView.Host,
writeState = null
)
}
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnChangePassword
) {
setEffect {
HostContract.Effect.Navigation.NavigateToChangePassword
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnHideWriteBlePreview
) {
setEffect {
HostContract.Effect.HideWriteBlePreview
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnShowWriteBlePreview
) {
if(state is HostContract.State.Display){
val newBle = bleViewMapper.map(state.host) as Ble.Host
val writeRequest = Ble.Host.WriteRequest(
tx = if(newBle.state.tx == state.origin.state.tx) null else newBle.state.tx,
interval = if(newBle.hostState.historyInterval == state.origin.hostState.historyInterval) null else newBle.hostState.historyInterval,
readInterval = if(newBle.hostState.readInterval == state.origin.hostState.readInterval) null else newBle.hostState.readInterval,
)
setState {
state.copy(
writeState = HostContract.State.Display.WriteState.DisplayPreview(
writeRequest
)
)
}
setEffect {
HostContract.Effect.ShowWriteBlePreview
}
}
}
private fun reduce(
state: HostContract.State,
event: HostContract.Event.OnWriteBle
) {
if(state is HostContract.State.Display){
state.writeState?.let { request ->
if(request is HostContract.State.Display.WriteState.DisplayPreview) {
viewModelScope.launch {
setState {
state.copy(
writeState = HostContract.State.Display.WriteState.Writing(request.writeRequest)
)
}
val currentState = viewState.value
if(currentState is HostContract.State.Display) {
val newBleObject = Ble.Host(
info = currentState.origin.info,
state = currentState.origin.state.copy(
tx = request.writeRequest.tx ?: state.origin.state.tx
),
hostState = currentState.origin.hostState.copy(
historyInterval = request.writeRequest.interval
?: currentState.origin.hostState.historyInterval
)
)
writeBle(state.host.info.serial, request.writeRequest).fold(
onSuccess = {
setState {
currentState.copy(
origin = newBleObject,
host = bleMapper.map(newBleObject) as BleView.Host,
writeState = HostContract.State.Display.WriteState.Success
)
}
},
onFailure = {
setState {
state.copy(
writeState = HostContract.State.Display.WriteState.Failure
)
}
}
)
}
}
}
}
}
}
}

View File

@ -1,198 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
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.draw.shadow
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.domain.model.Ble
@Composable
fun DisplayState(
onEvent: (HostContract.Event) -> Unit,
origin: Ble.Host,
ble: BleView.Host
) {
val scrollState = rememberScrollState()
Column {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
)
}
Column(
modifier = Modifier,
content = {
BleMenuItem(
title = "Мощность",
subtitle = "${ble.state.tx.value} db",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(HostContract.Event.OnPowerEdit)
}
var hours =
ble.hostState.historyInterval / millisInHour
var minutes =
(ble.hostState.historyInterval - (hours * millisInHour)) / millisInMinute
var seconds =
(ble.hostState.historyInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек.",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(HostContract.Event.OnShowIntervalEdit)
}
hours = ble.hostState.readInterval / millisInHour
minutes =
(ble.hostState.readInterval - (hours * millisInHour)) / millisInMinute
seconds =
(ble.hostState.readInterval - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem(
title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек.",
icon = rememberVectorPainter(Icons.Rounded.KeyboardArrowDown)
) {
onEvent(HostContract.Event.OnShowReadIntervalEdit)
}
BleMenuItem(
title = "График измерений",
icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
) {
onEvent(HostContract.Event.OnShowHostHistory)
}
BleMenuItem(
title = "Таблица BLE ID",
icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
) {
onEvent(HostContract.Event.OnShowHostBleTable)
}
BleMenuItem(
title = "Изменить пароль",
icon = rememberVectorPainter(Icons.AutoMirrored.Rounded.KeyboardArrowRight)
) {
onEvent(HostContract.Event.OnChangePassword)
}
}
)
}
PrimaryButton(
modifier = Modifier.shadow(
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
) {
onEvent(HostContract.Event.OnShowWriteBlePreview)
}
}
}
@Composable
fun BleMenuItem(
title: String,
subtitle: String? = null,
icon: Painter? = null,
onClick: (() -> Unit) = {}
){
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(onClick = onClick)
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title
)
subtitle?.let {
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = it
)
}
}
if (icon != null) {
Icon(
painter = icon,
contentDescription = null
)
}
}
}
}

View File

@ -1,705 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host.view
import android.content.res.Configuration
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowColumn
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ContentAlpha
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisLineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.column.columnChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollSpec
import com.patrykandpatrick.vico.compose.component.shapeComponent
import com.patrykandpatrick.vico.compose.component.textComponent
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.chart.scale.AutoScaleUp
import com.patrykandpatrick.vico.core.component.marker.MarkerComponent
import com.patrykandpatrick.vico.core.component.shape.LineComponent
import com.patrykandpatrick.vico.core.component.shape.Shapes.pillShape
import com.patrykandpatrick.vico.core.dimensions.MutableDimensions
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.composed.ComposedChartEntryModelProducer
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName
import llc.arma.ble.domain.usecase.ExportToXlsx
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.GetBleNamesFlow
import llc.arma.ble.domain.usecase.GetHostHistoryBySerial
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class HostEntry(
val localDate: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = HostEntry(localDate, x, y)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HostHistory(
ble: BleInfo,
onDismiss: (() -> Unit)? = null,
) {
val viewModel = hiltViewModel<HostHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(ble.serial) {
viewModel.setEvent(HostHistoryContract.Event.OnStart(ble.name, ble.serial))
}
Column {
TopAppBar(
navigationIcon = {
onDismiss?.let {
IconButton(onClick = it) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
},
title = {
val title = when(state){
is HostHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "Таблица (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "Таблица"
is ProgressState.Progress -> "Таблица"
}
}
HostHistoryContract.State.Exception -> "Таблица"
}
Text(
modifier = Modifier.weight(1f),
text = title,
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(HostHistoryContract.Event.OnExportHistory(ble.serial))
},
enabled = when(state){
is HostHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
HostHistoryContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Save,
contentDescription = null
)
}
IconButton(
onClick = {
viewModel.setEvent(HostHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial))
},
enabled = when(state){
is HostHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
HostHistoryContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
Box(modifier = Modifier) {
when (state) {
is HostHistoryContract.State.Display -> Display(state = state)
is HostHistoryContract.State.Exception -> Exception()
}
}
}
}
val dayFormatter = SimpleDateFormat("dd", Locale.getDefault())
val dateFormatter = SimpleDateFormat("dd.MM", Locale.getDefault())
val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
val colorsStack = listOf(
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
Color(0xff2f4f4f), Color(0xff7f0000), Color(0xFFFF0000), Color(0xffffd700),
Color(0xffa9a9a9), Color(0xff00fa9a), Color(0xff00ffff), Color(0xfff0e68c),
Color(0xff00bfff), Color(0xff0000ff), Color(0xfff08080), Color(0xffadff2f),
Color(0xffff00ff), Color(0xff4169e1), Color(0xffff1493), Color(0xffee82ee),
)
val startAxisValueFormatter =
AxisValueFormatter<AxisPosition.Vertical.Start> { value, chartValues ->
" "
}
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
val first = (chartValues.chartEntryModel.entries.firstOrNull()?.firstOrNull() as? HostEntry)
val last = (chartValues.chartEntryModel.entries.firstOrNull()?.lastOrNull() as? HostEntry)
val previous = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt() - 1) as? HostEntry)
val current = (chartValues.chartEntryModel.entries.firstOrNull()?.getOrNull(value.toInt()) as? HostEntry)
if(current != null) {
if (first == current || last == current) {
dateFormatter.format(Date(current.localDate))
} else {
if(previous != null && dayFormatter.format(previous.localDate) != dayFormatter.format(current.localDate)){
dateFormatter.format(Date(current.localDate))
}else{
timeFormatter.format(Date(current.localDate))
}
}.orEmpty()
} else {
" "
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun Display(
state: HostHistoryContract.State.Display
) {
val configuration = LocalConfiguration.current
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) {
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
val allSerials = remember(state) { state.loadingHistoryState.data.flatMap { it.value }.distinct() }
val colors = remember(allSerials) {
allSerials.mapIndexed { index, s ->
Pair(s, colorsStack[index])
}.toMap()
}
var selectedSerials by rememberSaveable {
mutableStateOf(allSerials.take(1))
}
val serials = remember(selectedSerials) { allSerials.filter { selectedSerials.contains(it) } }
val entries = remember(serials, state) {
serials.map { serial ->
ChartEntryModelProducer(
state.loadingHistoryState.data.mapIndexed { index, historyPoint ->
if(historyPoint.value.contains(serial)) {
HostEntry(historyPoint.date, index.toFloat(), 1f)
} else {
HostEntry(historyPoint.date, index.toFloat(), 0.0001f)
}
}
)
}
}
val producer = remember(entries) { ComposedChartEntryModelProducer(entries) }
val chart = columnChart(
persistentMarkers = state.loadingHistoryState.data.mapIndexedNotNull { index, historyPoint ->
if(historyPoint.hit) {
Pair(
index.toFloat(),
MarkerComponent(
label = textComponent(
textSize = 0.sp,
padding = MutableDimensions(10f, 10f, 10f, 10f),
margins = MutableDimensions(10f, 10f, 10f, 10f),
color = Color.Red,
background = shapeComponent(
color = Color.Red,
shape = pillShape
)
),
indicator = null,
guideline = axisLineComponent(
thickness = 0.dp
)
)
)
}else{
null
}
}.toMap(),
innerSpacing = 2.dp,
columns = serials.map { LineComponent(color = colors[it]!!.toArgb(), thicknessDp = 7f, shape= pillShape) },
spacing = 8.dp,
)
@Composable
fun LegendItem(s: String) {
FilterChip(
selected = selectedSerials.contains(s),
onClick = {
selectedSerials = if(selectedSerials.contains(s)){
selectedSerials.toMutableList().apply {
remove(s)
}
}else{
selectedSerials.toMutableList().apply {
add(s)
}
}
},
leadingIcon = {
Surface(
shape = CircleShape,
color = colors[s]!!,
modifier = Modifier.size(28.dp)
) {}
},
label = { Column {
Text(text = state.bleNames.firstOrNull { it.serial == s }?.name ?: s)
Text(
style = MaterialTheme.typography.bodySmall.copy(
color = LocalTextStyle.current.color.copy(
alpha = ContentAlpha.medium
)
),
text = s
)
}}
)
}
when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Chart(
chart = chart,
chartModelProducer = producer,
bottomAxis = bottomAxis(
labelRotationDegrees = -90f,
valueFormatter = axisValueFormatter,
tickLength = 0.dp,
),
startAxis = startAxis(
valueFormatter = startAxisValueFormatter
),
modifier = Modifier
.fillMaxHeight()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
VerticalDivider()
FlowRow(
maxItemsInEachRow = 1,
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.verticalScroll(rememberScrollState())
) {
allSerials.mapIndexed { _, s ->
LegendItem(s = s)
}
}
}
}
else -> {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
FlowColumn(
maxItemsInEachColumn = 2,
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.horizontalScroll(rememberScrollState())
) {
allSerials.mapIndexed { _, s ->
LegendItem(s = s)
}
}
HorizontalDivider()
Chart(
chart = chart,
chartModelProducer = producer,
bottomAxis = bottomAxis(
labelRotationDegrees = -90f,
valueFormatter = axisValueFormatter,
tickLength = 0.dp,
),
modifier = Modifier
.fillMaxWidth()
.weight(1f),
autoScaleUp = AutoScaleUp.None,
diffAnimationSpec = tween(0),
chartScrollSpec = rememberChartScrollSpec(
initialScroll = InitialScroll.End,
autoScrollCondition = AutoScrollCondition.OnModelSizeIncreased,
autoScrollAnimationSpec = tween(0)
)
)
}
}
}
}
}
is ProgressState.Indeterminate -> {
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
), label = ""
)
CircularProgressIndicator(
strokeCap = StrokeCap.Round,
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
private fun Exception() {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}
class HostHistoryContract {
sealed class Event : ViewEvent {
data object StopMeasure : Event()
data class OnStart(
val bleName: String,
val serial: String,
) : Event()
data class OnRefreshHistory(
val bleName: String,
val serial: String,
) : Event()
data class OnExportHistory(
val serial: String,
) : Event()
}
sealed class State : ViewState {
data class Display(
val bleName: String,
val bleNames: List<BleName>,
val loadingHistoryState : ProgressState<List<Ble.Host.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
}
}
@HiltViewModel
class HostHistoryViewModel @Inject constructor(
private val getHostHistoryBySerial: GetHostHistoryBySerial,
private val getBleNamesFlow: GetBleNamesFlow,
private val exportToXlsx: ExportToXlsx
) : BaseViewModel<HostHistoryContract.State, HostHistoryContract.Event, HostHistoryContract.Effect>() {
private var measureJob: Job? = null
private var lastSerial: String? = null
override fun setInitialState() = HostHistoryContract.State.Display(
"",
emptyList(),
ProgressState.Indeterminate
)
override fun handleEvents(event: HostHistoryContract.Event) {
when(event){
is HostHistoryContract.Event.OnStart -> reduce(viewState.value, event)
is HostHistoryContract.Event.OnRefreshHistory -> reduce(viewState.value, event)
is HostHistoryContract.Event.StopMeasure -> reduce(viewState.value, event)
is HostHistoryContract.Event.OnExportHistory -> reduce(viewState.value, event)
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.OnExportHistory
) {
if(state is HostHistoryContract.State.Display) {
if(state.loadingHistoryState is ProgressState.Finished) {
exportToXlsx.invoke(event.serial, state.loadingHistoryState.data)
}
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.StopMeasure
) {
measureJob?.cancel()
measureJob = null
setState {
HostHistoryContract.State.Exception
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.OnStart
) {
viewModelScope.launch {
if(state is HostHistoryContract.State.Display) {
if(lastSerial != event.serial) {
lastSerial = event.serial
setState {
HostHistoryContract.State.Display(event.bleName, emptyList(), ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
val names = getBleNamesFlow().first()
measureJob = getHostHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
HostHistoryContract.State.Display(event.bleName, names, it)
}
},
onFailure = {
setState {
HostHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}
}
private fun reduce(
state: HostHistoryContract.State,
event: HostHistoryContract.Event.OnRefreshHistory
) {
viewModelScope.launch {
setState {
HostHistoryContract.State.Display("", emptyList(), ProgressState.Indeterminate)
}
measureJob?.cancel()
measureJob = null
val names = getBleNamesFlow().first()
measureJob = getHostHistoryBySerial(event.serial).onEach {
it.fold(
onSuccess = {
setState {
HostHistoryContract.State.Display(event.bleName, names, it)
}
},
onFailure = {
setState {
HostHistoryContract.State.Exception
}
}
)
}.launchIn(this)
}
}
}

View File

@ -1,65 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.host.view
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
@Composable
fun ReadIntervalEdit(
state: BleView.Host,
onEvent: (HostContract.Event) -> Unit,
){
var value by remember(state.hostState.readInterval) {
mutableIntStateOf((state.hostState.readInterval).toInt())
}
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Интервал чтения",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
DurationPicker(
modifier = Modifier.align(Alignment.CenterHorizontally),
value = value
) {
value = it
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Применить"
) {
onEvent(
HostContract.Event.OnSaveReadIntervalChanged(
value.toLong()
)
)
}
}
}

View File

@ -0,0 +1,37 @@
package llc.arma.ble.app.ui.screen.inspection.selector.duration
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
class DurationSelectorContract {
sealed class Event : ViewEvent {
data class OnDurationChanged(
val duration: Int
) : Event()
data object OnSave : Event()
}
data class State(
val duration: Int
) : ViewState
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object Up : Navigation()
data class UpWithResult(
val duration: Int
) : Navigation()
}
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.host.view
package llc.arma.ble.app.ui.screen.inspection.selector.duration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
@ -10,15 +10,21 @@ import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -29,22 +35,54 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
import com.ramcosta.composedestinations.result.ResultBackNavigator
import com.ramcosta.composedestinations.spec.DestinationStyle
import kotlinx.serialization.Serializable
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.host.HostContract
import llc.arma.ble.app.ui.screen.inspection.gate.main.GateContract
@Serializable
data class DurationSelectResult(
val qualifier: String?,
val duration: Int,
)
@Destination<RootGraph>(style = DestinationStyleBottomSheet::class)
@Composable
fun IntervalEdit(
state: BleView.Host,
onEvent: (HostContract.Event) -> Unit,
){
fun DurationSelectorScreen(
qualifier: String? = null,
duration: Int?,
minimum: Int = 10_000,
maximum: Int = 10 * 24 * 60 * 60 * 1000,
daysComponent: Boolean = true,
hoursComponent: Boolean = true,
minutesComponent: Boolean = true,
secondsComponent: Boolean = true,
resultNavigator: ResultBackNavigator<DurationSelectResult>
) {
var value by remember(state.hostState.historyInterval) {
mutableIntStateOf((state.hostState.historyInterval).toInt())
val viewModel = hiltViewModel<DurationSelectorViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
DurationSelectorContract.Effect.Navigation.Up ->
resultNavigator.navigateBack()
is DurationSelectorContract.Effect.Navigation.UpWithResult ->
resultNavigator.navigateBack(DurationSelectResult(qualifier, it.duration))
}
}
}
Column(
modifier = Modifier
modifier = Modifier.padding(16.dp).fillMaxWidth()
) {
Text(
@ -56,22 +94,30 @@ fun IntervalEdit(
Spacer(modifier = Modifier.height(16.dp))
DurationPicker(
modifier = Modifier.align(Alignment.CenterHorizontally),
value = value
minInterval = minimum,
maxInterval = maximum,
days = daysComponent,
hours = hoursComponent,
minutes = minutesComponent,
seconds = secondsComponent,
value = state.duration,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
value = it
viewModel.setEvent(DurationSelectorContract.Event.OnDurationChanged(it))
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Применить"
Button(
onClick = {
viewModel.setEvent(DurationSelectorContract.Event.OnSave)
},
modifier = Modifier.fillMaxWidth()
) {
onEvent(
HostContract.Event.OnSaveIntervalChanged(
value.toLong()
)
Text(
text = "Применить"
)
}
}

View File

@ -0,0 +1,52 @@
package llc.arma.ble.app.ui.screen.inspection.selector.duration
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import llc.arma.ble.app.ui.common.BaseViewModel
import javax.inject.Inject
@HiltViewModel
class DurationSelectorViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) : BaseViewModel<DurationSelectorContract.State, DurationSelectorContract.Event, DurationSelectorContract.Effect>() {
init {
val params = DurationSelectorScreenDestination.argsFrom(savedStateHandle)
setState { DurationSelectorContract.State(params.duration ?: 0) }
}
override fun setInitialState() = DurationSelectorContract.State(0)
override fun handleEvents(event: DurationSelectorContract.Event) {
when(event){
is DurationSelectorContract.Event.OnDurationChanged -> reduce(viewState.value, event)
is DurationSelectorContract.Event.OnSave -> reduce(viewState.value, event)
}
}
private fun reduce(
state: DurationSelectorContract.State,
event: DurationSelectorContract.Event.OnDurationChanged,
) {
setState { DurationSelectorContract.State(event.duration) }
}
private fun reduce(
state: DurationSelectorContract.State,
event: DurationSelectorContract.Event.OnSave,
) {
setEffect {
DurationSelectorContract.Effect.Navigation.UpWithResult(state.duration)
}
}
}

View File

@ -0,0 +1,40 @@
package llc.arma.ble.app.ui.screen.inspection.selector.power
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView
class TxPowerSelectorContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data class OnSelected(
val tx: BleView.BleState.TX
) : Event()
data object OnSave : Event()
}
data class State(
val tx: BleView.BleState.TX?
) : ViewState
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object NavigateUp : Navigation()
data class NavigateUpWithResult(
val tx: BleView.BleState.TX
) : Navigation()
}
}
}

View File

@ -0,0 +1,79 @@
package llc.arma.ble.app.ui.screen.inspection.selector.power
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultBackNavigator
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.accelerometer.main.view.SelectorItem
@Destination<RootGraph>(style = DestinationStyleBottomSheet::class)
@Composable
fun TxPowerSelectorScreen(
tx: BleView.BleState.TX?,
resultNavigator: ResultBackNavigator<BleView.BleState.TX>
) {
val viewModel = hiltViewModel<TxPowerSelectorViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
TxPowerSelectorContract.Effect.Navigation.NavigateUp ->
resultNavigator.navigateBack()
is TxPowerSelectorContract.Effect.Navigation.NavigateUpWithResult ->
resultNavigator.navigateBack(it.tx)
}
}
}
Column {
Text(
text = "Мощность",
style = MaterialTheme.typography.titleLarge
)
BleView.BleState.TX.entries.forEach {
SelectorItem(
label = "${it.value} dBb (${it.powerPercentage} %)",
selected = it == state.tx
){
viewModel.setEvent(TxPowerSelectorContract.Event.OnSelected(it))
}
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Применить"
) {
viewModel.setEvent(TxPowerSelectorContract.Event.OnSave)
}
}
}

View File

@ -0,0 +1,70 @@
package llc.arma.ble.app.ui.screen.inspection.selector.power
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import llc.arma.ble.app.ui.common.BaseViewModel
import javax.inject.Inject
@HiltViewModel
class TxPowerSelectorViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : BaseViewModel<TxPowerSelectorContract.State, TxPowerSelectorContract.Event, TxPowerSelectorContract.Effect>() {
init {
val params = TxPowerSelectorScreenDestination.argsFrom(savedStateHandle)
setState { TxPowerSelectorContract.State(params.tx) }
}
override fun setInitialState() = TxPowerSelectorContract.State(null)
override fun handleEvents(event: TxPowerSelectorContract.Event) {
when(event){
is TxPowerSelectorContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is TxPowerSelectorContract.Event.OnSave -> reduce(viewState.value, event)
is TxPowerSelectorContract.Event.OnSelected -> reduce(viewState.value, event)
}
}
private fun reduce(
state: TxPowerSelectorContract.State,
event: TxPowerSelectorContract.Event.OnNavigateUp
){
setEffect {
TxPowerSelectorContract.Effect.Navigation.NavigateUp
}
}
private fun reduce(
state: TxPowerSelectorContract.State,
event: TxPowerSelectorContract.Event.OnSave
){
if(state.tx != null) {
setEffect {
TxPowerSelectorContract.Effect.Navigation.NavigateUpWithResult(state.tx)
}
}
}
private fun reduce(
state: TxPowerSelectorContract.State,
event: TxPowerSelectorContract.Event.OnSelected
){
setState {
TxPowerSelectorContract.State(event.tx)
}
}
}

View File

@ -1,183 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.TxLevelSelector
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.DisplayState
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.IntervalEdit
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.LoadingState
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.TemperatureHistory
import llc.arma.ble.app.ui.screen.inspection.thermometer.view.Write
import llc.arma.ble.domain.model.Ble
enum class SheetPage {
INTERVAL, POWER, TEMPERATURE_HISTORY, WRITE
}
val Boolean.localizedName: String
get() {
return if(this){
"Включено"
} else {
"Выключено"
}
}
@Composable
fun ThermometerScreen(
ble: Ble.Thermometer,
onNavigationEvent: (ThermometerContract.Effect.Navigation) -> Unit
) {
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val viewModel = hiltViewModel<ThermometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.INTERVAL -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is ThermometerContract.State.Display) {
IntervalEdit(
state = currentState.thermometer,
onEvent = viewModel::setEvent
)
}
}
SheetPage.POWER -> bottomDialog.show {
val currentState = viewModel.viewState.value
if(currentState is ThermometerContract.State.Display) {
TxLevelSelector(
tx = currentState.thermometer.state.tx
) {
viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it))
}
}
}
SheetPage.TEMPERATURE_HISTORY -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is ThermometerContract.State.Display) {
TemperatureHistory(
ble = currentState.thermometer.info
)
}
}
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is ThermometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = viewModel::setEvent
)
}
}
}
else -> {
bottomDialog.hide()
}
}
}
LaunchedEffect("effect"){
viewModel.effect.onEach {
when(it){
is ThermometerContract.Effect.Navigation -> {
sheetPage = null
onNavigationEvent(it)
}
is ThermometerContract.Effect.HideIntervalPicker -> launch {
sheetPage = null
delay(100)
}
is ThermometerContract.Effect.ShowIntervalPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.INTERVAL
}
is ThermometerContract.Effect.HidePowerPicker -> launch {
sheetPage = null
delay(100)
}
is ThermometerContract.Effect.ShowPowerPicker -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.POWER
}
is ThermometerContract.Effect.HideTemperatureHistory -> launch {
sheetPage = null
delay(100)
}
is ThermometerContract.Effect.ShowTemperatureHistory -> launch {
sheetPage = null
delay(100)
sheetPage = SheetPage.TEMPERATURE_HISTORY
}
is ThermometerContract.Effect.HideWriteBle -> {
sheetPage = null
delay(100)
}
is ThermometerContract.Effect.ShowWriteBle -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
}
}.launchIn(this)
}
LaunchedEffect(ble){
viewModel.setEvent(ThermometerContract.Event.OnBleChanged(ble))
}
Column {
when(state){
is ThermometerContract.State.Display -> {
DisplayState(
origin = state.origin,
ble = state.thermometer,
onEvent = {
viewModel.setEvent(it)
}
)
}
is ThermometerContract.State.Loading -> LoadingState()
}
}
}

View File

@ -0,0 +1,39 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.history
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.model.Ble
class ThermometerHistoryContract {
sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
data object OnRefresh : Event()
}
sealed class State : ViewState {
data class Display(
val loadingHistoryState : ProgressState<List<Ble.Thermometer.HistoryPoint>>
) : State()
data object Exception : State()
}
sealed class Effect : ViewSideEffect {
sealed class Navigation : Effect() {
data object Up : Navigation()
}
}
}

View File

@ -0,0 +1,264 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.history
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
import com.patrykandpatrick.vico.compose.chart.Chart
import com.patrykandpatrick.vico.compose.chart.line.lineChart
import com.patrykandpatrick.vico.compose.chart.scroll.rememberChartScrollState
import com.patrykandpatrick.vico.core.axis.AxisPosition
import com.patrykandpatrick.vico.core.axis.formatter.AxisValueFormatter
import com.patrykandpatrick.vico.core.entry.ChartEntry
import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import llc.arma.ble.domain.common.ProgressState
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class TemperatureEntry(
val localDate: Long,
override val x: Float,
override val y: Float,
) : ChartEntry {
override fun withY(y: Float) = TemperatureEntry(localDate, x, y)
}
val formatter = SimpleDateFormat("dd.MM HH:mm", Locale.getDefault())
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThermometerHistoryScreen(
bleSerial: String,
navigator: DestinationsNavigator
) {
val viewModel = hiltViewModel<ThermometerHistoryViewModel>()
val state = viewModel.viewState.value
LaunchedEffect(Unit) {
viewModel.effect.collect {
when(it){
ThermometerHistoryContract.Effect.Navigation.Up ->
navigator.popBackStack()
}
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(ThermometerHistoryContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
val title = when(state){
is ThermometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) {
is ProgressState.Finished -> "История (${state.loadingHistoryState.data.size})"
is ProgressState.Indeterminate -> "История"
is ProgressState.Progress -> "История"
}
}
ThermometerHistoryContract.State.Exception -> "История"
}
Text(
modifier = Modifier,
text = title,
style = MaterialTheme.typography.titleLarge
)
},
actions = {
IconButton(
onClick = {
viewModel.setEvent(ThermometerHistoryContract.Event.OnRefresh)
},
enabled = when(state){
is ThermometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
ThermometerHistoryContract.State.Exception -> true
}
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when (state) {
is ThermometerHistoryContract.State.Display -> DisplayState(state = state)
ThermometerHistoryContract.State.Exception -> ExceptionState()
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun DisplayState(
state: ThermometerHistoryContract.State.Display
) {
Box(modifier = Modifier
.padding(8.dp)
.fillMaxSize()
) {
when (state.loadingHistoryState) {
is ProgressState.Finished -> {
if(state.loadingHistoryState.data.isEmpty()){
Text(
modifier = Modifier.align(Alignment.Center),
text = "Нет данных"
)
} else {
val producer = remember(state.loadingHistoryState.data) {
state.loadingHistoryState.data.mapIndexed { index, measurePoint ->
TemperatureEntry(measurePoint.date, index.toFloat(), measurePoint.value)
}.let {
ChartEntryModelProducer(it)
}
}
val axisValueFormatter =
AxisValueFormatter<AxisPosition.Horizontal.Bottom> { value, chartValues ->
(chartValues.chartEntryModel.entries.firstOrNull()
?.getOrNull(value.toInt()) as? TemperatureEntry)
?.localDate
?.let { formatter.format(Date(it)) }
.orEmpty()
}
val lineChart = lineChart()
val scrollState = rememberChartScrollState()
Chart(
chartScrollState = scrollState,
chart = lineChart,
chartModelProducer = producer,
startAxis = startAxis(),
bottomAxis = bottomAxis(
tickLength = 0.dp,
valueFormatter = axisValueFormatter,
labelRotationDegrees = -90f,
),
modifier = Modifier.fillMaxSize(),
)
LaunchedEffect(scrollState.maxValue) {
scrollState.scrollBy(scrollState.maxValue)
}
}
}
is ProgressState.Indeterminate -> {
ContainedLoadingIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
is ProgressState.Progress -> {
val progressAnimDuration = 1500
val progressAnimation by animateFloatAsState(
targetValue = state.loadingHistoryState.value,
animationSpec = tween(
durationMillis = progressAnimDuration,
easing = FastOutSlowInEasing
), label = ""
)
LoadingIndicator(
progress = { progressAnimation },
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
private fun ExceptionState() {
Box(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.aspectRatio(2f),
){
Text(
textAlign = TextAlign.Center,
text = "Во время загрузки произошла ошибка",
modifier = Modifier.align(Alignment.Center)
)
}
}

View File

@ -0,0 +1,83 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.history
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.common.ProgressState
import llc.arma.ble.domain.usecase.GetTemperatureHistoryBySerial
import javax.inject.Inject
@HiltViewModel
class ThermometerHistoryViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val getTemperatureHistoryBySerial: GetTemperatureHistoryBySerial
) : BaseViewModel<ThermometerHistoryContract.State, ThermometerHistoryContract.Event, ThermometerHistoryContract.Effect>() {
init {
viewModelScope.launch { refresh() }
}
override fun setInitialState() = ThermometerHistoryContract.State.Display(
ProgressState.Indeterminate
)
override fun handleEvents(event: ThermometerHistoryContract.Event) {
when(event){
is ThermometerHistoryContract.Event.OnRefresh -> reduce(viewState.value, event)
is ThermometerHistoryContract.Event.OnNavigateUp -> reduce(viewState.value, event)
}
}
private fun reduce(
state: ThermometerHistoryContract.State,
event: ThermometerHistoryContract.Event.OnRefresh
) {
viewModelScope.launch { refresh() }
}
private fun reduce(
state: ThermometerHistoryContract.State,
event: ThermometerHistoryContract.Event.OnNavigateUp
) {
setEffect {
ThermometerHistoryContract.Effect.Navigation.Up
}
}
private suspend fun refresh() {
val params = ThermometerHistoryScreenDestination.argsFrom(savedStateHandle)
setState {
ThermometerHistoryContract.State.Display(ProgressState.Indeterminate)
}
getTemperatureHistoryBySerial(params.bleSerial).collect {
it.fold(
onSuccess = {
setState {
ThermometerHistoryContract.State.Display(it)
}
},
onFailure = {
setState {
ThermometerHistoryContract.State.Exception
}
}
)
}
}
}

View File

@ -0,0 +1,196 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
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.shadow
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.domain.model.Ble
@Composable
fun DisplayState(
viewModel: ThermometerViewModel,
state: ThermometerContract.State.Display
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp)
) {
BleInfoView(
bleInfo = state.origin.info,
version = state.origin.state.version
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
content = {
BleMenuItem(
shapeType = ShapeType.Start,
title = "Мощность",
subtitle = "${state.thermometer.state.tx.value} db",
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnTxSelect)
},
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
)
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Температура",
subtitle = "${state.thermometer.thermometerState.temperature.value} °C",
onClick = {
//onEvent(ThermometerContract.Event.OnPowerEdit)
}
)
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Сохранять измерения",
onClick = {
//onEvent(ThermometerContract.Event.OnPowerEdit)
},
icon = {
Switch(
checked = state.thermometer.thermometerState.saveHistory,
onCheckedChange = {
viewModel.setEvent(ThermometerContract.Event.OnSaveHistoryChanged(it))
}
)
}
)
val hours = state.thermometer.thermometerState.historyInterval / 1000 / 60 / 60
val minutes =
(state.thermometer.thermometerState.historyInterval - (hours * 1000 * 60 * 60)) / 1000 / 60
BleMenuItem(
shapeType = ShapeType.Middle,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин.",
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalEdit)
},
icon = {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
)
BleMenuItem(
shapeType = ShapeType.Middle,
title = "График измерений",
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnShowTemperatureHistory)
}
)
BleMenuItem(
shapeType = ShapeType.End,
title = "Изменить пароль",
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnChangePassword)
}
)
}
)
Button(
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnShowWriteBlePreview)
},
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Text(
text = "Сохранить"
)
}
}
}
@Composable
fun BleMenuItem(
shapeType: ShapeType,
title: String,
subtitle: String? = null,
icon: (@Composable () -> Unit)? = null,
onClick: (() -> Unit) = {}
){
Surface(
onClick = onClick,
shape = shapeType.shape,
color = MaterialTheme.colorScheme.surfaceContainer
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(
vertical = 12.dp,
horizontal = 16.dp
)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = title
)
subtitle?.let {
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = it
)
}
}
icon?.invoke()
}
}
}

View File

@ -0,0 +1,25 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun LoadingState(){
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize()
) {
ContainedLoadingIndicator()
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import llc.arma.ble.app.ui.common.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect
@ -10,45 +10,39 @@ class ThermometerContract {
sealed class Event : ViewEvent {
object OnWriteBle : Event()
data object OnTxSelect : Event()
object OnHideWriteBlePreview : Event()
data object OnWriteBle : Event()
object OnShowWriteBlePreview : Event()
data object OnHideWriteBlePreview : Event()
object OnShowTemperatureHistory : Event()
data object OnShowWriteBlePreview : Event()
object OnHideTemperatureHistory : Event()
data object OnShowTemperatureHistory : Event()
object OnChangePassword : Event()
data object OnChangePassword : Event()
data class OnSaveHistoryChanged(
val saveHistory: Boolean
) : Event()
object OnPowerEdit : Event()
data class OnPowerChanged(
val tx: BleView.BleState.TX
) : Event()
object OnSaveIntervalEdit : Event()
data object OnSaveIntervalEdit : Event()
data class OnSaveIntervalChanged(
val interval: Long
) : Event()
data class OnBleChanged(
val ble: Ble.Thermometer
) : Event()
object OnNavigateUpClicked : Event()
data object OnNavigateUp : Event()
}
sealed class State : ViewState {
object Loading : State()
data object Loading : State()
data class Display(
val origin: Ble.Thermometer,
@ -66,9 +60,9 @@ class ThermometerContract {
val writeRequest: Ble.Thermometer.WriteRequest
) : WriteState()
object Success : WriteState()
data object Success : WriteState()
object Failure : WriteState()
data object Failure : WriteState()
}
@ -78,27 +72,29 @@ class ThermometerContract {
sealed class Effect : ViewSideEffect {
object ShowTemperatureHistory : Effect()
object HideTemperatureHistory : Effect()
object ShowIntervalPicker : Effect()
object HideIntervalPicker : Effect()
object ShowPowerPicker : Effect()
object HidePowerPicker : Effect()
object ShowWriteBle : Effect()
object HideWriteBle : Effect()
sealed class Navigation : Effect() {
object NavigateUp : Navigation()
data object Up : Navigation()
object NavigateToChangePassword : Navigation()
data class DurationSelector(
val duration: Int
) : Navigation()
data class TxSelector(
val tx: BleView.BleState.TX?
) : Navigation()
data class ChangePassword(
val bleSerial: String
) : Navigation()
data class ThermometerHistory(
val bleSerial: String
) : Navigation()
}

View File

@ -0,0 +1,182 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.ChangePasswordScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DurationSelectorScreenDestination
import com.ramcosta.composedestinations.generated.destinations.ThermometerHistoryScreenDestination
import com.ramcosta.composedestinations.generated.destinations.TxPowerSelectorScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import com.ramcosta.composedestinations.result.onResult
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.rememberBottomDialogState
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.selector.duration.DurationSelectResult
import llc.arma.ble.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.BleInfo
import kotlin.time.DurationUnit
import kotlin.time.toDuration
enum class SheetPage {
WRITE
}
val Boolean.localizedName: String
get() {
return if(this){
"Включено"
} else {
"Выключено"
}
}
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThermometerScreen(
bleSerial: String,
txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
durationSelectResult: ResultRecipient<DurationSelectorScreenDestination, DurationSelectResult>,
navigator: DestinationsNavigator
) {
var sheetPage by rememberSaveable {
mutableStateOf<SheetPage?>(null)
}
val viewModel = hiltViewModel<ThermometerViewModel>()
val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
txSelectResult.onResult {
viewModel.setEvent(ThermometerContract.Event.OnPowerChanged(it))
}
durationSelectResult.onResult {
viewModel.setEvent(ThermometerContract.Event.OnSaveIntervalChanged(it.duration.toLong()))
}
LaunchedEffect(sheetPage){
when(sheetPage){
SheetPage.WRITE -> bottomDialog.show {
val currentState = viewModel.viewState.value
if (currentState is ThermometerContract.State.Display) {
currentState.writeState?.let {
Write(
state = it,
onEvent = viewModel::setEvent
)
}
}
}
else -> {
bottomDialog.hide()
}
}
}
LaunchedEffect(Unit){
viewModel.effect.onEach {
when(it){
is ThermometerContract.Effect.HideWriteBle -> {
sheetPage = null
delay(100)
}
is ThermometerContract.Effect.ShowWriteBle -> {
sheetPage = null
delay(100)
sheetPage = SheetPage.WRITE
}
is ThermometerContract.Effect.Navigation.ChangePassword ->
navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
is ThermometerContract.Effect.Navigation.ThermometerHistory ->
navigator.navigate(ThermometerHistoryScreenDestination(it.bleSerial))
is ThermometerContract.Effect.Navigation.TxSelector ->
navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
is ThermometerContract.Effect.Navigation.DurationSelector ->
navigator.navigate(DurationSelectorScreenDestination(
duration = it.duration,
minimum = 60 * 1000,
maximum = 240 * 60 * 60 * 1000,
minutesComponent = true,
secondsComponent = false
))
ThermometerContract.Effect.Navigation.Up ->
navigator.popBackStack()
}
}.launchIn(this)
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
IconButton(
onClick = {
viewModel.setEvent(ThermometerContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
title = {
Text(text = BleInfo.Type.THERMOMETER.localized)
}
)
}
) {
Column(
modifier = Modifier.padding(it)
) {
when(state){
is ThermometerContract.State.Display -> DisplayState(viewModel, state)
is ThermometerContract.State.Loading -> LoadingState()
}
}
}
}

View File

@ -1,51 +1,108 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.beacon.BeaconContract
import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject
@HiltViewModel
class ThermometerViewModel @Inject constructor(
private val getBleBySerial: GetBleBySerial,
private val savedStateHandle: SavedStateHandle,
private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle
) : BaseViewModel<ThermometerContract.State, ThermometerContract.Event, ThermometerContract.Effect>() {
init {
val params = ThermometerScreenDestination.argsFrom(savedStateHandle)
viewModelScope.launch {
val ble = getBleBySerial.invoke(params.bleSerial, this).fold(
onSuccess = { it },
onFailure = { null }
)
if(ble != null && ble is Ble.Thermometer){
setState {
when(this){
is ThermometerContract.State.Display -> {
copy(
origin = Ble.Thermometer(
info = ble.info,
state = origin.state,
thermometerState = origin.thermometerState
)
)
}
ThermometerContract.State.Loading -> {
ThermometerContract.State.Display(
origin = ble,
thermometer = bleMapper.map(ble) as BleView.Thermometer,
writeState = null
)
}
}
}
}
}
}
override fun setInitialState() = ThermometerContract.State.Loading
override fun handleEvents(event: ThermometerContract.Event) {
when(event){
is ThermometerContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event)
is ThermometerContract.Event.OnBleChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
is ThermometerContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnPowerEdit -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveHistoryChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is ThermometerContract.Event.OnTxSelect -> reduce(viewState.value, event)
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnNavigateUpClicked
event: ThermometerContract.Event.OnTxSelect
) {
setEffect { ThermometerContract.Effect.Navigation.NavigateUp }
if(state is ThermometerContract.State.Display){
setEffect { ThermometerContract.Effect.Navigation.TxSelector(state.thermometer.state.tx) }
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnNavigateUp
) {
setEffect { ThermometerContract.Effect.Navigation.Up }
}
/*private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnBleChanged
) {
@ -69,14 +126,16 @@ class ThermometerViewModel @Inject constructor(
}
}
}
}*/
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalEdit
) {
if(state is ThermometerContract.State.Display) {
setEffect {
ThermometerContract.Effect.ShowIntervalPicker
ThermometerContract.Effect.Navigation.DurationSelector(state.thermometer.thermometerState.historyInterval.toInt())
}
}
}
@ -84,41 +143,26 @@ class ThermometerViewModel @Inject constructor(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalChanged
) {
if(state is ThermometerContract.State.Display) {
state.thermometer.thermometerState.historyInterval = event.interval
}
setEffect {
ThermometerContract.Effect.HideIntervalPicker
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnPowerEdit
) {
setEffect {
ThermometerContract.Effect.ShowPowerPicker
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnPowerChanged
) {
if(state is ThermometerContract.State.Display) {
state.thermometer.state.tx = event.tx
}
setEffect {
ThermometerContract.Effect.HidePowerPicker
}
}
private fun reduce(
@ -138,19 +182,14 @@ class ThermometerViewModel @Inject constructor(
event: ThermometerContract.Event.OnShowTemperatureHistory
) {
setEffect {
ThermometerContract.Effect.ShowTemperatureHistory
}
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnHideTemperatureHistory
) {
if(state is ThermometerContract.State.Display) {
setEffect {
ThermometerContract.Effect.HideTemperatureHistory
ThermometerContract.Effect.Navigation.ThermometerHistory(
state.origin.info.serial
)
}
}
}
@ -286,7 +325,7 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display){
setEffect {
ThermometerContract.Effect.Navigation.NavigateToChangePassword
ThermometerContract.Effect.Navigation.ChangePassword(state.thermometer.info.serial)
}
}

View File

@ -1,4 +1,4 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.view
package llc.arma.ble.app.ui.screen.inspection.thermometer.main
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.Image
@ -21,9 +21,7 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.app.ui.screen.inspection.thermometer.localizedName
import llc.arma.ble.app.ui.screen.ShapeType
import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable
@ -52,6 +50,7 @@ fun Write(
state.writeRequest.tx?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Мощность",
subtitle = "${it.localizedName} db"
)
@ -61,6 +60,7 @@ fun Write(
state.writeRequest.saveHistory?.let {
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений",
subtitle = it.localizedName
)
@ -73,6 +73,7 @@ fun Write(
val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60
BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин."
)

View File

@ -1,313 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.view
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowRight
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
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.draw.shadow
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.BleInfoView
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
import llc.arma.ble.domain.model.Ble
@Composable
fun DisplayState(
onEvent: (ThermometerContract.Event) -> Unit,
origin: Ble.Thermometer,
ble: BleView.Thermometer
) {
val scrollState = rememberScrollState()
Column {
Column(
modifier = Modifier
.verticalScroll(scrollState)
.weight(1f)
) {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
BleInfoView(
bleInfo = origin.info,
version = origin.state.version
)
}
Column(
modifier = Modifier,
content = {
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(ThermometerContract.Event.OnPowerEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Мощность"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${ble.state.tx.value} db"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Температура"
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "${ble.thermometerState.temperature.value} °C"
)
}
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable { }
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Сохранять историю измерений"
)
}
Switch(
checked = ble.thermometerState.saveHistory,
onCheckedChange = {
onEvent(ThermometerContract.Event.OnSaveHistoryChanged(it))
}
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(ThermometerContract.Event.OnSaveIntervalEdit)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Интервал измерений"
)
val hours = ble.thermometerState.historyInterval / 1000 / 60 / 60
val minutes = (ble.thermometerState.historyInterval - ( hours * 1000 * 60 * 60 )) / 1000 / 60
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = "$hours ч. $minutes мин."
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(ThermometerContract.Event.OnShowTemperatureHistory)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "График измерений"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
Box(
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 8.dp
)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable {
onEvent(ThermometerContract.Event.OnChangePassword)
}
.padding(8.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Изменить пароль"
)
}
Icon(
imageVector = Icons.Rounded.KeyboardArrowRight,
contentDescription = null
)
}
}
}
)
}
PrimaryButton(
modifier = Modifier.shadow(
if(scrollState.canScrollForward){
8.dp
} else {
0.dp
}
).background(MaterialTheme.colorScheme.background),
label = "Сохранить"
) {
onEvent(ThermometerContract.Event.OnShowWriteBlePreview)
}
}
}

View File

@ -1,200 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.view
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp
import androidx.compose.material3.FilledIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView
import llc.arma.ble.app.ui.screen.inspection.thermometer.ThermometerContract
@Composable
fun IntervalEdit(
state: BleView.Thermometer,
onEvent: (ThermometerContract.Event) -> Unit,
){
var value by remember(state.thermometerState.historyInterval) {
mutableIntStateOf((state.thermometerState.historyInterval / 1000 / 60 / 60).toInt())
}
val maxInterval = 240
if(value > maxInterval){
value = maxInterval
}
if(value < 1){
value = 1
}
val maxHours = maxInterval
val maxDays = maxInterval / 24
val dayValue = value / 24
val hourValue = value - (24 * dayValue)
Column(
modifier = Modifier
) {
Text(
modifier = Modifier.padding(horizontal = 12.dp),
text = "Интервал измерений",
style = MaterialTheme.typography.titleLarge
)
Spacer(modifier = Modifier.height(16.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
NumberPicker(
range = -1..maxDays,
value = dayValue,
onValueChanged = { value = (it * 24) + hourValue }
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Дни")
Spacer(modifier = Modifier.width(16.dp))
NumberPicker(
range = -1..maxHours,
value = hourValue,
onValueChanged = {
value = it + (dayValue * 24)
}
)
Spacer(modifier = Modifier.width(8.dp))
Text(text = "Часы")
}
Spacer(modifier = Modifier.height(16.dp))
PrimaryButton(
label = "Применить"
) {
onEvent(
ThermometerContract.Event.OnSaveIntervalChanged(
value.toLong() * 1000 * 60 * 60
)
)
}
}
}
@Composable
fun NumberPicker(
modifier: Modifier = Modifier,
range: IntRange,
value: Int,
onValueChanged: (Int) -> Unit
) {
LaunchedEffect(range){
if(value > range.last){
onValueChanged(range.last)
}
if(value < range.first){
onValueChanged(range.first)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
){
FilledIconButton(
onClick = {
if(value < range.last) onValueChanged(value + 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowUp,
contentDescription = null
)
}
Spacer(modifier = Modifier.height(36.dp))
AnimatedContent(
targetState = value,
transitionSpec = {
if (targetState > initialState) {
(slideInVertically { height -> height } + fadeIn()).togetherWith(
slideOutVertically { height -> -height } + fadeOut())
} else {
(slideInVertically { height -> -height } + fadeIn()).togetherWith(
slideOutVertically { height -> height } + fadeOut())
}.using(
SizeTransform(clip = false)
)
}
) { targetCount ->
Text(
style = MaterialTheme.typography.displaySmall,
text = "$targetCount"
)
}
Spacer(modifier = Modifier.height(36.dp))
FilledIconButton(
onClick = {
if(value > range.first) onValueChanged(value - 1)
}
) {
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null
)
}
}
}

View File

@ -1,19 +0,0 @@
package llc.arma.ble.app.ui.screen.inspection.thermometer.view
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@Composable
fun LoadingState(){
Box(modifier = Modifier.fillMaxSize()) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}

Some files were not shown because too many files have changed in this diff Show More