Filter log by device

This commit is contained in:
2026-06-12 21:14:11 +07:00
parent c462373762
commit 54db47dc45
17 changed files with 362 additions and 206 deletions
@@ -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()
}
}
}
} }
} }
@@ -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
}
} }
} }
} }
@@ -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() }
@@ -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,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
@@ -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"
} }
@@ -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)
}) })
} }
@@ -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)
} }
} }
@@ -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()
// )
} }
@@ -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"))
} }
@@ -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
@@ -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 = {}
)
}
} }
} }
} }
@@ -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
@@ -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)
} }
@@ -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
@@ -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,
@@ -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)
}
}
} }