Filter log by device
This commit is contained in:
+13
-2
@@ -14,7 +14,7 @@ class LogRepository(
|
|||||||
private val dbQueries: AppDatabaseQueries
|
private val dbQueries: AppDatabaseQueries
|
||||||
) {
|
) {
|
||||||
fun addLog(title: String, content: String, deviceId: String? = null) {
|
fun addLog(title: String, content: String, deviceId: String? = null) {
|
||||||
val source = if (deviceId == null) LogFilter.System.name else LogFilter.Device.name
|
val source = if (deviceId == null) LogFilter.SYSTEM else LogFilter.DEVICE
|
||||||
dbQueries.insertLog(
|
dbQueries.insertLog(
|
||||||
title = title,
|
title = title,
|
||||||
content = content,
|
content = content,
|
||||||
@@ -40,13 +40,24 @@ class LogRepository(
|
|||||||
suspend fun getLogsByFilterPaged(filter: LogFilter, limit: Long, lastTimestamp: Long?, lastId: Long?): List<Log> {
|
suspend fun getLogsByFilterPaged(filter: LogFilter, limit: Long, lastTimestamp: Long?, lastId: Long?): List<Log> {
|
||||||
return when (filter) {
|
return when (filter) {
|
||||||
LogFilter.All -> getLogsPaged(limit, lastTimestamp, lastId)
|
LogFilter.All -> getLogsPaged(limit, lastTimestamp, lastId)
|
||||||
LogFilter.System, LogFilter.Device -> {
|
LogFilter.System -> {
|
||||||
if (lastTimestamp == null || lastId == null) {
|
if (lastTimestamp == null || lastId == null) {
|
||||||
dbQueries.getLogsByTypeFirstPage(filter.name, limit).executeAsList()
|
dbQueries.getLogsByTypeFirstPage(filter.name, limit).executeAsList()
|
||||||
} else {
|
} else {
|
||||||
dbQueries.getLogsByTypeNextPage(source = filter.name, lastTimestamp = lastTimestamp, lastId = lastId, limit = limit).executeAsList()
|
dbQueries.getLogsByTypeNextPage(source = filter.name, lastTimestamp = lastTimestamp, lastId = lastId, limit = limit).executeAsList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is LogFilter.Device -> {
|
||||||
|
if (filter.deviceId != null) {
|
||||||
|
getLogsByDevicePaged(filter.deviceId, limit, lastTimestamp, lastId)
|
||||||
|
} else {
|
||||||
|
if (lastTimestamp == null || lastId == null) {
|
||||||
|
dbQueries.getLogsByTypeFirstPage(filter.name, limit).executeAsList()
|
||||||
|
} else {
|
||||||
|
dbQueries.getLogsByTypeNextPage(source = filter.name, lastTimestamp = lastTimestamp, lastId = lastId, limit = limit).executeAsList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+16
-5
@@ -1,13 +1,24 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.domain.model
|
package com.digitoolsolutions.app.torquevaultkmp.domain.model
|
||||||
|
|
||||||
enum class LogFilter {
|
sealed class LogFilter(val name: String) {
|
||||||
All,
|
data object All : LogFilter(ALL)
|
||||||
System,
|
data object System : LogFilter(SYSTEM)
|
||||||
Device;
|
data class Device(val deviceId: String? = null, val deviceName: String? = null) : LogFilter(DEVICE)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
const val ALL = "All"
|
||||||
|
const val SYSTEM = "System"
|
||||||
|
const val DEVICE = "Device"
|
||||||
|
|
||||||
|
val entries = listOf(All, System, Device())
|
||||||
|
|
||||||
fun fromString(value: String): LogFilter {
|
fun fromString(value: String): LogFilter {
|
||||||
return entries.find { it.name == value } ?: All
|
return when (value) {
|
||||||
|
ALL -> All
|
||||||
|
SYSTEM -> System
|
||||||
|
DEVICE -> Device()
|
||||||
|
else -> All
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -71,7 +71,8 @@ class BleManager {
|
|||||||
}
|
}
|
||||||
suspend fun sendCommand(peripheral: Peripheral, command: String) {
|
suspend fun sendCommand(peripheral: Peripheral, command: String) {
|
||||||
Napier.d(">>>>> Sent to uart: $command")
|
Napier.d(">>>>> Sent to uart: $command")
|
||||||
peripheral.write(RX_CHAR, command.encodeToByteArray())
|
val cmd = command.replace("\\r\\n", "\r\n")
|
||||||
|
peripheral.write(RX_CHAR, cmd.encodeToByteArray())
|
||||||
}
|
}
|
||||||
fun observeRx(peripheral: Peripheral): Flow<String> =
|
fun observeRx(peripheral: Peripheral): Flow<String> =
|
||||||
peripheral.observe(TX_CHAR).map { it.decodeToString() }
|
peripheral.observe(TX_CHAR).map { it.decodeToString() }
|
||||||
|
|||||||
+9
-3
@@ -127,7 +127,9 @@ fun MainScreen(
|
|||||||
) {
|
) {
|
||||||
composable(AuthDestination.route) { LoginScreen(navController) }
|
composable(AuthDestination.route) { LoginScreen(navController) }
|
||||||
composable(HomeDestination.route) {
|
composable(HomeDestination.route) {
|
||||||
HomeScreen(navController)
|
HomeScreen(onDeviceClicked = {device ->
|
||||||
|
navController.navigate(CommunicationDestination.createRoute(device.identifier.toString(), cancellable = false))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
composable(BondedDestination.route) {
|
composable(BondedDestination.route) {
|
||||||
BondedScreen()
|
BondedScreen()
|
||||||
@@ -139,10 +141,14 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
composable(
|
composable(
|
||||||
route = CommunicationDestination.route,
|
route = CommunicationDestination.route,
|
||||||
arguments = listOf(navArgument("deviceId") { type = NavType.StringType })
|
arguments = listOf(
|
||||||
|
navArgument("deviceId") { type = NavType.StringType },
|
||||||
|
navArgument("cancellable") { type = NavType.BoolType; defaultValue = true }
|
||||||
|
)
|
||||||
) { backStackEntry ->
|
) { backStackEntry ->
|
||||||
val deviceId = backStackEntry.savedStateHandle.get<String>("deviceId") ?: ""
|
val deviceId = backStackEntry.savedStateHandle.get<String>("deviceId") ?: ""
|
||||||
UartCommunicationScreen(navController, deviceId)
|
val cancellable = backStackEntry.savedStateHandle.get<Boolean>("cancellable") ?: true
|
||||||
|
UartCommunicationScreen(navController, deviceId, cancellable)
|
||||||
}
|
}
|
||||||
composable(LogDestination.route) { LogScreen() }
|
composable(LogDestination.route) { LogScreen() }
|
||||||
composable(SettingDestination.route) {
|
composable(SettingDestination.route) {
|
||||||
|
|||||||
-1
@@ -1,6 +1,5 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view
|
package com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view
|
||||||
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
|||||||
+2
-2
@@ -8,7 +8,7 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.Destinations
|
|||||||
object CommunicationDestination: Destinations(
|
object CommunicationDestination: Destinations(
|
||||||
label = "Communication",
|
label = "Communication",
|
||||||
icon = AppIcon.Vector(Icons.Outlined.Bluetooth),
|
icon = AppIcon.Vector(Icons.Outlined.Bluetooth),
|
||||||
route = "communication/{deviceId}"
|
route = "communication/{deviceId}?cancellable={cancellable}"
|
||||||
) {
|
) {
|
||||||
fun createRoute(deviceId: String) = "communication/$deviceId"
|
fun createRoute(deviceId: String, cancellable: Boolean = true) = "communication/$deviceId?cancellable=$cancellable"
|
||||||
}
|
}
|
||||||
+30
-8
@@ -1,11 +1,17 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.screens.communication
|
package com.digitoolsolutions.app.torquevaultkmp.screens.communication
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.DeleteForever
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
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.Color
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||||
@@ -17,7 +23,6 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
|||||||
import com.juul.kable.State
|
import com.juul.kable.State
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.action_cancel
|
import torquevaultkmp.composeapp.generated.resources.action_cancel
|
||||||
import torquevaultkmp.composeapp.generated.resources.action_retry
|
import torquevaultkmp.composeapp.generated.resources.action_retry
|
||||||
@@ -27,14 +32,16 @@ import torquevaultkmp.composeapp.generated.resources.action_retry
|
|||||||
fun UartCommunicationScreen(
|
fun UartCommunicationScreen(
|
||||||
navController: NavController,
|
navController: NavController,
|
||||||
deviceId: String,
|
deviceId: String,
|
||||||
|
cancellable: Boolean = true,
|
||||||
viewModel: ScannerViewModel = koinInject()
|
viewModel: ScannerViewModel = koinInject()
|
||||||
) {
|
) {
|
||||||
val messages by viewModel.rxMessages.collectAsState()
|
val focusManager = LocalFocusManager.current
|
||||||
|
val histories by viewModel.historyFlow(deviceId).collectAsState(initial = emptyList())
|
||||||
val connectedDevices by viewModel.connectedDevices.collectAsState()
|
val connectedDevices by viewModel.connectedDevices.collectAsState()
|
||||||
val connectionReasons by viewModel.connectionReasons.collectAsState()
|
val connectionReasons by viewModel.connectionReasons.collectAsState()
|
||||||
|
|
||||||
val connectionState = remember(connectedDevices, deviceId) {
|
val connectionState = remember(connectedDevices, deviceId) {
|
||||||
connectedDevices[deviceId] ?: State.Disconnected()
|
connectedDevices[deviceId]?.state ?: State.Disconnected()
|
||||||
}
|
}
|
||||||
val reason = remember(connectionReasons, deviceId) {
|
val reason = remember(connectionReasons, deviceId) {
|
||||||
connectionReasons[deviceId] ?: ConnectionReason.LINK_LOSS
|
connectionReasons[deviceId] ?: ConnectionReason.LINK_LOSS
|
||||||
@@ -42,8 +49,8 @@ fun UartCommunicationScreen(
|
|||||||
val deviceName = remember(connectedDevices) { viewModel.getDeviceName(deviceId) }
|
val deviceName = remember(connectedDevices) { viewModel.getDeviceName(deviceId) }
|
||||||
|
|
||||||
LaunchedEffect(deviceId) {
|
LaunchedEffect(deviceId) {
|
||||||
viewModel.clearMessages()
|
// viewModel.clearMessages(deviceId)
|
||||||
viewModel.connectById(deviceId)
|
if(cancellable) viewModel.connectById(deviceId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -51,16 +58,31 @@ fun UartCommunicationScreen(
|
|||||||
AppBar(
|
AppBar(
|
||||||
title = { Text(deviceName) },
|
title = { Text(deviceName) },
|
||||||
onNavigationButtonClick = {
|
onNavigationButtonClick = {
|
||||||
viewModel.disconnect(deviceId)
|
// if(cancellable) viewModel.disconnect(deviceId)
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
},
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = { viewModel.clearMessages(deviceId) }) {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.DeleteForever,
|
||||||
|
contentDescription = "Clear history",
|
||||||
|
tint = Color.Red
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(onTap = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.padding(padding)
|
||||||
|
.imePadding(),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
when (connectionState) {
|
when (connectionState) {
|
||||||
@@ -90,7 +112,7 @@ fun UartCommunicationScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
is State.Connected -> {
|
is State.Connected -> {
|
||||||
DeviceConnected(messages = messages, onSend = { msg ->
|
DeviceConnected(histories = histories, onSend = { msg ->
|
||||||
viewModel.sendRawCommand(deviceId, msg)
|
viewModel.sendRawCommand(deviceId, msg)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -30,7 +30,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DeviceConnected(
|
internal fun DeviceConnected(
|
||||||
messages: List<String>,
|
histories: List<String>,
|
||||||
onSend: (msg: String) -> Unit
|
onSend: (msg: String) -> Unit
|
||||||
) {
|
) {
|
||||||
var txInput by rememberSaveable { mutableStateOf("") }
|
var txInput by rememberSaveable { mutableStateOf("") }
|
||||||
@@ -51,14 +51,14 @@ internal fun DeviceConnected(
|
|||||||
.border(1.dp, Color.Gray, RoundedCornerShape(4.dp))
|
.border(1.dp, Color.Gray, RoundedCornerShape(4.dp))
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
) {
|
) {
|
||||||
items(messages) { message ->
|
items(histories) { history ->
|
||||||
Text(message)
|
Text(history)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(messages.size) {
|
LaunchedEffect(histories.size) {
|
||||||
if (messages.isNotEmpty()) {
|
if (histories.isNotEmpty()) {
|
||||||
listState.animateScrollToItem(messages.size - 1)
|
listState.animateScrollToItem(histories.size - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-57
@@ -1,82 +1,43 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.screens.home
|
package com.digitoolsolutions.app.torquevaultkmp.screens.home
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Bluetooth
|
|
||||||
import androidx.compose.material.icons.outlined.SyncAlt
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedCard
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.SwipeToDismissBox
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
|
||||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
//import androidx.compose.ui.platform.LocalContext
|
|
||||||
//import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.auth.AuthDestination
|
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.views.HomeEmpty
|
import com.digitoolsolutions.app.torquevaultkmp.screens.home.views.HomeEmpty
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.screens.home.views.HomeItem
|
import com.digitoolsolutions.app.torquevaultkmp.screens.home.views.HomeItem
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.utils.ForceLogoutException
|
import com.digitoolsolutions.app.torquevaultkmp.screens.scanner.ScannerViewModel
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import com.juul.kable.Advertisement
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.home_title
|
import torquevaultkmp.composeapp.generated.resources.home_title
|
||||||
//import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|
||||||
//import androidx.navigation.NavController
|
|
||||||
//import com.digitoolsolutions.android.components.ui.AppBar
|
|
||||||
//import com.digitoolsolutions.android.components.ui.CircularIcon
|
|
||||||
//import com.digitoolsolutions.android.home.view.HomeEmpty
|
|
||||||
//import com.digitoolsolutions.android.home.view.HomeItem
|
|
||||||
//import com.digitoolsolutions.android.scanner.viewmodel.ScannerViewModel
|
|
||||||
//import com.digitoolsolutions.android.uart.spec.ConnectionState
|
|
||||||
//import com.digitoolsolutions.android.uart.spec.UartDevice
|
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalUuidApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen(
|
fun HomeScreen(
|
||||||
navController: NavController,
|
onDeviceClicked: (Advertisement) -> Unit,
|
||||||
// bleScannerViewModel: ScannerViewModel
|
viewModel: HomeViewModel = koinViewModel(),
|
||||||
|
scannerViewModel: ScannerViewModel = koinInject()
|
||||||
) {
|
) {
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
// val connectedDevice by bleScannerViewModel.connectedDevices.collectAsState(initial = emptyList())
|
val connectedDevices by scannerViewModel.connectedDevices.collectAsState()
|
||||||
val viewModel = koinViewModel<HomeViewModel>()
|
|
||||||
val workOrders by viewModel.workOrders.collectAsState()
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
viewModel.loadWorkOrders()
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier.imePadding(),
|
modifier = Modifier.imePadding(),
|
||||||
@@ -94,23 +55,17 @@ fun HomeScreen(
|
|||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
) {
|
) {
|
||||||
if (workOrders.isEmpty())
|
if (connectedDevices.isEmpty())
|
||||||
HomeEmpty()
|
HomeEmpty()
|
||||||
else
|
else
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(workOrders) { workOrder ->
|
connectedDevices.forEach { (_, connectedDevice) ->
|
||||||
HomeItem(workOrder)
|
val (device, connectionState) = connectedDevice
|
||||||
|
item {
|
||||||
|
HomeItem(device, connectionState, onDeviceClicked = {onDeviceClicked(device)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun HomeScreenPreview() {
|
|
||||||
// HomeScreen(
|
|
||||||
// navController = NavController(LocalContext.current),
|
|
||||||
//// bleScannerViewModel = hiltViewModel()
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
|
|||||||
+59
-46
@@ -1,14 +1,16 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.screens.home.views
|
package com.digitoolsolutions.app.torquevaultkmp.screens.home.views
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.SyncAlt
|
import androidx.compose.material.icons.filled.Bluetooth
|
||||||
|
import androidx.compose.material.icons.rounded.SyncAlt
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedCard
|
import androidx.compose.material3.OutlinedCard
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -16,60 +18,71 @@ import androidx.compose.runtime.Composable
|
|||||||
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.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.juul.kable.Advertisement
|
||||||
|
import com.juul.kable.State
|
||||||
//import com.digitoolsolutions.android.components.ui.CircularIcon
|
|
||||||
//import com.digitoolsolutions.android.uart.spec.ConnectionState
|
|
||||||
//import com.digitoolsolutions.android.uart.spec.UartDevice
|
|
||||||
|
|
||||||
|
|
||||||
|
sealed class Status(val color: Color, val text: String) {
|
||||||
|
object Disconnected : Status(Color.Red, "Disconnected")
|
||||||
|
object Connecting : Status(Color.Gray, "Connecting")
|
||||||
|
object Connected : Status(Color(0xFF388E3C), "Connected")
|
||||||
|
object Disconnecting : Status(Color.Yellow, "Disconnecting")
|
||||||
|
}
|
||||||
@Composable
|
@Composable
|
||||||
//fun HomeItem(device: UartDevice, modifier: Modifier = Modifier) {
|
fun HomeItem(device: Advertisement, connectionState: State?, onDeviceClicked: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
fun HomeItem(device: WorkOrder, modifier: Modifier = Modifier) {
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = modifier.fillMaxWidth().padding(vertical = 4.dp),
|
modifier = modifier.fillMaxWidth().padding(vertical = 4.dp).clickable {
|
||||||
|
onDeviceClicked()
|
||||||
|
},
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||||
) {
|
) {
|
||||||
|
val status = when (connectionState) {
|
||||||
|
is State.Disconnected -> Status.Disconnected
|
||||||
|
is State.Connecting -> Status.Connecting
|
||||||
|
is State.Connected -> Status.Connected
|
||||||
|
is State.Disconnecting -> Status.Disconnecting
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
ListItem(
|
||||||
|
headlineContent = {
|
||||||
|
Text(
|
||||||
|
text = device.name ?: "No name",
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = "${device.identifier}",
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
leadingContent = {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
CircularIcon(Icons.Outlined.SyncAlt)
|
CircularIcon(
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
imageVector = if(connectionState is State.Connected) Icons.Rounded.SyncAlt else Icons.Default.Bluetooth,
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
backgroundColor = if (connectionState is State.Connected) Color(0xFF0082FC) else MaterialTheme.colorScheme.secondaryContainer,
|
||||||
// device.name?.let {
|
iconTint = if (connectionState is State.Connected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
// Text(
|
)
|
||||||
// text = it,
|
|
||||||
// style = MaterialTheme.typography.titleMedium
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// Text(
|
|
||||||
// text = device.identifier,
|
|
||||||
// style = MaterialTheme.typography.bodyMedium,
|
|
||||||
// color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
// )
|
|
||||||
Text(text=device.id)
|
|
||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
},
|
||||||
// val statusColor = when (device.status) {
|
trailingContent = {
|
||||||
// ConnectionState.CONNECTING -> MaterialTheme.colorScheme.onSurfaceVariant
|
if (connectionState is State.Connecting) {
|
||||||
// ConnectionState.READY -> Color.Green
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 3.dp, color = Color.Blue)
|
||||||
// else -> MaterialTheme.colorScheme.error
|
} else {
|
||||||
// }
|
if(status != null)
|
||||||
// Text(
|
Text(
|
||||||
// text = device.status.name,
|
text = status.text,
|
||||||
// style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
// color = statusColor
|
color = status.color
|
||||||
// )
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
|
||||||
@Composable
|
|
||||||
private fun ItemPreview() {
|
|
||||||
// HomeItem(device = UartDevice("AA:BB:CC:DD:EE", "Device 1"))
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ class LogViewModel(
|
|||||||
private val _logs = MutableStateFlow<List<Log>>(emptyList())
|
private val _logs = MutableStateFlow<List<Log>>(emptyList())
|
||||||
val logs: StateFlow<List<Log>> = _logs.asStateFlow()
|
val logs: StateFlow<List<Log>> = _logs.asStateFlow()
|
||||||
|
|
||||||
private val _activeFilter = MutableStateFlow(LogFilter.All)
|
private val _activeFilter = MutableStateFlow<LogFilter>(LogFilter.All)
|
||||||
val activeFilter: StateFlow<LogFilter> = _activeFilter.asStateFlow()
|
val activeFilter: StateFlow<LogFilter> = _activeFilter.asStateFlow()
|
||||||
|
|
||||||
private var lastTimestamp: Long? = null
|
private var lastTimestamp: Long? = null
|
||||||
|
|||||||
+124
-2
@@ -8,27 +8,45 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
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.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
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.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
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.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter
|
||||||
|
import com.digitoolsolutions.app.torquevaultkmp.utils.Helper
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
|
import org.koin.compose.koinInject
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.clear
|
import torquevaultkmp.composeapp.generated.resources.clear
|
||||||
|
|
||||||
@@ -59,7 +77,9 @@ fun FilterBottomSheet(
|
|||||||
activeFilter = activeFilter,
|
activeFilter = activeFilter,
|
||||||
onFilterSelected = {
|
onFilterSelected = {
|
||||||
onFilterSelected(it)
|
onFilterSelected(it)
|
||||||
|
if (it !is LogFilter.Device || it.deviceId != null) {
|
||||||
onDismissRequest()
|
onDismissRequest()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onClear = {
|
onClear = {
|
||||||
onFilterSelected(LogFilter.All)
|
onFilterSelected(LogFilter.All)
|
||||||
@@ -87,11 +107,18 @@ internal fun FilterContent(
|
|||||||
)
|
)
|
||||||
|
|
||||||
LogFilter.entries.forEach { filter ->
|
LogFilter.entries.forEach { filter ->
|
||||||
|
val isSelected = if (filter is LogFilter.Device) {
|
||||||
|
activeFilter is LogFilter.Device
|
||||||
|
} else {
|
||||||
|
activeFilter == filter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.selectable(
|
.selectable(
|
||||||
selected = activeFilter == filter,
|
selected = isSelected,
|
||||||
onClick = { onFilterSelected(filter) },
|
onClick = { onFilterSelected(filter) },
|
||||||
role = Role.RadioButton
|
role = Role.RadioButton
|
||||||
)
|
)
|
||||||
@@ -99,7 +126,7 @@ internal fun FilterContent(
|
|||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
RadioButton(
|
RadioButton(
|
||||||
selected = activeFilter == filter,
|
selected = isSelected,
|
||||||
onClick = { onFilterSelected(filter) }
|
onClick = { onFilterSelected(filter) }
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
@@ -108,6 +135,101 @@ internal fun FilterContent(
|
|||||||
modifier = Modifier.padding(start = 8.dp)
|
modifier = Modifier.padding(start = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filter is LogFilter.Device && isSelected) {
|
||||||
|
DeviceSelector(
|
||||||
|
selectedDeviceName = if((activeFilter as? LogFilter.Device)?.deviceName != null)
|
||||||
|
"${activeFilter.deviceName} - ${Helper.formatDeviceId(activeFilter.deviceId)}"
|
||||||
|
else "Select device",
|
||||||
|
onDeviceSelected = { device ->
|
||||||
|
onFilterSelected(LogFilter.Device(device.id, device.name))
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DeviceSelector(
|
||||||
|
selectedDeviceName: String,
|
||||||
|
onDeviceSelected: (Device) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val deviceRepository = koinInject<DeviceRepository>()
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
var devices by remember { mutableStateOf(emptyList<Device>()) }
|
||||||
|
var isLoading by remember { mutableStateOf(false) }
|
||||||
|
var isLastPage by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val loadMore = {
|
||||||
|
if (!isLoading && !isLastPage) {
|
||||||
|
scope.launch {
|
||||||
|
isLoading = true
|
||||||
|
val lastId = devices.lastOrNull()?.id
|
||||||
|
val newDevices = deviceRepository.getDevicesPaged(lastId = lastId)
|
||||||
|
if (newDevices.size < DeviceRepository.PAGE_SIZE) {
|
||||||
|
isLastPage = true
|
||||||
|
}
|
||||||
|
devices = devices + newDevices
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (devices.isEmpty()) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExposedDropdownMenuBox(
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandedChange = { expanded = !expanded },
|
||||||
|
modifier = modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = selectedDeviceName,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text("Device") },
|
||||||
|
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||||
|
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||||
|
modifier = Modifier.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable, true).fillMaxWidth()
|
||||||
|
)
|
||||||
|
|
||||||
|
ExposedDropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false }
|
||||||
|
) {
|
||||||
|
devices.forEachIndexed { index, device ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("${device.name} - ${device.id.takeLast(12)}") },
|
||||||
|
onClick = {
|
||||||
|
onDeviceSelected(device)
|
||||||
|
expanded = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (index == devices.size - 1 && !isLastPage) {
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isLoading) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ fun LogItem(log: Log, modifier: Modifier = Modifier) {
|
|||||||
text = log.title,
|
text = log.title,
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
val sourceText = if (log.source == LogFilter.Device.name) {
|
val sourceText = if (log.source == LogFilter.DEVICE) {
|
||||||
log.deviceId ?: "Unknown Device"
|
log.deviceId ?: "Unknown Device"
|
||||||
} else {
|
} else {
|
||||||
log.source
|
log.source
|
||||||
|
|||||||
+6
-29
@@ -15,25 +15,19 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
|||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
import com.digitoolsolutions.app.torquevaultkmp.components.AppBar
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.RssiIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.RssiIcon
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.WarningView
|
import com.digitoolsolutions.app.torquevaultkmp.components.WarningView
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager
|
|
||||||
import com.juul.kable.Advertisement
|
import com.juul.kable.Advertisement
|
||||||
import com.juul.kable.State
|
import com.juul.kable.State
|
||||||
import org.jetbrains.compose.resources.painterResource
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
|
||||||
import torquevaultkmp.composeapp.generated.resources.Res
|
import torquevaultkmp.composeapp.generated.resources.Res
|
||||||
import torquevaultkmp.composeapp.generated.resources.baseline_filter_list_24
|
|
||||||
import torquevaultkmp.composeapp.generated.resources.baseline_warning_24
|
|
||||||
import torquevaultkmp.composeapp.generated.resources.filter_title
|
|
||||||
import torquevaultkmp.composeapp.generated.resources.no_device_guide_info
|
import torquevaultkmp.composeapp.generated.resources.no_device_guide_info
|
||||||
import torquevaultkmp.composeapp.generated.resources.scan_empty_title
|
import torquevaultkmp.composeapp.generated.resources.scan_empty_title
|
||||||
import torquevaultkmp.composeapp.generated.resources.scanner_title
|
import torquevaultkmp.composeapp.generated.resources.scanner_title
|
||||||
@@ -88,7 +82,7 @@ fun ScannerScreen(
|
|||||||
title = stringResource(Res.string.scan_empty_title),
|
title = stringResource(Res.string.scan_empty_title),
|
||||||
hint = stringResource(Res.string.no_device_guide_info),
|
hint = stringResource(Res.string.no_device_guide_info),
|
||||||
hintTextAlign = TextAlign.Justify,
|
hintTextAlign = TextAlign.Justify,
|
||||||
){}
|
)
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -97,8 +91,8 @@ fun ScannerScreen(
|
|||||||
) {
|
) {
|
||||||
items(devices) { device ->
|
items(devices) { device ->
|
||||||
val deviceId = device.identifier.toString()
|
val deviceId = device.identifier.toString()
|
||||||
val connectionState = connectedDevices[deviceId]
|
val connectedDevice = connectedDevices[deviceId]
|
||||||
|
val connectionState = connectedDevice?.state
|
||||||
DeviceListItem(
|
DeviceListItem(
|
||||||
device = device,
|
device = device,
|
||||||
connectionState = connectionState,
|
connectionState = connectionState,
|
||||||
@@ -132,14 +126,6 @@ fun DeviceListItem(
|
|||||||
connectionState: State?,
|
connectionState: State?,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val statusText = when (connectionState) {
|
|
||||||
is State.Connecting -> "Connecting..."
|
|
||||||
is State.Connected -> "Connected"
|
|
||||||
is State.Disconnecting -> "Disconnecting..."
|
|
||||||
is State.Disconnected -> "Disconnected"
|
|
||||||
null -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -160,20 +146,11 @@ fun DeviceListItem(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
supportingContent = {
|
supportingContent = {
|
||||||
Column {
|
|
||||||
Text(
|
Text(
|
||||||
text = device.identifier.toString(),
|
text = "${device.identifier}",
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
if (statusText.isNotEmpty()) {
|
|
||||||
Text(
|
|
||||||
text = statusText,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = if (connectionState is State.Connected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
CircularIcon(
|
CircularIcon(
|
||||||
@@ -184,7 +161,7 @@ fun DeviceListItem(
|
|||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
if (connectionState is State.Connecting) {
|
if (connectionState is State.Connecting) {
|
||||||
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 2.dp)
|
CircularProgressIndicator(modifier = Modifier.size(24.dp), strokeWidth = 3.dp, color = Color.Blue)
|
||||||
} else {
|
} else {
|
||||||
RssiIcon(rssi = device.rssi)
|
RssiIcon(rssi = device.rssi)
|
||||||
}
|
}
|
||||||
|
|||||||
+33
-14
@@ -9,7 +9,6 @@ import com.digitoolsolutions.app.torquevaultkmp.data.repository.LogRepository
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository
|
import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.load
|
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.observe
|
import com.digitoolsolutions.app.torquevaultkmp.data.storage.observe
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.ConnectionReason
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.ConnectionReason
|
||||||
import com.juul.kable.Advertisement
|
import com.juul.kable.Advertisement
|
||||||
@@ -18,7 +17,6 @@ import com.juul.kable.State
|
|||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.domain.model.computeWheelsNuts
|
import com.digitoolsolutions.app.torquevaultkmp.domain.model.computeWheelsNuts
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.utils.Helper
|
import com.digitoolsolutions.app.torquevaultkmp.utils.Helper
|
||||||
import com.juul.kable.ExperimentalApi
|
|
||||||
import io.github.aakira.napier.Napier
|
import io.github.aakira.napier.Napier
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -33,13 +31,20 @@ import kotlinx.coroutines.flow.catch
|
|||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withTimeout
|
import kotlinx.coroutines.withTimeout
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
import kotlin.time.TimeSource
|
import kotlin.time.TimeSource
|
||||||
import kotlin.uuid.ExperimentalUuidApi
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
data class ConnectedDevice(
|
||||||
|
val advertisement: Advertisement,
|
||||||
|
val state: State
|
||||||
|
)
|
||||||
@OptIn(ExperimentalUuidApi::class)
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
class ScannerViewModel(
|
class ScannerViewModel(
|
||||||
private val bleManager: BleManager,
|
private val bleManager: BleManager,
|
||||||
@@ -51,21 +56,19 @@ class ScannerViewModel(
|
|||||||
companion object {
|
companion object {
|
||||||
private val SCAN_TIMEOUT = 1.seconds
|
private val SCAN_TIMEOUT = 1.seconds
|
||||||
private val CONNECTION_TIMEOUT = 5.seconds
|
private val CONNECTION_TIMEOUT = 5.seconds
|
||||||
|
private const val MAX_LOG_ENTRIES = 50
|
||||||
}
|
}
|
||||||
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
||||||
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
||||||
|
|
||||||
private val _isScanning = MutableStateFlow(false)
|
private val _isScanning = MutableStateFlow(false)
|
||||||
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()
|
||||||
private val _connectedDevices = MutableStateFlow<Map<String, State>>(emptyMap())
|
private val _connectedDevices = MutableStateFlow<Map<String, ConnectedDevice>>(emptyMap())
|
||||||
val connectedDevices: StateFlow<Map<String, State>> = _connectedDevices.asStateFlow()
|
val connectedDevices: StateFlow<Map<String, ConnectedDevice>> = _connectedDevices.asStateFlow()
|
||||||
|
|
||||||
private val _connectionReasons = MutableStateFlow<Map<String, ConnectionReason>>(emptyMap())
|
private val _connectionReasons = MutableStateFlow<Map<String, ConnectionReason>>(emptyMap())
|
||||||
val connectionReasons: StateFlow<Map<String, ConnectionReason>> = _connectionReasons.asStateFlow()
|
val connectionReasons: StateFlow<Map<String, ConnectionReason>> = _connectionReasons.asStateFlow()
|
||||||
|
private val _rxHistories = MutableStateFlow<Map<String,List<String>>>(emptyMap())
|
||||||
private val _rxMessages = MutableStateFlow<List<String>>(emptyList())
|
|
||||||
val rxMessages: StateFlow<List<String>> = _rxMessages.asStateFlow()
|
|
||||||
|
|
||||||
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
|
private val _workOrders = MutableStateFlow<List<WorkOrder>>(emptyList())
|
||||||
private val _autoConnect: StateFlow<Boolean> = storage.observe<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT)
|
private val _autoConnect: StateFlow<Boolean> = storage.observe<Boolean>(ReferKeys.DEVICE_AUTO_CONNECT)
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
.stateIn(viewModelScope, SharingStarted.Eagerly, false)
|
||||||
@@ -175,8 +178,23 @@ class ScannerViewModel(
|
|||||||
fun getPeripheral(identifier: String): Peripheral? {
|
fun getPeripheral(identifier: String): Peripheral? {
|
||||||
return bleManager.getPeripheral(identifier)
|
return bleManager.getPeripheral(identifier)
|
||||||
}
|
}
|
||||||
fun clearMessages() {
|
|
||||||
_rxMessages.value = emptyList()
|
fun historyFlow(deviceId: String): Flow<List<String>> =
|
||||||
|
_rxHistories.map { it[deviceId] ?: emptyList() }
|
||||||
|
fun clearMessages(deviceId: String? = null) {
|
||||||
|
if (deviceId == null) {
|
||||||
|
_rxHistories.value = emptyMap()
|
||||||
|
} else {
|
||||||
|
_rxHistories.update { it - deviceId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendHistory(deviceId: String, message: String) {
|
||||||
|
_rxHistories.update { currentMap ->
|
||||||
|
val currentLogs = currentMap[deviceId] ?: emptyList()
|
||||||
|
val newLogs = (currentLogs + message).takeLast(MAX_LOG_ENTRIES)
|
||||||
|
currentMap + (deviceId to newLogs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendRawCommand(deviceIdentifier: String, command: String) {
|
fun sendRawCommand(deviceIdentifier: String, command: String) {
|
||||||
@@ -187,10 +205,11 @@ class ScannerViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendCommand(peripheral: Peripheral, command: String) {
|
fun sendCommand(peripheral: Peripheral, command: String) {
|
||||||
|
val deviceId = peripheral.identifier.toString()
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
|
bleManager.sendCommand(peripheral, Helper.buildUartCommand(command))
|
||||||
_rxMessages.value += "Sent: $command"
|
appendHistory(deviceId, "Sent: $command")
|
||||||
logRepository.addLog(title = "Communicate - Sent", content = command, peripheral.identifier.toString())
|
logRepository.addLog(title = "Communicate - Sent", content = command, deviceId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +226,7 @@ class ScannerViewModel(
|
|||||||
var hasEmitted = false
|
var hasEmitted = false
|
||||||
launch {
|
launch {
|
||||||
peripheral.state.collect { state ->
|
peripheral.state.collect { state ->
|
||||||
_connectedDevices.value += (deviceIdentifier to state)
|
_connectedDevices.value += (deviceIdentifier to ConnectedDevice(adv, state))
|
||||||
if (state is State.Connected) {
|
if (state is State.Connected) {
|
||||||
/** Add bonded device */
|
/** Add bonded device */
|
||||||
addBondedDevice(deviceIdentifier, adv.name ?: "No name")
|
addBondedDevice(deviceIdentifier, adv.name ?: "No name")
|
||||||
@@ -293,7 +312,7 @@ class ScannerViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
|
private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) {
|
||||||
_rxMessages.value += "Received: $msg"
|
appendHistory(deviceIdentifier, "Received: $msg")
|
||||||
logRepository.addLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier)
|
logRepository.addLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier)
|
||||||
val decoded = Helper.decodeRxData(msg) ?: return
|
val decoded = Helper.decodeRxData(msg) ?: return
|
||||||
val data = decoded.data
|
val data = decoded.data
|
||||||
|
|||||||
+14
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.digitoolsolutions.app.torquevaultkmp.screens.settings
|
package com.digitoolsolutions.app.torquevaultkmp.screens.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
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
|
||||||
@@ -27,6 +28,8 @@ 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.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -94,7 +97,7 @@ fun SettingContent(
|
|||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
onChangeAutoConnect: (Boolean) -> Unit
|
onChangeAutoConnect: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
// val context = LocalContext.current
|
val focusManager = LocalFocusManager.current
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
// remember the initial serverUrl to check if it has changed when saving
|
// remember the initial serverUrl to check if it has changed when saving
|
||||||
@@ -125,7 +128,16 @@ fun SettingContent(
|
|||||||
snackbarHost = { SnackbarHost(snackbarHostState) })
|
snackbarHost = { SnackbarHost(snackbarHostState) })
|
||||||
{ padding ->
|
{ padding ->
|
||||||
Column (
|
Column (
|
||||||
modifier = Modifier.fillMaxSize().imePadding().padding(padding).padding(16.dp),
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.imePadding()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(16.dp)
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(onTap = {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
})
|
||||||
|
},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
+8
@@ -52,4 +52,12 @@ object Helper {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
fun formatDeviceId(deviceId: String?): String {
|
||||||
|
if(deviceId == null) return ""
|
||||||
|
return if (deviceId.contains(":")) {
|
||||||
|
deviceId
|
||||||
|
} else {
|
||||||
|
deviceId.takeLast(12)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user