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> </DropdownSelection>
<DialogSelection /> <DialogSelection />
</SelectionState> </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> </selectionStates>
</component> </component>
</project> </project>

View File

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

View File

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

View File

@ -1,6 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <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"> <component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.22" /> <option name="version" value="2.1.20" />
</component> </component>
</project> </project>

View File

@ -1,4 +1,10 @@
<project version="4"> <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="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK"> <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" /> <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. # Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the # 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 # For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html # http://developer.android.com/guide/developing/tools/proguard.html

View File

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

View File

@ -15,6 +15,7 @@ import android.view.SurfaceView
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer 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.height
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
@ -33,6 +36,7 @@ import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -72,9 +76,9 @@ class MainActivity : ComponentActivity() {
val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter val mBluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
WindowCompat.setDecorFitsSystemWindows(window, false) enableEdgeToEdge()
installSplashScreen() //installSplashScreen()
setContent { setContent {
@ -103,7 +107,9 @@ class MainActivity : ComponentActivity() {
) )
) { ) {
BoxWithConstraints { BoxWithConstraints(
modifier = Modifier.navigationBarsPadding()
) {
val maxHeight = with(LocalDensity.current) { val maxHeight = with(LocalDensity.current) {
this@BoxWithConstraints.constraints.maxHeight.toDp() this@BoxWithConstraints.constraints.maxHeight.toDp()
@ -168,86 +174,88 @@ class MainActivity : ComponentActivity() {
}, },
content = { content = {
Surface( Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
modifier = Modifier Surface(
.fillMaxSize() modifier = Modifier
.navigationBarsPadding(), .fillMaxSize()
color = MaterialTheme.colorScheme.background .navigationBarsPadding(),
) { color = MaterialTheme.colorScheme.background
) {
val multiplePermissionsState = val multiplePermissionsState =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
)
)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
rememberMultiplePermissionsState( rememberMultiplePermissionsState(
listOf( listOf(
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_MEDIA_VIDEO,
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT Manifest.permission.BLUETOOTH_CONNECT
) )
) )
} else { } else {
rememberMultiplePermissionsState( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
listOf( rememberMultiplePermissionsState(
Manifest.permission.WRITE_EXTERNAL_STORAGE, listOf(
Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.ACCESS_COARSE_LOCATION Manifest.permission.BLUETOOTH_CONNECT
)
) )
) } else {
rememberMultiplePermissionsState(
listOf(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
} }
var bleEnabled by remember {
mutableStateOf(mBluetoothAdapter.isEnabled)
} }
var bleEnabled by remember { val lifecycleOwner = LocalLifecycleOwner.current
mutableStateOf(mBluetoothAdapter.isEnabled) val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
}
val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(lifecycleState) {
val lifecycleState by lifecycleOwner.lifecycle.currentStateFlow.collectAsState() bleEnabled = mBluetoothAdapter.isEnabled
}
LaunchedEffect(lifecycleState) { if (multiplePermissionsState.allPermissionsGranted) {
bleEnabled = mBluetoothAdapter.isEnabled
}
if (multiplePermissionsState.allPermissionsGranted) { if (bleEnabled) {
if (bleEnabled) { MainScreen()
MainScreen() } else {
val context = LocalContext.current
LaunchedEffect(mBluetoothAdapter.isEnabled) {
val intent =
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
(context as Activity).startActivityForResult(
intent,
1
)
}
}
} else { } else {
val context = LocalContext.current LaunchedEffect(multiplePermissionsState) {
multiplePermissionsState.launchMultiplePermissionRequest()
LaunchedEffect(mBluetoothAdapter.isEnabled) {
val intent =
Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
(context as Activity).startActivityForResult(
intent,
1
)
} }
}
} else {
LaunchedEffect(multiplePermissionsState) {
multiplePermissionsState.launchMultiplePermissionRequest()
} }
} }
} }
} }

View File

@ -17,19 +17,19 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun SignalLevel( fun SignalLevel(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
maxLevel: Int = 5, maxLevel: Int = 4,
level: Int level: Int
){ ){
val step = (16 - 4) / 4 val step = (16 - 4) / maxLevel
Row( Row(
modifier = modifier.height(16.dp), modifier = modifier.height(12.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp), horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.Bottom verticalAlignment = Alignment.Bottom
) { ) {
for(col in 0..4 step 1){ for(col in 0..<maxLevel step 1){
Surface( Surface(
color = LocalContentColor.current.copy( color = LocalContentColor.current.copy(
alpha = if(col <= level + 1) ContentAlpha.high else ContentAlpha.disabled 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 -> { is Ble.Gate -> {
BleView.Host( BleView.Gate(
info = input.info, info = input.info,
state = BleView.BleState( state = BleView.BleState(
tx = txMapper.map(input.state.tx), tx = txMapper.map(input.state.tx),
version = input.state.version version = input.state.version
), ),
hostState = BleView.Host.HostState( hostState = BleView.Gate.HostState(
historyInterval = input.hostState.historyInterval, historyInterval = input.gateState.historyInterval,
readInterval = input.hostState.readInterval readInterval = input.gateState.readInterval
) )
) )
} }

View File

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

View File

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

View File

@ -1,5 +1,6 @@
package llc.arma.ble.app.ui.screen package llc.arma.ble.app.ui.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -21,6 +22,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -35,71 +37,57 @@ fun BleInfoView(
version: BleRepositoryImpl.Version version: BleRepositoryImpl.Version
) { ) {
Surface( Column(
modifier = Modifier.padding(bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(2.dp)
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceVariant
) { ) {
Column( BleInfoItem(
modifier = Modifier.padding(8.dp) shapeType = ShapeType.Start,
) { icon = {
Icon(
Column { imageVector = bleInfo.type.icon,
contentDescription = null
BleInfoItem(
icon = {
Icon(
imageVector = bleInfo.type.icon,
contentDescription = null
)
},
title = bleInfo.name,
subtitle = "${bleInfo.type.localized} v${version}"
) )
},
title = bleInfo.name,
subtitle = "${bleInfo.type.localized} v${version}"
)
SpecDivider() BleInfoItem(
shapeType = ShapeType.Middle,
BleInfoItem( icon = {
icon = { Icon(
Icon( imageVector = Icons.Rounded.Key,
imageVector = Icons.Rounded.Key, contentDescription = null
contentDescription = null
)
},
title = "Адрес",
subtitle = bleInfo.serial
) )
},
title = "Адрес",
subtitle = bleInfo.serial
)
SpecDivider() BleInfoItem(
shapeType = ShapeType.Middle,
BleInfoItem( icon = {
icon = { Icon(
Icon( imageVector = Icons.Rounded.BatteryFull,
imageVector = Icons.Rounded.BatteryFull, contentDescription = null
contentDescription = null
)
},
title = "Заряд батареи",
subtitle = "${bleInfo.batteryLevel} %"
) )
},
title = "Заряд батареи",
subtitle = "${bleInfo.batteryLevel} %"
)
SpecDivider() BleInfoItem(
shapeType = ShapeType.End,
BleInfoItem( icon = {
icon = { Icon(
Icon( imageVector = Icons.Rounded.NetworkCell,
imageVector = Icons.Rounded.NetworkCell, contentDescription = null
contentDescription = null
)
},
title = "Мощность сигнала",
subtitle = if(bleInfo.rssi != null) "${bleInfo.rssi } dBm" else "Нет сигнала"
) )
},
} title = "Мощность сигнала",
subtitle = if (bleInfo.rssi != null) "${bleInfo.rssi} dBm" else "Нет сигнала"
} )
} }
@ -116,48 +104,88 @@ 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 @Composable
private fun BleInfoItem( private fun BleInfoItem(
shapeType: ShapeType,
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
title: String, title: String,
subtitle: String subtitle: String
){ ){
Row( Surface(
modifier = Modifier.padding(8.dp), shape = shapeType.shape,
verticalAlignment = Alignment.CenterVertically color = MaterialTheme.colorScheme.surfaceContainer
) { ) {
Surface( Row(
modifier = Modifier.size(40.dp), modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp),
shape = CircleShape verticalAlignment = Alignment.CenterVertically
) { ) {
Box( Surface(
modifier = Modifier.fillMaxSize(), shape = CircleShape,
contentAlignment = Alignment.Center color = MaterialTheme.colorScheme.surfaceContainerHighest,
){ modifier = Modifier.size(36.dp),
) {
icon() Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
icon()
}
} }
} Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(12.dp)) Column(
modifier = Modifier.weight(1f)
) {
Column( Text(
modifier = Modifier.weight(1f) text = title
) { )
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = subtitle
)
Text( }
text = title
)
Text(
color = MaterialTheme.colorScheme.secondary,
style = MaterialTheme.typography.bodyMedium,
text = subtitle
)
} }

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.app.ui.common.ViewState
import llc.arma.ble.domain.model.BleFilter import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo
class BleListContract { class BleListContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnResetFilter : Event() data object OnResetScanner : Event()
data object OnHideFilter : Event()
data object OnShowFilter : Event() data object OnShowFilter : Event()
@ -21,73 +18,32 @@ class BleListContract {
val bleAddress: String val bleAddress: String
) : Event() ) : 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( data class State(
val connectedBleList: List<ConnectedBleInfo>,
val bleList: List<BleInfo>, val bleList: List<BleInfo>,
val bleFilter: BleFilter val bleFilter: BleFilter
) : ViewState { ) : 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
}
}*/
}
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
object ShowFilter : Effect()
object HideFilter : Effect()
sealed class Navigation : 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 val serial: String
) : Navigation() ) : Navigation()

View File

@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.ContentAlpha
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowRightAlt 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.BatteryFull
import androidx.compose.material.icons.rounded.CompareArrows import androidx.compose.material.icons.rounded.CompareArrows
import androidx.compose.material.icons.rounded.FilterAlt 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.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -47,201 +50,222 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel 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.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.SignalLevel import llc.arma.ble.app.ui.common.SignalLevel
import llc.arma.ble.app.ui.common.rememberBottomDialogState 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.app.ui.screen.locale.icon
import llc.arma.ble.domain.model.BleFilter import llc.arma.ble.domain.model.BleFilter
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.ConnectedBleInfo import llc.arma.ble.domain.model.ConnectedBleInfo
import kotlin.math.pow import kotlin.math.pow
@Destination<RootGraph>(start = true)
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BleListScreen( fun BleListScreen(
onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit //onNavigationEvent: (BleListContract.Effect.Navigation) -> Unit
navigator: DestinationsNavigator
) { ) {
val viewModel = hiltViewModel<BleListViewModel>() val viewModel = hiltViewModel<BleListViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
val bottomDialog = rememberBottomDialogState()
val scrollState = rememberLazyListState() 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"){ LaunchedEffect("effect"){
viewModel.effect.onEach { viewModel.effect.onEach {
when(it){ when(it){
is BleListContract.Effect.Navigation -> onNavigationEvent(it) is BleListContract.Effect.Navigation.Accelerometer ->
is BleListContract.Effect.HideFilter -> launch { navigator.navigate(AccelerometerScreenDestination(it.serial))
bottomDialog.hide()
} is BleListContract.Effect.Navigation.Beacon ->
is BleListContract.Effect.ShowFilter -> launch { navigator.navigate(BeaconScreenDestination(it.serial))
bottomDialog.show {
Filter( BleListContract.Effect.Navigation.BleFilter ->
filter = viewModel.viewState.value.bleFilter, navigator.navigate(BleFilterScreenDestination)
onEvent = {
viewModel.setEvent(it) is BleListContract.Effect.Navigation.Gate ->
} navigator.navigate(GateScreenDestination(it.serial))
)
} is BleListContract.Effect.Navigation.Thermometer ->
} navigator.navigate(ThermometerScreenDestination(it.serial))
} }
}.launchIn(this) }.launchIn(this)
} }
Column { Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Arma BLE")
},
actions = {
TopAppBar( Row(
modifier = Modifier modifier = Modifier
.zIndex(1f) .padding(horizontal = 8.dp)
.shadow(if (scrollState.canScrollBackward) 8.dp else 0.dp), .align(Alignment.CenterVertically)
title = { ) {
Text(text = "Arma BLE")
},
actions = {
Row( Text(text = "${state.bleList.size}")
modifier = Modifier
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
) {
Text(text = "${state.bleList.size}") Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(12.dp))
Text(text = "${state.bleList.filter {
it.batteryLevel == 100
}.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}")
Text(text = " | ") Text(text = "${state.bleList.filter {
it.batteryLevel == 100
}.filterNot { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}")
Text( Text(text = " | ")
text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
Text(text = " | ") Text(
text = "${state.bleList.filter { SystemClock.elapsedRealtime() - it.scanTime > 10_000 }.size}",
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
Text( Text(text = " | ")
text = "${state.bleList.filter { it.batteryLevel < 100 }.size}",
color = MaterialTheme.colorScheme.error
)
} Text(
text = "${state.bleList.filter { it.batteryLevel < 100 }.size}",
color = MaterialTheme.colorScheme.error
)
}
IconButton(
onClick = {
viewModel.setEvent(BleListContract.Event.OnShowFilter)
}
) {
Icon(
imageVector = Icons.Rounded.FilterAlt,
contentDescription = null
)
IconButton(
onClick = {
viewModel.setEvent(BleListContract.Event.OnShowFilter)
}
) {
Icon(
imageVector = Icons.Rounded.FilterAlt,
contentDescription = null
)
}
}
)
val filteredData = remember(state.bleList, state.bleFilter) {
state.bleList.filter {
(it.type == state.bleFilter.bleType || state.bleFilter.bleType == null) &&
it.name.contains(state.bleFilter.name) &&
it.serial.contains(state.bleFilter.mac) &&
state.bleFilter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
state.bleFilter.battery.contains(it.batteryLevel.toFloat())
}.let {
when (state.bleFilter.sortField) {
BleFilter.Field.Name -> it.sortedBy { it.name }
BleFilter.Field.Mac -> it.sortedBy { it.serial }
BleFilter.Field.Distance -> it.sortedBy {
10.0.pow(
(it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
).toFloat()
} }
BleFilter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
BleFilter.Field.Battery -> it.sortedBy { it.batteryLevel }
} }
}.let {
when (state.bleFilter.sortOrder) {
BleFilter.Order.Asc -> it
BleFilter.Order.Desc -> it.reversed()
}
}
}
if(filteredData.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.height(3.dp)
) )
} }
) {
if(filteredData.isEmpty()){ Column(
modifier = Modifier.padding(it)
) {
Box(modifier = Modifier.fillMaxSize()){ val filteredData = remember(state.bleList, state.bleFilter) {
Text(
modifier = Modifier.align(Alignment.Center), state.bleList.filter {
style = MaterialTheme.typography.titleMedium, (it.type == state.bleFilter.bleType || state.bleFilter.bleType == null) &&
text = "Метки в области не найдены" it.name.contains(state.bleFilter.name) &&
it.serial.contains(state.bleFilter.mac) &&
state.bleFilter.rssi.contains(it.rssi?.toFloat() ?: Float.MIN_VALUE) &&
state.bleFilter.battery.contains(it.batteryLevel.toFloat())
}.let {
when (state.bleFilter.sortField) {
BleFilter.Field.Name -> it.sortedBy { it.name }
BleFilter.Field.Mac -> it.sortedBy { it.serial }
BleFilter.Field.Distance -> it.sortedBy {
10.0.pow(
(it.tx.toDouble() - (it.rssi?.toDouble() ?: 0.0) - 74) / 20
).toFloat()
}
BleFilter.Field.Dbm -> it.sortedBy { it.rssi ?: 0 }
BleFilter.Field.Battery -> it.sortedBy { it.batteryLevel }
}
}.let {
when (state.bleFilter.sortOrder) {
BleFilter.Order.Asc -> it
BleFilter.Order.Desc -> it.reversed()
}
}
}
if(filteredData.isEmpty()){
LinearProgressIndicator(
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.height(3.dp)
) )
} }
} else { if(filteredData.isEmpty()){
LazyColumn( Box(modifier = Modifier.fillMaxSize()){
state = scrollState, Text(
verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.align(Alignment.Center),
modifier = Modifier.fillMaxSize() style = MaterialTheme.typography.titleMedium,
) { text = "Метки в области не найдены"
)
}
items(items = state.connectedBleList){ } else {
LazyColumn(
state = scrollState,
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
items(
items = filteredData,
key = { it.serial }
) {
BleItem(
ble = it,
shapeType = filteredData.takeShapeType(it),
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
)
ConnectedBleItem(ble = it) {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
} }
} }
items(
items = filteredData,
key = { it.serial }
) {
BleItem(
ble = it,
onClick = {
viewModel.setEvent(BleListContract.Event.OnConnectToBle(it.serial))
}
)
}
} }
} }
} }
} }
@Composable @Composable
@ -251,7 +275,7 @@ fun ItemIcon(
Surface( Surface(
modifier = Modifier.size(40.dp), modifier = Modifier.size(40.dp),
color = MaterialTheme.colorScheme.surfaceVariant, color = MaterialTheme.colorScheme.surfaceContainerHighest,
shape = CircleShape shape = CircleShape
) { ) {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
@ -275,6 +299,7 @@ private fun Int.toSignalLevel(): Int {
@Composable @Composable
fun BleItem( fun BleItem(
shapeType: ShapeType,
ble: BleInfo, ble: BleInfo,
onClick: () -> Unit onClick: () -> Unit
){ ){
@ -282,7 +307,7 @@ fun BleItem(
val color = if(ble.batteryLevel < 100){ val color = if(ble.batteryLevel < 100){
MaterialTheme.colorScheme.errorContainer MaterialTheme.colorScheme.errorContainer
} else { } else {
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.surfaceContainer
} }
val highAlpha = ContentAlpha.high val highAlpha = ContentAlpha.high
@ -313,13 +338,10 @@ fun BleItem(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clip(RoundedCornerShape(16.dp)) .clip(shapeType.shape)
.background(color) .background(color)
.clickable(onClick = onClick) .clickable(onClick = onClick)
.padding( .padding(horizontal = 16.dp, vertical = 12.dp)
vertical = 8.dp,
horizontal = 16.dp
)
.alpha(alpha) .alpha(alpha)
) { ) {
@ -366,64 +388,15 @@ fun BleItem(
Text(text = ble.name) Text(text = ble.name)
Text(
style = MaterialTheme.typography.bodyMedium,
text = ble.serial
)
Row( Row(
verticalAlignment = Alignment.CenterVertically, 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) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( Text(
verticalAlignment = Alignment.CenterVertically, style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.alpha(0.7f) text = ble.serial
) { )
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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -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( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.alpha(0.7f) modifier = Modifier.alpha(0.7f)
@ -508,62 +539,4 @@ 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 kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.domain.model.BleFilter 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.GetBleAroundFlow
import llc.arma.ble.domain.usecase.filter.GetFilterFlow import llc.arma.ble.domain.usecase.filter.GetFilterFlow
import llc.arma.ble.domain.usecase.filter.SaveFilter
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BleListViewModel @Inject constructor( class BleListViewModel @Inject constructor(
private val getFilterFlow: GetFilterFlow, private val getFilterFlow: GetFilterFlow,
private val saveFilter: SaveFilter, private val getBleAroundFlow: GetBleAroundFlow
getBleAroundFlow: GetBleAroundFlow
) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() { ) : BaseViewModel<BleListContract.State, BleListContract.Event, BleListContract.Effect>() {
private var scannerJob: Job? = null
init { init {
viewModelScope.launch { 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 { getFilterFlow.invoke().onEach {
setState { setState {
copy( copy(
@ -49,51 +32,18 @@ class BleListViewModel @Inject constructor(
) )
} }
}.launchIn(this) }.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 = override fun setInitialState(): BleListContract.State =
BleListContract.State(emptyList(), emptyList(), BleFilter()) BleListContract.State(emptyList(), BleFilter())
override fun handleEvents(event: BleListContract.Event) { override fun handleEvents(event: BleListContract.Event) {
when(event){ when(event){
is BleListContract.Event.OnConnectToBle -> reduce(viewState.value, 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.OnShowFilter -> reduce(viewState.value, event)
is BleListContract.Event.OnTypeChanged -> reduce(viewState.value, event) is BleListContract.Event.OnResetScanner -> 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)
} }
} }
@ -101,159 +51,55 @@ class BleListViewModel @Inject constructor(
state: BleListContract.State, state: BleListContract.State,
event: BleListContract.Event.OnConnectToBle 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.bleList.firstOrNull { it.serial == event.bleAddress }?.let { ble ->
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())
setEffect { 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( private fun reduce(
state: BleListContract.State, state: BleListContract.State,
event: BleListContract.Event.OnShowFilter event: BleListContract.Event.OnShowFilter
) { ) {
setEffect { 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.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState 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.beacon.BeaconContract
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.thermometer.ThermometerContract import llc.arma.ble.app.ui.screen.inspection.thermometer.main.ThermometerContract
import llc.arma.ble.domain.common.BleException import llc.arma.ble.domain.common.BleException
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
@ -31,7 +31,7 @@ class ConnectionContract {
) : Event() ) : Event()
data class OnHostNavigationEvent( data class OnHostNavigationEvent(
val event: HostContract.Effect.Navigation val event: GateContract.Effect.Navigation
) : Event() ) : Event()
data class OnThermometerNavigationEvent( data class OnThermometerNavigationEvent(
@ -50,6 +50,7 @@ class ConnectionContract {
data object Loading : State() data object Loading : State()
data class DisplayException( data class DisplayException(
val tries: Long,
val exception: BleException val exception: BleException
) : State() ) : State()
@ -73,6 +74,10 @@ class ConnectionContract {
val serial: String val serial: String
) : Navigation() ) : Navigation()
data class NavigateToThermometerHistory(
val bleSerial: String
) : Navigation()
} }
sealed class InnerNavigation : Effect() { 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.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.SizeTransform import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.animation.with
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height 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.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.ArrowBack import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -35,24 +32,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import llc.arma.ble.app.ui.common.SmallPrimaryButton 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -86,7 +71,7 @@ fun ConnectionScreen(
Column { Column {
CenterAlignedTopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
IconButton( IconButton(
onClick = { onClick = {
@ -127,18 +112,17 @@ fun ConnectionScreen(
} }
) )
when (state) { /*when (state) {
is ConnectionContract.State.DisplayException -> DisplayException( is ConnectionContract.State.DisplayException -> DisplayException(
onEvent = { viewState = state,
viewModel.setEvent(it) onEvent = viewModel::setEvent
}
) )
is ConnectionContract.State.Loading -> LoadingState() is ConnectionContract.State.Loading -> LoadingState()
is ConnectionContract.State.Display -> { is ConnectionContract.State.Display -> {
when (state.ble) { when (state.ble) {
is Ble.Beacon -> BeaconScreen( is Ble.Beacon -> BeaconScreen(
ble = state.ble, null,
onNavigationEvent = { onNavigationEvent = {
viewModel.setEvent( viewModel.setEvent(
ConnectionContract.Event.OnBeaconNavigationEvent( ConnectionContract.Event.OnBeaconNavigationEvent(
@ -151,7 +135,8 @@ fun ConnectionScreen(
is Ble.Thermometer -> { is Ble.Thermometer -> {
ThermometerScreen( ThermometerScreen(
ble = state.ble, txSelectResult = null,
//ble = state.ble,
onNavigationEvent = { onNavigationEvent = {
viewModel.setEvent( viewModel.setEvent(
ConnectionContract.Event.OnThermometerNavigationEvent( ConnectionContract.Event.OnThermometerNavigationEvent(
@ -164,16 +149,17 @@ fun ConnectionScreen(
} }
is Ble.Accelerometer -> { is Ble.Accelerometer -> {
AccelerometerScreen(ble = state.ble) { /*AccelerometerScreen {
viewModel.setEvent( viewModel.setEvent(
ConnectionContract.Event.OnAccelNavigationEvent(it) ConnectionContract.Event.OnAccelNavigationEvent(it)
) )
} }*/
} }
is Ble.Host -> { is Ble.Gate -> {
HostScreen( GateScreen(
ble = state.ble, null,
null,
onNavigationEvent = { onNavigationEvent = {
viewModel.setEvent( viewModel.setEvent(
ConnectionContract.Event.OnHostNavigationEvent(it) ConnectionContract.Event.OnHostNavigationEvent(it)
@ -186,10 +172,12 @@ fun ConnectionScreen(
} }
} }*/
} }
/*
innerScreen?.let { innerScreen?.let {
Surface( Surface(
@ -245,20 +233,20 @@ fun ConnectionScreen(
} }
is ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory -> { is ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory -> {
HostHistory( /*GateHistoryScreen(
ble = it.ble, ble = it.ble,
onDismiss = { onDismiss = {
innerScreen = null innerScreen = null
} }
) )*/
} }
is ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable -> { is ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable -> {
BleTableEditScreen(it.serial){ GateBleTableScreen {
when(it){ when(it){
BleTableEditContract.Effect.Navigation.NavigateUp -> { GateBleTableContract.Effect.Navigation.Up -> {
innerScreen = null innerScreen = null
} }
} }
@ -272,26 +260,28 @@ fun ConnectionScreen(
} }
*/
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun LoadingState(){ private fun LoadingState(){
Column { Box(
Box(modifier = Modifier.fillMaxSize()) { contentAlignment = Alignment.Center,
CircularProgressIndicator( modifier = Modifier.fillMaxSize()
strokeCap = StrokeCap.Round, ) {
modifier = Modifier.align(Alignment.Center ContainedLoadingIndicator()
)
)
}
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun DisplayException( private fun DisplayException(
viewState: ConnectionContract.State.DisplayException,
onEvent: (ConnectionContract.Event) -> Unit onEvent: (ConnectionContract.Event) -> Unit
){ ){
@ -301,9 +291,20 @@ private fun DisplayException(
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, 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( Text(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
@ -313,9 +314,9 @@ private fun DisplayException(
Spacer(modifier = Modifier.height(18.dp)) Spacer(modifier = Modifier.height(18.dp))
SmallPrimaryButton( 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.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel 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.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 llc.arma.ble.domain.usecase.GetBleBySerial
import javax.inject.Inject import javax.inject.Inject
@ -41,19 +38,19 @@ class ConnectionViewModel @Inject constructor(
state: ConnectionContract.State, state: ConnectionContract.State,
event: ConnectionContract.Event.OnHostNavigationEvent event: ConnectionContract.Event.OnHostNavigationEvent
) { ) {
when(event.event){ /*when(event.event){
HostContract.Effect.Navigation.NavigateUp -> { GateContract.Effect.Navigation.Up -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateUp ConnectionContract.Effect.Navigation.NavigateUp
} }
} }
HostContract.Effect.Navigation.NavigateToChangePassword -> { is GateContract.Effect.Navigation.ChangePassword -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!) ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
} }
} }
is HostContract.Effect.Navigation.NavigateToHostHistory -> { is GateContract.Effect.Navigation.GateHistory -> {
setEffect { setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory( ConnectionContract.Effect.InnerNavigation.NavigateToHostHistory(
event.event.ble event.event.ble
@ -61,14 +58,17 @@ class ConnectionViewModel @Inject constructor(
} }
} }
is HostContract.Effect.Navigation.NavigateToBleTable -> { is GateContract.Effect.Navigation.BleTable -> {
setEffect { setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable( ConnectionContract.Effect.InnerNavigation.NavigateHostToBleTable(
event.event.serial event.event.serial
) )
} }
} }
}
is GateContract.Effect.Navigation.TxSelector -> TODO()
is GateContract.Effect.Navigation.ReadIntervalSelector -> TODO()
}*/
} }
private fun reduce( private fun reduce(
@ -76,16 +76,18 @@ class ConnectionViewModel @Inject constructor(
event: ConnectionContract.Event.OnBeaconNavigationEvent event: ConnectionContract.Event.OnBeaconNavigationEvent
) { ) {
when(event.event){ when(event.event){
BeaconContract.Effect.Navigation.NavigateUp -> { BeaconContract.Effect.Navigation.Up -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateUp ConnectionContract.Effect.Navigation.NavigateUp
} }
} }
BeaconContract.Effect.Navigation.NavigateToChangePassword -> { is BeaconContract.Effect.Navigation.PasswordForm -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!) 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, state: ConnectionContract.State,
event: ConnectionContract.Event.OnThermometerNavigationEvent event: ConnectionContract.Event.OnThermometerNavigationEvent
) { ) {
when(event.event){ /*(event.event){
ThermometerContract.Effect.Navigation.NavigateUp -> { ThermometerContract.Effect.Navigation.Up -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateUp ConnectionContract.Effect.Navigation.NavigateUp
} }
} }
ThermometerContract.Effect.Navigation.NavigateToChangePassword -> { ThermometerContract.Effect.Navigation.ChangePassword -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!) 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( private fun reduce(
state: ConnectionContract.State, state: ConnectionContract.State,
event: ConnectionContract.Event.OnAccelNavigationEvent event: ConnectionContract.Event.OnAccelNavigationEvent
) { ) {
when(event.event){ /*when(event.event){
AccelerometerContract.Effect.Navigation.NavigateToChangePassword -> { is AccelerometerContract.Effect.Navigation.ChangePassword -> {
setEffect { setEffect {
ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!) ConnectionContract.Effect.Navigation.NavigateToChangePassword(savedStateHandle.get<String>("serial")!!)
} }
} }
is AccelerometerContract.Effect.Navigation.NavigateToAccelHistory -> { is AccelerometerContract.Effect.Navigation.AccelHistory -> {
setEffect { setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory( ConnectionContract.Effect.InnerNavigation.NavigateToAccelHistory(
event.event.ble, event.event.ble,
@ -131,7 +141,7 @@ class ConnectionViewModel @Inject constructor(
} }
} }
is AccelerometerContract.Effect.Navigation.NavigateToAccelRealtime -> { is AccelerometerContract.Effect.Navigation.AccelRealtime -> {
setEffect { setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime( ConnectionContract.Effect.InnerNavigation.NavigateToAccelRealtime(
event.event.ble, event.event.ble,
@ -143,7 +153,7 @@ class ConnectionViewModel @Inject constructor(
) )
} }
} }
is AccelerometerContract.Effect.Navigation.NavigateToAccelSpectre -> { is AccelerometerContract.Effect.Navigation.AccelSpectre -> {
setEffect { setEffect {
ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre( ConnectionContract.Effect.InnerNavigation.NavigateToAccelSpectre(
event.event.ble, 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( private fun reduce(
@ -179,7 +194,7 @@ class ConnectionViewModel @Inject constructor(
} }
private fun refreshBle(){ private fun refreshBle(){
val serial = savedStateHandle.get<String>("serial") /*val serial = savedStateHandle.get<String>("serial")
if(serial != null){ if(serial != null){
@ -189,28 +204,37 @@ class ConnectionViewModel @Inject constructor(
ConnectionContract.State.Loading ConnectionContract.State.Loading
} }
getBleBySerial(serial).fold( var tries = 0L
onSuccess = {
it.onEach { while (true) {
getBleBySerial(serial, this).fold(
onSuccess = {
it.onEach {
setState {
ConnectionContract.State.Display(
ble = it
)
}
}.launchIn(viewModelScope)
return@launch
},
onFailure = {
setState { setState {
ConnectionContract.State.Display( tries += 1
ble = it ConnectionContract.State.DisplayException(tries, it)
)
} }
}.launchIn(viewModelScope)
},
onFailure = {
setState {
ConnectionContract.State.DisplayException(it)
} }
} )
)
}
} }
} else { } else {
throw IllegalArgumentException("serial arg must not be null") 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.FastOutSlowInEasing
import androidx.compose.animation.core.animateFloatAsState 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.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack 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.CloudUpload
import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.TableView 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis 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.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll import com.patrykandpatrick.vico.core.scroll.InitialScroll
import dagger.hilt.android.lifecycle.HiltViewModel import com.ramcosta.composedestinations.annotation.Destination
import kotlinx.coroutines.Job import com.ramcosta.composedestinations.annotation.RootGraph
import kotlinx.coroutines.flow.launchIn import com.ramcosta.composedestinations.navigation.DestinationsNavigator
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.common.ProgressState
import llc.arma.ble.domain.model.Ble 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.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
import javax.inject.Inject
class AccelEntry( class AccelEntry(
val localDate: Long, val localDate: Long,
@ -85,43 +67,43 @@ class AccelEntry(
} }
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AccelerometerHistory( fun AccelerometerHistory(
ble: BleInfo, bleSerial: String,
accelScale: AccelScale, navigator: DestinationsNavigator
accelMode: AccelViewMode,
fftAxis: FftAxis,
fftMode: FftViewMode,
frequency: FftFrequency,
onDismiss: (() -> Unit)? = null,
onShowStatistic: () -> Unit,
) { ) {
val viewModel = hiltViewModel<AccelerometerHistoryViewModel>() val viewModel = hiltViewModel<AccelerometerHistoryViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
LaunchedEffect(ble.serial) { LaunchedEffect(bleSerial) {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnStart(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency)) viewModel.setEvent(
AccelerometerHistoryContract.Event.OnStart(bleSerial)
)
} }
Column { Column {
TopAppBar( TopAppBar(
navigationIcon = { navigationIcon = {
onDismiss?.let {
IconButton(onClick = it) { IconButton(
Icon( onClick = {
imageVector = Icons.AutoMirrored.Rounded.ArrowBack, navigator.popBackStack()
contentDescription = null
)
} }
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
} }
}, },
title = { title = {
val title = when(state){ val title = /*when(state){
is AccelerometerHistoryContract.State.Display -> { is AccelerometerHistoryContract.State.Display -> {
when (state.loadingHistoryState) { when (state.loadingHistoryState) {
is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})" is ProgressState.Finished -> "${accelMode.localized} (${state.loadingHistoryState.data.size})"
@ -130,7 +112,7 @@ fun AccelerometerHistory(
} }
} }
AccelerometerHistoryContract.State.Exception -> accelMode.localized AccelerometerHistoryContract.State.Exception -> accelMode.localized
} }*/ ""
Text( Text(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
@ -141,7 +123,7 @@ fun AccelerometerHistory(
actions = { actions = {
IconButton( IconButton(
onClick = onShowStatistic, onClick = {} ,
enabled = when(state){ enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
AccelerometerHistoryContract.State.Exception -> false AccelerometerHistoryContract.State.Exception -> false
@ -170,7 +152,9 @@ fun AccelerometerHistory(
IconButton( IconButton(
onClick = { onClick = {
viewModel.setEvent(AccelerometerHistoryContract.Event.OnRefreshHistory(ble.name, ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency)) viewModel.setEvent(
AccelerometerHistoryContract.Event.OnRefreshHistory(bleSerial)
)
}, },
enabled = when(state){ enabled = when(state){
is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished is AccelerometerHistoryContract.State.Display -> state.loadingHistoryState is ProgressState.Finished
@ -190,8 +174,8 @@ fun AccelerometerHistory(
Box(modifier = Modifier) { Box(modifier = Modifier) {
when (state) { when (state) {
is AccelerometerHistoryContract.State.Display -> Display(state = state) is AccelerometerHistoryContract.State.Display -> DisplayState(state = state)
is AccelerometerHistoryContract.State.Exception -> Exception() is AccelerometerHistoryContract.State.Exception -> ErrorState()
} }
} }
@ -206,13 +190,14 @@ val timeFormatter = SimpleDateFormat("HH:mm", Locale.getDefault())
@Composable @Composable
fun Display( private fun DisplayState(
state: AccelerometerHistoryContract.State.Display state: AccelerometerHistoryContract.State.Display
) { ) {
Box(modifier = Modifier Box(
.padding(8.dp) modifier = Modifier
.fillMaxSize() .padding(8.dp)
.fillMaxSize()
) { ) {
when (state.loadingHistoryState) { when (state.loadingHistoryState) {
@ -514,7 +499,7 @@ fun Display(
} }
@Composable @Composable
private fun Exception() { private fun ErrorState() {
Box( Box(
modifier = Modifier modifier = Modifier
.padding(8.dp) .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.clickable
import androidx.compose.foundation.layout.Column 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.draw.clip
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import llc.arma.ble.app.ui.common.PrimaryButton 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.app.ui.screen.locale.localized
import llc.arma.ble.domain.usecase.FftAxis import llc.arma.ble.domain.usecase.FftAxis
@ -44,45 +44,4 @@ fun SelectorItem(
Text(text = label) Text(text = label)
} }
}
@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.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -21,8 +21,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton 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.ShapeType
import llc.arma.ble.app.ui.screen.inspection.host.view.BleMenuItem 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.app.ui.screen.locale.localizedName
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
@ -57,6 +61,7 @@ fun Write(
state.writeRequest.tx?.let { state.writeRequest.tx?.let {
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Мощность", title = "Мощность",
subtitle = "${it.localizedName} db", subtitle = "${it.localizedName} db",
) )
@ -66,6 +71,7 @@ fun Write(
state.writeRequest.saveHistorySettings?.let { state.writeRequest.saveHistorySettings?.let {
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений", title = "Сохранять историю измерений",
subtitle = when(it){ subtitle = when(it){
Ble.Accelerometer.HistorySettings.Disabled -> "Выключено" Ble.Accelerometer.HistorySettings.Disabled -> "Выключено"
@ -82,6 +88,7 @@ fun Write(
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал измерений", title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин. $seconds сек." subtitle = "$hours ч. $minutes мин. $seconds сек."
) )
@ -95,6 +102,7 @@ fun Write(
val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond val seconds = (it - (hours * millisInHour) - (minutes * millisInMinute)) / millisInSecond
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал чтения", title = "Интервал чтения",
subtitle = "$hours ч. $minutes мин. $seconds сек." subtitle = "$hours ч. $minutes мин. $seconds сек."
) )
@ -112,7 +120,7 @@ fun Write(
SecondaryButton( SecondaryButton(
label = "Отменить" label = "Отменить"
) { ) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
} }
} else { } else {
@ -130,7 +138,7 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ок" label = "Ок"
) { ) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
} }
} }
@ -155,7 +163,7 @@ fun Write(
SecondaryButton( SecondaryButton(
label = "Отменить" label = "Отменить"
) { ) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
} }
} }
@ -197,7 +205,7 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ок" label = "Ок"
) { ) {
onEvent(AccelerometerContract.Event.OnHideWriteBlePreview) //onEvent(AccelerometerContract.Event.OnHideWriteBlePreview)
} }
} }
@ -239,7 +247,7 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ок" 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.animation.core.tween
import androidx.compose.foundation.layout.Arrangement 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.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack 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.Refresh
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Divider import androidx.compose.material3.Divider
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.viewModelScope
import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent import com.patrykandpatrick.vico.compose.axis.axisGuidelineComponent
import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis import com.patrykandpatrick.vico.compose.axis.horizontal.bottomAxis
import com.patrykandpatrick.vico.compose.axis.vertical.startAxis import com.patrykandpatrick.vico.compose.axis.vertical.startAxis
@ -47,110 +48,85 @@ import com.patrykandpatrick.vico.core.entry.ChartEntryModelProducer
import com.patrykandpatrick.vico.core.entry.FloatEntry import com.patrykandpatrick.vico.core.entry.FloatEntry
import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition import com.patrykandpatrick.vico.core.scroll.AutoScrollCondition
import com.patrykandpatrick.vico.core.scroll.InitialScroll import com.patrykandpatrick.vico.core.scroll.InitialScroll
import dagger.hilt.android.lifecycle.HiltViewModel import com.ramcosta.composedestinations.annotation.Destination
import kotlinx.coroutines.Job import com.ramcosta.composedestinations.annotation.RootGraph
import kotlinx.coroutines.flow.launchIn import com.ramcosta.composedestinations.navigation.DestinationsNavigator
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.app.ui.screen.locale.localized
import llc.arma.ble.domain.model.Ble import llc.arma.ble.domain.model.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.usecase.AccelScale import llc.arma.ble.domain.usecase.AccelScale
import llc.arma.ble.domain.usecase.AccelViewMode 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.FftAxis
import llc.arma.ble.domain.usecase.FftFrequency import llc.arma.ble.domain.usecase.FftFrequency
import llc.arma.ble.domain.usecase.FftViewMode import llc.arma.ble.domain.usecase.FftViewMode
import llc.arma.ble.domain.usecase.GetAccelerometerMeasureBySerialFlow
import javax.inject.Inject
@OptIn(ExperimentalMaterial3Api::class)
@Destination<RootGraph>
@Composable @Composable
fun AccelerometerRealtime( fun AccelerometerRealtime(
ble: BleInfo, bleSerial: String,
accelScale: AccelScale, accelScale: AccelScale,
accelMode: AccelViewMode, accelMode: AccelViewMode,
fftAxis: FftAxis, fftAxis: FftAxis,
fftMode: FftViewMode, fftMode: FftViewMode,
frequency: FftFrequency, frequency: FftFrequency,
onDismiss: (() -> Unit)? = null navigator: DestinationsNavigator
) { ) {
val viewModel = hiltViewModel<AccelerometerAccelViewModel>() val viewModel = hiltViewModel<AccelerometerAccelViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
viewModel.setEvent(AccelerometerAccelContract.Event.OnStart(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency)) Scaffold(
topBar = {
DisposableEffect(key1 = "ble", effect = { TopAppBar(
navigationIcon = {
onDispose { IconButton(
viewModel.setEvent(AccelerometerAccelContract.Event.StopMeasure) onClick = navigator::popBackStack
} ) {
Icon(
}) imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
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
)
}
}
Text(
modifier = Modifier.weight(1f),
text = accelMode.localized,
style = MaterialTheme.typography.titleLarge
)
IconButton(
onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefreshHistory(ble.serial, accelScale, accelMode, fftAxis, fftMode, frequency))
}, },
enabled = true title = {
) { Text(
Icon( text = accelMode.localized,
imageVector = Icons.Rounded.Refresh, style = MaterialTheme.typography.titleLarge
contentDescription = null )
) },
} actions = {
IconButton(
onClick = {
viewModel.setEvent(AccelerometerAccelContract.Event.OnRefresh)
},
enabled = true
) {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
}
)
} }
) {
Spacer(modifier = Modifier.height(16.dp)) Box(modifier = Modifier.padding(it)) {
Box(modifier = Modifier) {
when (state) { when (state) {
is AccelerometerAccelContract.State.Display -> Display(state = state) is AccelerometerAccelContract.State.Display -> DisplayState(state = state)
is AccelerometerAccelContract.State.Exception -> Exception() is AccelerometerAccelContract.State.Exception -> ExceptionState()
} }
} }
} }
} }
@Composable @Composable
fun Display( private fun DisplayState(
state: AccelerometerAccelContract.State.Display state: AccelerometerAccelContract.State.Display
) { ) {
@ -528,7 +504,7 @@ fun Angle(
} }
@Composable @Composable
private fun Exception( private fun ExceptionState(
) { ) {
Box( Box(
@ -546,161 +522,4 @@ 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.ViewSideEffect
import llc.arma.ble.app.ui.common.ViewState import llc.arma.ble.app.ui.common.ViewState
import llc.arma.ble.app.ui.model.BleView 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 import llc.arma.ble.domain.model.Ble
class BeaconContract { class BeaconContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
data object OnNavigateUp : Event()
object OnWriteBle : Event() object OnWriteBle : Event()
object OnHideWriteBlePreview : Event() object OnHideWriteBlePreview : Event()
object OnShowWriteBlePreview : Event() object OnShowWriteBlePreview : Event()
object OnPowerEdit : Event() data object OnPowerEdit : Event()
data class OnBleChanged( data class OnBleChanged(
val ble: Ble.Beacon val ble: Ble.Beacon
@ -26,7 +29,7 @@ class BeaconContract {
val tx: BleView.BleState.TX val tx: BleView.BleState.TX
) : Event() ) : Event()
data class OnTxChanged(val tx: Int) : Event() data class OnTxChanged(val tx: BleView.BleState.TX) : Event()
object OnNavigateUpClicked : Event() object OnNavigateUpClicked : Event()
@ -36,7 +39,7 @@ class BeaconContract {
sealed class State : ViewState { sealed class State : ViewState {
object Loading : State() data object Loading : State()
data class Display( data class Display(
val origin: Ble.Beacon, val origin: Ble.Beacon,
@ -54,9 +57,9 @@ class BeaconContract {
val writeRequest: Ble.Beacon.WriteRequest val writeRequest: Ble.Beacon.WriteRequest
) : WriteState() ) : 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 { sealed class Effect : ViewSideEffect {
object ShowPowerPicker : Effect() data object HideWriteBlePreview : Effect()
object HidePowerPicker : Effect() data object ShowWriteBlePreview : Effect()
object HideWriteBlePreview : Effect()
object ShowWriteBlePreview : Effect()
sealed class Navigation : 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.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize 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.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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -13,24 +24,35 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.hilt.navigation.compose.hiltViewModel 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.delay
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch 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.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.DisplayState
import llc.arma.ble.app.ui.screen.inspection.beacon.view.Write 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 { enum class SheetPage {
WRITE, POWER_EDIT WRITE
} }
@Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BeaconScreen( fun BeaconScreen(
ble: Ble.Beacon, bleSerial: String,
onNavigationEvent: (BeaconContract.Effect.Navigation) -> Unit txSelectResult: ResultRecipient<TxPowerSelectorScreenDestination, BleView.BleState.TX>,
navigator: DestinationsNavigator
) { ) {
val viewModel = hiltViewModel<BeaconViewModel>() val viewModel = hiltViewModel<BeaconViewModel>()
@ -42,10 +64,14 @@ fun BeaconScreen(
val bottomDialog = rememberBottomDialogState() val bottomDialog = rememberBottomDialogState()
LaunchedEffect("effect"){ txSelectResult.onResult {
viewModel.setEvent(BeaconContract.Event.OnTxChanged(it))
}
LaunchedEffect(Unit){
viewModel.effect.onEach { viewModel.effect.onEach {
when(it){ when(it){
is BeaconContract.Effect.Navigation -> onNavigationEvent(it)
BeaconContract.Effect.HideWriteBlePreview -> launch { BeaconContract.Effect.HideWriteBlePreview -> launch {
sheetPage = null sheetPage = null
} }
@ -54,22 +80,19 @@ fun BeaconScreen(
delay(100) delay(100)
sheetPage = SheetPage.WRITE sheetPage = SheetPage.WRITE
} }
BeaconContract.Effect.HidePowerPicker -> launch {
sheetPage = null is BeaconContract.Effect.Navigation.PasswordForm ->
} navigator.navigate(ChangePasswordScreenDestination(it.bleSerial))
BeaconContract.Effect.ShowPowerPicker -> launch {
sheetPage = null is BeaconContract.Effect.Navigation.TxSelector ->
delay(100) navigator.navigate(TxPowerSelectorScreenDestination(it.tx))
sheetPage = SheetPage.POWER_EDIT
} BeaconContract.Effect.Navigation.Up ->
navigator.popBackStack()
} }
}.launchIn(this) }.launchIn(this)
} }
LaunchedEffect(ble){
viewModel.setEvent(BeaconContract.Event.OnBleChanged(ble))
}
LaunchedEffect(sheetPage){ LaunchedEffect(sheetPage){
when(sheetPage){ when(sheetPage){
SheetPage.WRITE -> bottomDialog.show { SheetPage.WRITE -> bottomDialog.show {
@ -88,48 +111,67 @@ 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 -> { else -> {
bottomDialog.hide() bottomDialog.hide()
} }
} }
} }
Column { Scaffold(
topBar = {
when(state){ TopAppBar(
is BeaconContract.State.Display -> DisplayState( navigationIcon = {
onEvent = { IconButton(
viewModel.setEvent(it) onClick = {
viewModel.setEvent(BeaconContract.Event.OnNavigateUp)
}
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}, },
ble = state.beacon, title = {
origin = state.origin Text(text = BleInfo.Type.BEACON.localized)
}
) )
is BeaconContract.State.Loading -> LoadingState()
} }
) {
Box(
modifier = Modifier.padding(it)
) {
when(state){
is BeaconContract.State.Display -> DisplayState(
onEvent = {
viewModel.setEvent(it)
},
ble = state.beacon,
origin = state.origin
)
is BeaconContract.State.Loading -> LoadingState()
}
}
} }
} }
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun LoadingState(){ 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 package llc.arma.ble.app.ui.screen.inspection.beacon
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.BeaconScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView 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.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 llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BeaconViewModel @Inject constructor( class BeaconViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
getBleBySerial: GetBleBySerial,
private val bleMapper: BleMapper, private val bleMapper: BleMapper,
private val writeBle: WriteBle, private val writeBle: WriteBle,
private val bleViewMapper: BleViewMapper private val bleViewMapper: BleViewMapper
) : BaseViewModel<BeaconContract.State, BeaconContract.Event, BeaconContract.Effect>() { ) : 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 setInitialState() = BeaconContract.State.Loading
override fun handleEvents(event: BeaconContract.Event) { 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.OnWriteBle -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event) is BeaconContract.Event.OnPowerChanged -> reduce(viewState.value, event)
is BeaconContract.Event.OnPowerEdit -> 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 { setEffect {
BeaconContract.Effect.HidePowerPicker BeaconContract.Effect.Navigation.Up
} }
} }
@ -56,7 +115,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State, state: BeaconContract.State,
event: BeaconContract.Event.OnPowerEdit 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, state: BeaconContract.State,
event: BeaconContract.Event.OnNavigateUpClicked event: BeaconContract.Event.OnNavigateUpClicked
) { ) {
setEffect { BeaconContract.Effect.Navigation.NavigateUp } setEffect { BeaconContract.Effect.Navigation.Up }
} }
private fun reduce( private fun reduce(
@ -107,9 +172,13 @@ class BeaconViewModel @Inject constructor(
state: BeaconContract.State, state: BeaconContract.State,
event: BeaconContract.Event.OnChangePassword event: BeaconContract.Event.OnChangePassword
) { ) {
val params = BeaconScreenDestination.argsFrom(savedStateHandle)
setEffect { setEffect {
BeaconContract.Effect.Navigation.NavigateToChangePassword BeaconContract.Effect.Navigation.PasswordForm(params.bleSerial)
} }
} }
private fun reduce( private fun reduce(

View File

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

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.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect 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.Ble
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
class HostContract { class GateContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
@ -17,19 +17,13 @@ class HostContract {
data object OnShowWriteBlePreview : Event() data object OnShowWriteBlePreview : Event()
data object OnPowerEdit : Event() data object OnTxSelect : Event()
data class OnBleChanged(
val ble: Ble.Host
) : Event()
data class OnPowerChanged( data class OnPowerChanged(
val tx: BleView.BleState.TX val tx: BleView.BleState.TX
) : Event() ) : Event()
data class OnTxChanged(val tx: Int) : Event() data object OnHistoryIntervalSelect : Event()
data object OnShowIntervalEdit : Event()
data class OnSaveIntervalChanged( data class OnSaveIntervalChanged(
val interval: Long val interval: Long
@ -41,7 +35,7 @@ class HostContract {
val interval: Long val interval: Long
) : Event() ) : Event()
data object OnNavigateUpClicked : Event() data object OnNavigateUp : Event()
data object OnChangePassword : Event() data object OnChangePassword : Event()
@ -53,22 +47,24 @@ class HostContract {
sealed class State : ViewState { sealed class State : ViewState {
data object Loading : State() data class Loading(
val attempt: Int?
) : State()
data class Display( data class Display(
val origin: Ble.Host, val origin: Ble.Gate,
val host: BleView.Host, val gate: BleView.Gate,
val writeState: WriteState? val writeState: WriteState?
) : State() { ) : State() {
sealed class WriteState { sealed class WriteState {
data class DisplayPreview( data class DisplayPreview(
val writeRequest: Ble.Host.WriteRequest val writeRequest: Ble.Gate.WriteRequest
) : WriteState() ) : WriteState()
data class Writing( data class Writing(
val writeRequest: Ble.Host.WriteRequest val writeRequest: Ble.Gate.WriteRequest
) : WriteState() ) : WriteState()
data object Success : WriteState() data object Success : WriteState()
@ -83,36 +79,36 @@ class HostContract {
sealed class Effect : ViewSideEffect { sealed class Effect : ViewSideEffect {
data object ShowPowerPicker : Effect()
data object HidePowerPicker : Effect()
data object HideWriteBlePreview : Effect()
data object ShowWriteBlePreview : 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() { 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, val ble: BleInfo,
) : Navigation() ) : Navigation()
data class NavigateToBleTable( data class BleTable(
val serial: String, val serial: String,
) : Navigation() ) : 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.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -24,13 +24,16 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton 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 import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable @Composable
fun Write( fun Write(
state: HostContract.State.Display.WriteState, state: GateContract.State.Display.WriteState,
onEvent: (HostContract.Event) -> Unit onEvent: (GateContract.Event) -> Unit
) { ) {
Column( Column(
@ -46,7 +49,7 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
when (state) { 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) { if(state.writeRequest.tx != null || state.writeRequest.interval != null || state.writeRequest.readInterval !== null) {
@ -174,13 +177,13 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Записать" label = "Записать"
) { ) {
onEvent(HostContract.Event.OnWriteBle) onEvent(GateContract.Event.OnWriteBle)
} }
SecondaryButton( SecondaryButton(
label = "Отменить" label = "Отменить"
) { ) {
onEvent(HostContract.Event.OnHideWriteBlePreview) onEvent(GateContract.Event.OnHideWriteBlePreview)
} }
} else { } else {
@ -198,14 +201,14 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ок" label = "Ок"
) { ) {
onEvent(HostContract.Event.OnHideWriteBlePreview) onEvent(GateContract.Event.OnHideWriteBlePreview)
} }
} }
} }
is HostContract.State.Display.WriteState.Writing -> { is GateContract.State.Display.WriteState.Writing -> {
Box { Box {
@ -224,7 +227,7 @@ fun Write(
SecondaryButton( SecondaryButton(
label = "Отменить" 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 { Box {
@ -266,7 +269,7 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ок" 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 { Box {
@ -308,7 +311,7 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ок" 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.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect 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.BleInfo
import llc.arma.ble.domain.model.BleName import llc.arma.ble.domain.model.BleName
class BleTableEditContract { class GateBleTableContract {
sealed class Event : ViewEvent { sealed class Event : ViewEvent {
@ -16,9 +16,7 @@ class BleTableEditContract {
data object OnWrite: Event() data object OnWrite: Event()
data class OnStart( data object OnRestart : Event()
val serial: String
) : Event()
data class OnAddBle( data class OnAddBle(
val ble: BleName val ble: BleName
@ -63,7 +61,7 @@ class BleTableEditContract {
sealed class Navigation : Effect() { 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.activity.compose.BackHandler
import androidx.compose.foundation.clickable 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.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.RemoveCircleOutline import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ContainedLoadingIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -38,40 +37,42 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.hilt.navigation.compose.hiltViewModel 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.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.PrimaryButton import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.rememberBottomDialogState 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.app.ui.screen.ble.BleItem
import llc.arma.ble.domain.model.BleInfo import llc.arma.ble.domain.model.BleInfo
import llc.arma.ble.domain.model.BleName import llc.arma.ble.domain.model.BleName
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Destination<RootGraph>
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun BleTableEditScreen( fun GateBleTableScreen(
serial: String, bleSerial: String,
onEvent: (event: BleTableEditContract.Effect.Navigation) -> Unit navigator: DestinationsNavigator
) { ) {
val viewModel = hiltViewModel<BleTableEditViewModel>() val viewModel = hiltViewModel<GateBleTableViewModel>()
val state = viewModel.viewState.value val state = viewModel.viewState.value
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.effect.onEach { viewModel.effect.collect {
when(it){ 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 { var showSelector by remember {
@ -102,7 +103,7 @@ fun BleTableEditScreen(
if(showSelector){ if(showSelector){
showSelector = false showSelector = false
} else { } else {
onEvent(BleTableEditContract.Effect.Navigation.NavigateUp) navigator.popBackStack()
} }
}) { }) {
Icon( Icon(
@ -125,7 +126,7 @@ fun BleTableEditScreen(
actions = { actions = {
if(showSelector.not()){ if(showSelector.not()){
IconButton( IconButton(
enabled = state is BleTableEditContract.State.Display, enabled = state is GateBleTableContract.State.Display,
onClick = { showSelector=true } onClick = { showSelector=true }
) { ) {
Icon( Icon(
@ -138,20 +139,20 @@ fun BleTableEditScreen(
) )
if(state is BleTableEditContract.State.Loading){ if(state is GateBleTableContract.State.Loading){
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize() modifier = Modifier.fillMaxSize()
){ ){
CircularProgressIndicator() ContainedLoadingIndicator()
} }
} }
if(state is BleTableEditContract.State.Error){ if(state is GateBleTableContract.State.Error){
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
@ -172,7 +173,7 @@ fun BleTableEditScreen(
PrimaryButton( PrimaryButton(
label = "Повторить" 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) { if(showSelector) {
@ -194,7 +195,7 @@ fun BleTableEditScreen(
} }
) { ) {
viewModel.setEvent( viewModel.setEvent(
BleTableEditContract.Event.OnAddBle( GateBleTableContract.Event.OnAddBle(
BleName( BleName(
serial = it.serial, serial = it.serial,
name = it.name name = it.name
@ -210,9 +211,10 @@ fun BleTableEditScreen(
} }
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(horizontal = 12.dp) .padding(horizontal = 16.dp)
) { ) {
val savedBleSerials = state.savedBleTable.map { it.serial } val savedBleSerials = state.savedBleTable.map { it.serial }
@ -222,45 +224,76 @@ fun BleTableEditScreen(
item { item {
Text( Text(
style = MaterialTheme.typography.titleLarge,
textAlign = TextAlign.Center,
text = "Новые BLE", text = "Новые BLE",
modifier = Modifier
.padding(
horizontal = 12.dp,
vertical = 8.dp
)
) )
} }
items(items = newBle) { items(items = newBle) {
SelectBleItem( SelectBleItem(
ble = it, ble = it,
onClick = { onClick = {
editBle = it 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 {
item { Text(
Text( text = "Сохраненные BLE",
style = MaterialTheme.typography.titleLarge, modifier = Modifier
textAlign = TextAlign.Center, .padding(
text = "Сохраненные BLE", horizontal = 12.dp,
) vertical = 8.dp
} )
)
items(items = state.savedBleTable) { ble -> }
SavedBleItem(
checked = state.newTable.any { it.serial == ble.serial}, items(items = state.savedBleTable) { ble ->
ble = ble SavedBleItem(
){ checked = state.newTable.any { it.serial == ble.serial},
viewModel.setEvent(BleTableEditContract.Event.OnAddBle(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(GateBleTableContract.Event.OnAddBle(ble))
} }
} }
} }
@ -268,14 +301,14 @@ fun BleTableEditScreen(
PrimaryButton( PrimaryButton(
label = "Записать" label = "Записать"
) { ) {
viewModel.setEvent(BleTableEditContract.Event.OnWritePreview) viewModel.setEvent(GateBleTableContract.Event.OnWritePreview)
} }
if(editBle != null){ if(editBle != null){
Dialog( Dialog(
onDismissRequest = { onDismissRequest = {
viewModel.setEvent(BleTableEditContract.Event.OnAddBle(ble = editBle!!.copy())) viewModel.setEvent(GateBleTableContract.Event.OnAddBle(ble = editBle!!.copy()))
editBle = null editBle = null
} }
) { ) {
@ -310,7 +343,7 @@ fun BleTableEditScreen(
label = "Сохранить" label = "Сохранить"
) { ) {
viewModel.setEvent( viewModel.setEvent(
BleTableEditContract.Event.OnAddBle( GateBleTableContract.Event.OnAddBle(
ble = editBle!!.copy(name = name) ble = editBle!!.copy(name = name)
) )
) )
@ -327,7 +360,7 @@ fun BleTableEditScreen(
LaunchedEffect(key1 = bottomDialog.sheetState?.isVisible) { LaunchedEffect(key1 = bottomDialog.sheetState?.isVisible) {
if (bottomDialog.sheetState?.isVisible?.not() == true) { 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 @Composable
fun BleSelectorScreen( fun BleSelectorScreen(
saved: List<String>, saved: List<String>,
@ -370,6 +408,7 @@ fun BleSelectorScreen(
) { ) {
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
@ -390,7 +429,10 @@ fun BleSelectorScreen(
onCheckedChange = null onCheckedChange = null
) )
BleItem(ble = ble) { BleItem(
shapeType = bleList.filterNot { saved.contains(it.serial) }.takeShapeType(ble),
ble = ble
) {
onAddBle(ble) onAddBle(ble)
} }
@ -412,46 +454,56 @@ fun BleSelectorScreen(
@Composable @Composable
fun SelectBleItem( fun SelectBleItem(
shapeType: ShapeType,
ble: BleName, ble: BleName,
onClick: (() -> Unit)? = null, onClick: (() -> Unit)? = null,
onRemove: (() -> Unit)? = null, onRemove: (() -> Unit)? = null,
){ ){
Row( Surface(
verticalAlignment = Alignment.CenterVertically, color = MaterialTheme.colorScheme.surfaceContainer,
horizontalArrangement = Arrangement.spacedBy(12.dp), shape = shapeType.shape,
modifier = Modifier onClick = onClick ?: {}
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable { onClick?.invoke() }
.padding(vertical = 8.dp, horizontal = 16.dp)
) { ) {
Column( Row(
modifier = Modifier.weight(1f) verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp
)
) { ) {
Text(text = ble.name) Column(
modifier = Modifier.weight(1f)
) {
Text( Text(text = ble.name)
style = MaterialTheme.typography.bodyMedium,
text = ble.serial
)
} Text(
style = MaterialTheme.typography.bodyMedium,
onRemove?.let { text = ble.serial
IconButton(onClick = onRemove) {
Icon(
imageVector = Icons.Rounded.RemoveCircleOutline,
contentDescription = null
) )
} }
onRemove?.let {
IconButton(onClick = onRemove) {
Icon(
imageVector = Icons.Rounded.RemoveCircleOutline,
contentDescription = null
)
}
}
} }
} }
@ -460,31 +512,38 @@ fun SelectBleItem(
@Composable @Composable
fun SavedBleItem( fun SavedBleItem(
shapeType: ShapeType,
checked: Boolean, checked: Boolean,
ble: BleName, ble: BleName,
onClick: () -> Unit onClick: () -> Unit
){ ){
Row( Surface(
verticalAlignment = Alignment.CenterVertically, color = MaterialTheme.colorScheme.surfaceContainer,
horizontalArrangement = Arrangement.SpaceBetween, shape = shapeType.shape,
modifier = Modifier onClick = onClick
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.clickable { onClick() }
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(end = 12.dp)
) { ) {
Column { Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp, horizontal = 16.dp)
.padding(end = 12.dp)
) {
Text(text = ble.name) Column {
Text(text = ble.serial)
Text(text = ble.name)
Text(text = ble.serial)
}
Checkbox(checked = checked, onCheckedChange = null)
} }
Checkbox(checked = checked, onCheckedChange = null)
} }
} }

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.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.GateBleTableScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -14,32 +17,33 @@ import llc.arma.ble.domain.usecase.GetHostBleTableBySerial
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BleTableEditViewModel @Inject constructor( class GateBleTableViewModel @Inject constructor(
getFoundBle: GetFoundBle, private val getFoundBle: GetFoundBle,
private val savedStateHandle: SavedStateHandle,
private val getBleNamesFlow: GetBleNamesFlow, private val getBleNamesFlow: GetBleNamesFlow,
private val addBleToHostTable: AddBleToHostTable, private val addBleToHostTable: AddBleToHostTable,
private val getHostBleTableBySerial: GetHostBleTableBySerial private val getHostBleTableBySerial: GetHostBleTableBySerial
) : BaseViewModel<BleTableEditContract.State, BleTableEditContract.Event, BleTableEditContract.Effect>() { ) : BaseViewModel<GateBleTableContract.State, GateBleTableContract.Event, GateBleTableContract.Effect>() {
private var lastSerial: String = ""
init { init {
setEvent(GateBleTableContract.Event.OnRestart)
viewModelScope.launch { viewModelScope.launch {
while (true){ while (true){
val state = viewState.value val state = viewState.value
if(state is BleTableEditContract.State.Display) { if(state is GateBleTableContract.State.Display) {
setState { setState {
state.copy(bleAround = getFoundBle()) state.copy(bleAround = getFoundBle())
} }
} }
delay(1_000)
delay(1_000)
} }
@ -47,49 +51,51 @@ class BleTableEditViewModel @Inject constructor(
} }
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){ when(event){
is BleTableEditContract.Event.OnStart -> reduce(viewState.value, event) is GateBleTableContract.Event.OnRestart -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnAddBle -> reduce(viewState.value, event) is GateBleTableContract.Event.OnAddBle -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnWritePreview -> reduce(viewState.value, event) is GateBleTableContract.Event.OnWritePreview -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnHideWritePreview -> reduce(viewState.value, event) is GateBleTableContract.Event.OnHideWritePreview -> reduce(viewState.value, event)
is BleTableEditContract.Event.OnWrite -> reduce(viewState.value, event) is GateBleTableContract.Event.OnWrite -> reduce(viewState.value, event)
} }
} }
private fun reduce( private fun reduce(
state: BleTableEditContract.State, state: GateBleTableContract.State,
event: BleTableEditContract.Event.OnWrite event: GateBleTableContract.Event.OnWrite
) { ) {
if(state is BleTableEditContract.State.Display) { val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
if(state is GateBleTableContract.State.Display) {
viewModelScope.launch { viewModelScope.launch {
setState { setState {
state.copy( state.copy(
writeState = BleTableEditContract.State.Display.WriteState.Writing(state.newTable) writeState = GateBleTableContract.State.Display.WriteState.Writing(state.newTable)
) )
} }
addBleToHostTable.invoke( addBleToHostTable.invoke(
serial = lastSerial, serial = params.bleSerial,
ble = state.newTable ble = state.newTable
).fold( ).fold(
onSuccess = { onSuccess = {
setState { setState {
state.copy( state.copy(
writeState = BleTableEditContract.State.Display.WriteState.Success writeState = GateBleTableContract.State.Display.WriteState.Success
) )
} }
setEvent(BleTableEditContract.Event.OnStart(lastSerial)) setEvent(GateBleTableContract.Event.OnRestart)
}, },
onFailure = { onFailure = {
setState { setState {
state.copy( 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( private fun reduce(
state: BleTableEditContract.State, state: GateBleTableContract.State,
event: BleTableEditContract.Event.OnHideWritePreview event: GateBleTableContract.Event.OnHideWritePreview
) { ) {
if(state is BleTableEditContract.State.Display) { if(state is GateBleTableContract.State.Display) {
setState { setState {
state.copy(writeState = null) state.copy(writeState = null)
@ -117,14 +123,17 @@ class BleTableEditViewModel @Inject constructor(
} }
private fun reduce( private fun reduce(
state: BleTableEditContract.State, state: GateBleTableContract.State,
event: BleTableEditContract.Event.OnWritePreview event: GateBleTableContract.Event.OnWritePreview
) { ) {
if(state is BleTableEditContract.State.Display) { if(state is GateBleTableContract.State.Display) {
setState { 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( private fun reduce(
state: BleTableEditContract.State, state: GateBleTableContract.State,
event: BleTableEditContract.Event.OnAddBle 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}){ if(state.newTable.any { it.serial == event.ble.serial}){
@ -157,21 +166,22 @@ class BleTableEditViewModel @Inject constructor(
} }
private fun reduce( private fun reduce(
state: BleTableEditContract.State, state: GateBleTableContract.State,
event: BleTableEditContract.Event.OnStart event: GateBleTableContract.Event.OnRestart
) { ) {
lastSerial = event.serial
val params = GateBleTableScreenDestination.argsFrom(savedStateHandle)
setState { setState {
BleTableEditContract.State.Loading GateBleTableContract.State.Loading
} }
viewModelScope.launch { viewModelScope.launch {
val names = getBleNamesFlow.invoke().first() val names = getBleNamesFlow.invoke().first()
getHostBleTableBySerial(event.serial).fold( getHostBleTableBySerial(params.bleSerial).fold(
onSuccess = { onSuccess = {
val savedBle = it.map { ble -> BleName( val savedBle = it.map { ble -> BleName(
@ -179,17 +189,18 @@ class BleTableEditViewModel @Inject constructor(
serial = ble) } serial = ble) }
setState { setState {
BleTableEditContract.State.Display( GateBleTableContract.State.Display(
bleAround = emptyList(), bleAround = emptyList(),
newTable = savedBle, newTable = savedBle,
savedBleTable = savedBle, savedBleTable = savedBle,
writeState = null) writeState = null
)
} }
}, },
onFailure = { onFailure = {
setState { 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.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -24,11 +24,12 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton import llc.arma.ble.app.ui.common.SecondaryButton
import llc.arma.ble.app.ui.screen.ShapeType
@Composable @Composable
fun Write( fun Write(
state: BleTableEditContract.State.Display.WriteState, state: GateBleTableContract.State.Display.WriteState,
onEvent: (BleTableEditContract.Event) -> Unit onEvent: (GateBleTableContract.Event) -> Unit
) { ) {
Column( Column(
@ -44,7 +45,7 @@ fun Write(
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
when (state) { when (state) {
is BleTableEditContract.State.Display.WriteState.DisplayPreview -> { is GateBleTableContract.State.Display.WriteState.DisplayPreview -> {
Box( Box(
modifier = Modifier modifier = Modifier
@ -66,7 +67,7 @@ fun Write(
} }
items(items = state.writeRequest) { items(items = state.writeRequest) {
SelectBleItem(it) SelectBleItem(ShapeType.Singleton, it)
} }
if(state.writeRequest.isEmpty()){ if(state.writeRequest.isEmpty()){
@ -87,18 +88,18 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Записать" label = "Записать"
) { ) {
onEvent(BleTableEditContract.Event.OnWrite) onEvent(GateBleTableContract.Event.OnWrite)
} }
SecondaryButton ( SecondaryButton (
label = "Отменить" label = "Отменить"
) { ) {
onEvent(BleTableEditContract.Event.OnHideWritePreview) onEvent(GateBleTableContract.Event.OnHideWritePreview)
} }
} }
is BleTableEditContract.State.Display.WriteState.Writing -> { is GateBleTableContract.State.Display.WriteState.Writing -> {
Column { Column {
@ -115,13 +116,13 @@ fun Write(
SecondaryButton ( SecondaryButton (
label = "Отменить" label = "Отменить"
) { ) {
onEvent(BleTableEditContract.Event.OnHideWritePreview) onEvent(GateBleTableContract.Event.OnHideWritePreview)
} }
} }
} }
BleTableEditContract.State.Display.WriteState.Success -> { GateBleTableContract.State.Display.WriteState.Success -> {
Column { Column {
@ -153,13 +154,13 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ok" label = "Ok"
) { ) {
onEvent(BleTableEditContract.Event.OnHideWritePreview) onEvent(GateBleTableContract.Event.OnHideWritePreview)
} }
} }
} }
BleTableEditContract.State.Display.WriteState.Failure -> { GateBleTableContract.State.Display.WriteState.Failure -> {
Column { Column {
@ -191,7 +192,7 @@ fun Write(
PrimaryButton( PrimaryButton(
label = "Ok" 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.AnimatedContent
import androidx.compose.animation.SizeTransform 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.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.KeyboardArrowUp 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.FilledIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -29,22 +35,54 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.common.PrimaryButton
import llc.arma.ble.app.ui.model.BleView 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 @Composable
fun IntervalEdit( fun DurationSelectorScreen(
state: BleView.Host, qualifier: String? = null,
onEvent: (HostContract.Event) -> Unit, 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) { val viewModel = hiltViewModel<DurationSelectorViewModel>()
mutableIntStateOf((state.hostState.historyInterval).toInt()) 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( Column(
modifier = Modifier modifier = Modifier.padding(16.dp).fillMaxWidth()
) { ) {
Text( Text(
@ -56,22 +94,30 @@ fun IntervalEdit(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
DurationPicker( DurationPicker(
modifier = Modifier.align(Alignment.CenterHorizontally), minInterval = minimum,
value = value 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)) Spacer(modifier = Modifier.height(16.dp))
PrimaryButton( Button(
label = "Применить" onClick = {
viewModel.setEvent(DurationSelectorContract.Event.OnSave)
},
modifier = Modifier.fillMaxWidth()
) { ) {
onEvent( Text(
HostContract.Event.OnSaveIntervalChanged( text = "Применить"
value.toLong()
)
) )
} }
} }

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.ViewEvent
import llc.arma.ble.app.ui.common.ViewSideEffect import llc.arma.ble.app.ui.common.ViewSideEffect
@ -10,45 +10,39 @@ class ThermometerContract {
sealed class Event : ViewEvent { 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( data class OnSaveHistoryChanged(
val saveHistory: Boolean val saveHistory: Boolean
) : Event() ) : Event()
object OnPowerEdit : Event()
data class OnPowerChanged( data class OnPowerChanged(
val tx: BleView.BleState.TX val tx: BleView.BleState.TX
) : Event() ) : Event()
object OnSaveIntervalEdit : Event() data object OnSaveIntervalEdit : Event()
data class OnSaveIntervalChanged( data class OnSaveIntervalChanged(
val interval: Long val interval: Long
) : Event() ) : Event()
data class OnBleChanged( data object OnNavigateUp : Event()
val ble: Ble.Thermometer
) : Event()
object OnNavigateUpClicked : Event()
} }
sealed class State : ViewState { sealed class State : ViewState {
object Loading : State() data object Loading : State()
data class Display( data class Display(
val origin: Ble.Thermometer, val origin: Ble.Thermometer,
@ -66,9 +60,9 @@ class ThermometerContract {
val writeRequest: Ble.Thermometer.WriteRequest val writeRequest: Ble.Thermometer.WriteRequest
) : WriteState() ) : 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 { 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 ShowWriteBle : Effect()
object HideWriteBle : Effect() object HideWriteBle : Effect()
sealed class Navigation : 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.lifecycle.viewModelScope
import androidx.navigation.toRoute
import com.ramcosta.composedestinations.generated.destinations.ThermometerScreenDestination
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import llc.arma.ble.app.ui.common.BaseViewModel import llc.arma.ble.app.ui.common.BaseViewModel
import llc.arma.ble.app.ui.mapper.BleMapper import llc.arma.ble.app.ui.mapper.BleMapper
import llc.arma.ble.app.ui.mapper.BleViewMapper import llc.arma.ble.app.ui.mapper.BleViewMapper
import llc.arma.ble.app.ui.model.BleView 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.model.Ble
import llc.arma.ble.domain.usecase.GetBleBySerial
import llc.arma.ble.domain.usecase.WriteBle import llc.arma.ble.domain.usecase.WriteBle
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ThermometerViewModel @Inject constructor( class ThermometerViewModel @Inject constructor(
private val getBleBySerial: GetBleBySerial,
private val savedStateHandle: SavedStateHandle,
private val bleMapper: BleMapper, private val bleMapper: BleMapper,
private val bleViewMapper: BleViewMapper, private val bleViewMapper: BleViewMapper,
private val writeBle: WriteBle private val writeBle: WriteBle
) : BaseViewModel<ThermometerContract.State, ThermometerContract.Event, ThermometerContract.Effect>() { ) : 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 setInitialState() = ThermometerContract.State.Loading
override fun handleEvents(event: ThermometerContract.Event) { override fun handleEvents(event: ThermometerContract.Event) {
when(event){ when(event){
is ThermometerContract.Event.OnNavigateUpClicked -> reduce(viewState.value, event) is ThermometerContract.Event.OnNavigateUp -> reduce(viewState.value, event)
is ThermometerContract.Event.OnBleChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveIntervalChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event) is ThermometerContract.Event.OnSaveIntervalEdit -> reduce(viewState.value, event)
is ThermometerContract.Event.OnPowerChanged -> 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.OnSaveHistoryChanged -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowTemperatureHistory -> reduce(viewState.value, event) is ThermometerContract.Event.OnShowTemperatureHistory -> reduce(viewState.value, event)
is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event) is ThermometerContract.Event.OnShowWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event) is ThermometerContract.Event.OnHideWriteBlePreview -> reduce(viewState.value, event)
is ThermometerContract.Event.OnWriteBle -> reduce(viewState.value, event) is ThermometerContract.Event.OnWriteBle -> reduce(viewState.value, event)
is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event) is ThermometerContract.Event.OnChangePassword -> reduce(viewState.value, event)
is ThermometerContract.Event.OnTxSelect -> reduce(viewState.value, event)
} }
} }
private fun reduce( private fun reduce(
state: ThermometerContract.State, 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( private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnNavigateUp
) {
setEffect { ThermometerContract.Effect.Navigation.Up }
}
/*private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnBleChanged event: ThermometerContract.Event.OnBleChanged
) { ) {
@ -69,14 +126,16 @@ class ThermometerViewModel @Inject constructor(
} }
} }
} }*/
private fun reduce( private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalEdit event: ThermometerContract.Event.OnSaveIntervalEdit
) { ) {
setEffect { if(state is ThermometerContract.State.Display) {
ThermometerContract.Effect.ShowIntervalPicker setEffect {
ThermometerContract.Effect.Navigation.DurationSelector(state.thermometer.thermometerState.historyInterval.toInt())
}
} }
} }
@ -84,41 +143,26 @@ class ThermometerViewModel @Inject constructor(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnSaveIntervalChanged event: ThermometerContract.Event.OnSaveIntervalChanged
) { ) {
if(state is ThermometerContract.State.Display) { if(state is ThermometerContract.State.Display) {
state.thermometer.thermometerState.historyInterval = event.interval 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( private fun reduce(
state: ThermometerContract.State, state: ThermometerContract.State,
event: ThermometerContract.Event.OnPowerChanged event: ThermometerContract.Event.OnPowerChanged
) { ) {
if(state is ThermometerContract.State.Display) { if(state is ThermometerContract.State.Display) {
state.thermometer.state.tx = event.tx state.thermometer.state.tx = event.tx
} }
setEffect {
ThermometerContract.Effect.HidePowerPicker
}
} }
private fun reduce( private fun reduce(
@ -138,19 +182,14 @@ class ThermometerViewModel @Inject constructor(
event: ThermometerContract.Event.OnShowTemperatureHistory event: ThermometerContract.Event.OnShowTemperatureHistory
) { ) {
setEffect { if(state is ThermometerContract.State.Display) {
ThermometerContract.Effect.ShowTemperatureHistory
}
} setEffect {
ThermometerContract.Effect.Navigation.ThermometerHistory(
state.origin.info.serial
)
}
private fun reduce(
state: ThermometerContract.State,
event: ThermometerContract.Event.OnHideTemperatureHistory
) {
setEffect {
ThermometerContract.Effect.HideTemperatureHistory
} }
} }
@ -286,7 +325,7 @@ class ThermometerViewModel @Inject constructor(
if(state is ThermometerContract.State.Display){ if(state is ThermometerContract.State.Display){
setEffect { 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.animation.animateContentSize
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -21,9 +21,7 @@ import androidx.compose.ui.unit.dp
import llc.arma.ble.R import llc.arma.ble.R
import llc.arma.ble.app.ui.common.PrimaryButton import llc.arma.ble.app.ui.common.PrimaryButton
import llc.arma.ble.app.ui.common.SecondaryButton 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.ShapeType
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.locale.localizedName import llc.arma.ble.app.ui.screen.locale.localizedName
@Composable @Composable
@ -52,6 +50,7 @@ fun Write(
state.writeRequest.tx?.let { state.writeRequest.tx?.let {
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Мощность", title = "Мощность",
subtitle = "${it.localizedName} db" subtitle = "${it.localizedName} db"
) )
@ -61,6 +60,7 @@ fun Write(
state.writeRequest.saveHistory?.let { state.writeRequest.saveHistory?.let {
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Сохранять историю измерений", title = "Сохранять историю измерений",
subtitle = it.localizedName subtitle = it.localizedName
) )
@ -73,6 +73,7 @@ fun Write(
val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60 val minutes = (it - ( hours * 1000 * 60 * 60 )) / 1000 / 60
BleMenuItem( BleMenuItem(
shapeType = ShapeType.Singleton,
title = "Интервал измерений", title = "Интервал измерений",
subtitle = "$hours ч. $minutes мин." 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