From 9dcdf68c633d0097063c9f1bbf0e9866cf7fe717 Mon Sep 17 00:00:00 2001 From: lamtruong28 Date: Fri, 5 Jun 2026 18:51:17 +0700 Subject: [PATCH] Add logs communicate and display on log screen --- .../data/repository/LogRepository.kt | 2 +- .../app/torquevaultkmp/di/Modules.kt | 2 +- .../app/torquevaultkmp/screens/MainScreen.kt | 4 +-- .../screens/bonded/BondedScreen.kt | 13 ++++----- .../screens/bonded/BondedViewModel.kt | 10 ------- .../torquevaultkmp/screens/logs/LogScreen.kt | 27 ++++++++++++++----- .../screens/logs/LogViewModel.kt | 15 +---------- .../screens/scanner/ScannerViewModel.kt | 16 ++++++++--- 8 files changed, 42 insertions(+), 47 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt index fef7ba6..2005a89 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/data/repository/LogRepository.kt @@ -13,7 +13,7 @@ import kotlin.time.Clock class LogRepository( private val dbQueries: AppDatabaseQueries ) { - suspend 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 dbQueries.insertLog( title = title, diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt index 3c772be..b735990 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/di/Modules.kt @@ -63,5 +63,5 @@ val viewModelModule = module { factoryOf(::SettingViewModel) factoryOf(::BondedViewModel) factoryOf(::LogViewModel) - single { ScannerViewModel(get(), get(), get(), get()) } + single { ScannerViewModel(get(), get(), get(), get(), get()) } } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt index a0336ee..61317ba 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/MainScreen.kt @@ -130,7 +130,7 @@ fun MainScreen( HomeScreen(navController) } composable(BondedDestination.route) { - BondedScreen(navController) + BondedScreen() } composable(ScannerDestination.route) { ScannerScreen(onDeviceClick = { device -> @@ -144,7 +144,7 @@ fun MainScreen( val deviceId = backStackEntry.savedStateHandle.get("deviceId") ?: "" UartCommunicationScreen(navController, deviceId) } - composable(LogDestination.route) { LogScreen(navController) } + composable(LogDestination.route) { LogScreen() } composable(SettingDestination.route) { SettingScreen(navController, isDarkTheme, onThemeToggle) } diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt index 8c42c17..3accba3 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedScreen.kt @@ -45,7 +45,6 @@ import torquevaultkmp.composeapp.generated.resources.bonded_title @OptIn(ExperimentalMaterial3Api::class) @Composable fun BondedScreen( - navController: NavController, viewModel: BondedViewModel = koinViewModel() ) { val devices by viewModel.bondedDevices.collectAsState() @@ -57,7 +56,6 @@ fun BondedScreen( BondedContent( devices = devices, isLoading = isLoading, - onAddFake = { viewModel.addFakeDevice() }, onDelete = { viewModel.removeDevice(it.id) }, onLoadMore = { viewModel.loadNextPage() }, onRefresh = { viewModel.refresh() } @@ -69,7 +67,6 @@ fun BondedScreen( internal fun BondedContent( devices: List, isLoading: Boolean, - onAddFake: () -> Unit, onDelete: (Device) -> Unit, onLoadMore: () -> Unit, onRefresh: () -> Unit @@ -101,11 +98,11 @@ internal fun BondedContent( title = {Text(stringResource(Res.string.bonded_title))}, ) }, - floatingActionButton = { - FloatingActionButton(onClick = onAddFake) { - Icon(Icons.Default.Add, contentDescription = "Add Fake Device") - } - }, +// floatingActionButton = { +// FloatingActionButton(onClick = {}) { +// Icon(Icons.Default.Add, contentDescription = "Add") +// } +// }, snackbarHost = { SnackbarHost(snackbarHostState) }) { padding -> val pullToRefreshState = rememberPullToRefreshState() diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt index 163c76b..0c5b4e9 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/bonded/BondedViewModel.kt @@ -55,16 +55,6 @@ class BondedViewModel( loadNextPage() } } - - fun addFakeDevice() { - viewModelScope.launch { - val id = "AA:BB:CC:DD:EE:${Random.nextInt(10, 99)}" - val name = "Torque Tool ${Random.nextInt(1, 100)}" - deviceRepository.saveDevice(id, name) - refresh() - } - } - fun removeDevice(id: String) { viewModelScope.launch { deviceRepository.deleteDevice(id) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt index 6523bd6..21f294e 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogScreen.kt @@ -9,9 +9,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.DeleteForever import androidx.compose.material.icons.outlined.FilterList +import androidx.compose.material.icons.rounded.KeyboardArrowUp import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api @@ -32,13 +32,13 @@ import androidx.compose.runtime.derivedStateOf 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.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import com.digitoolsolutions.app.torquevaultkmp.components.AppBar import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Log import com.digitoolsolutions.app.torquevaultkmp.domain.model.LogFilter @@ -47,6 +47,7 @@ import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.FilterBottomSh import com.digitoolsolutions.app.torquevaultkmp.screens.logs.view.LogItem import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.stringResource import org.koin.compose.viewmodel.koinViewModel import torquevaultkmp.composeapp.generated.resources.Res @@ -55,19 +56,20 @@ import torquevaultkmp.composeapp.generated.resources.log_title @OptIn(ExperimentalMaterial3Api::class) @Composable fun LogScreen( - navController: NavController, viewModel: LogViewModel = koinViewModel() ) { val logs by viewModel.logs.collectAsState() val activeFilter by viewModel.activeFilter.collectAsState() val isLoading by viewModel.isLoading.collectAsState() + LaunchedEffect(Unit) { + viewModel.refresh() + } LogContent( logs = logs, activeFilter = activeFilter, isLoading = isLoading, onFilterSelected = { viewModel.setFilter(it) }, - onAddFakeLog = { viewModel.addFakeLog() }, onDeleteAllLogs = { viewModel.clearAllLogs() }, onLoadMore = { viewModel.loadNextPage() }, onRefresh = { viewModel.refresh() } @@ -81,7 +83,6 @@ internal fun LogContent( activeFilter: LogFilter, isLoading: Boolean, onFilterSelected: (LogFilter) -> Unit, - onAddFakeLog: () -> Unit, onDeleteAllLogs: () -> Unit, onLoadMore: () -> Unit, onRefresh: () -> Unit @@ -91,6 +92,7 @@ internal fun LogContent( var showBottomSheet by remember { mutableStateOf(false) } val listState = rememberLazyListState() + val coroutineScope = rememberCoroutineScope() // Infinite Scroll logic val shouldLoadMore = remember { @@ -100,6 +102,12 @@ internal fun LogContent( } } + LaunchedEffect(logs) { + if (logs.isNotEmpty()) { + listState.scrollToItem(0) + } + } + LaunchedEffect(shouldLoadMore) { snapshotFlow { shouldLoadMore.value } .distinctUntilChanged() @@ -133,8 +141,13 @@ internal fun LogContent( ) }, floatingActionButton = { - FloatingActionButton(onClick = onAddFakeLog) { - Icon(Icons.Default.Add, contentDescription = "Add Fake Log") + FloatingActionButton(onClick = { + onRefresh() + coroutineScope.launch { + listState.animateScrollToItem(0) + } + }) { + Icon(Icons.Rounded.KeyboardArrowUp, contentDescription = "Go to top") } }, snackbarHost = { SnackbarHost(snackbarHostState) }) diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogViewModel.kt index 151876a..968522b 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/logs/LogViewModel.kt @@ -57,7 +57,7 @@ class LogViewModel( val lastLog = newLogs.last() lastTimestamp = lastLog.timestamp lastId = lastLog.id - _logs.value = _logs.value + newLogs + _logs.value += newLogs } isLoading.value = false @@ -73,19 +73,6 @@ class LogViewModel( loadNextPage() } } - - fun addFakeLog() { - viewModelScope.launch { - val titles = listOf("System Alert", "Device Connected", "Torque Applied", "Calibration Sync") - val contents = listOf("Voltage stable", "Tool #42 joined", "15.5 Nm recorded", "Sync successful") - logRepository.addLog( - title = titles.random(), - content = contents.random() - ) - refresh() - } - } - fun clearAllLogs() { viewModelScope.launch { logRepository.clearLogs() diff --git a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt index d669635..baf8128 100644 --- a/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/digitoolsolutions/app/torquevaultkmp/screens/scanner/ScannerViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.digitoolsolutions.app.torquevaultkmp.data.network.dto.SendMeasureDto import com.digitoolsolutions.app.torquevaultkmp.kable.BleManager import com.digitoolsolutions.app.torquevaultkmp.data.repository.DeviceRepository +import com.digitoolsolutions.app.torquevaultkmp.data.repository.LogRepository import com.digitoolsolutions.app.torquevaultkmp.data.repository.ScannerRepository import com.digitoolsolutions.app.torquevaultkmp.data.storage.AppStorage import com.digitoolsolutions.app.torquevaultkmp.data.storage.ReferKeys @@ -17,6 +18,7 @@ import com.juul.kable.State import com.digitoolsolutions.app.torquevaultkmp.domain.model.WorkOrder import com.digitoolsolutions.app.torquevaultkmp.domain.model.computeWheelsNuts import com.digitoolsolutions.app.torquevaultkmp.utils.Helper +import com.juul.kable.ExperimentalApi import io.github.aakira.napier.Napier import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -41,8 +43,9 @@ import kotlin.uuid.ExperimentalUuidApi @OptIn(ExperimentalUuidApi::class) class ScannerViewModel( private val bleManager: BleManager, - private val storage: AppStorage, + storage: AppStorage, private val deviceRepository: DeviceRepository, + private val logRepository: LogRepository, private val scannerRepository: ScannerRepository ) : ViewModel() { companion object { @@ -118,6 +121,7 @@ class ScannerViewModel( } } } + logRepository.addLog(title="Scanner status", content="Scanning ...") } private fun startCleanupJob() { @@ -181,17 +185,19 @@ class ScannerViewModel( sendCommand(peripheral, command) } } + fun sendCommand(peripheral: Peripheral, command: String) { viewModelScope.launch { bleManager.sendCommand(peripheral, Helper.buildUartCommand(command)) _rxMessages.value += "Sent: $command" + logRepository.addLog(title = "Communicate - Sent", content = command, peripheral.identifier.toString()) } } fun connect(adv: Advertisement) { val deviceIdentifier = adv.identifier.toString() if (connectionJobs[deviceIdentifier]?.isActive == true) return - + logRepository.addLog(title = "Connection - Status", deviceId = deviceIdentifier ,content = "Device [${adv.name}] is connecting...") val job = viewModelScope.launch { try { val peripheral = bleManager.connect(adv) @@ -206,6 +212,7 @@ class ScannerViewModel( /** Add bonded device */ addBondedDevice(deviceIdentifier, adv.name ?: "No name") _connectionReasons.value -= deviceIdentifier + logRepository.addLog(title = "Connection - Status", deviceId = deviceIdentifier ,content = "Device [${adv.name}] has been connected.") } if (state is State.Disconnected && hasEmitted) { if (_connectionReasons.value[deviceIdentifier] == null) { @@ -233,8 +240,10 @@ class ScannerViewModel( if (e is TimeoutCancellationException) { Napier.e(">>>>> Connection timed out for $deviceIdentifier") _connectionReasons.value += (deviceIdentifier to ConnectionReason.TIMEOUT) + logRepository.addLog(title = "Connection - Status", content = "Connect to device [${adv.name}] failed. ${e.message}") } else if (e !is CancellationException) { Napier.e(">>>>> Connection failed for $deviceIdentifier", e) + logRepository.addLog(title = "Connection - Status", content = "Connect to device [${adv.name}] failed. ${e.message}") } } finally { _connectedDevices.value -= deviceIdentifier @@ -284,13 +293,12 @@ class ScannerViewModel( } private fun handleMessage(peripheral: Peripheral, msg: String, deviceIdentifier: String) { _rxMessages.value += "Received: $msg" + logRepository.addLog(title = "Communicate - Received", content = msg, deviceId = deviceIdentifier) val decoded = Helper.decodeRxData(msg) ?: return val data = decoded.data val woId = decoded.id val cmd = data.take(4) val action = if (cmd == "back") "back" else "next" - Napier.d(">>>>> Received data from $deviceIdentifier: $msg", tag="ScannerViewModel.kt") - Napier.d(">>>>> Data: $data", tag="ScannerViewModel.kt") viewModelScope.launch { when (cmd) { "requ", "next", "back" -> fetchWorkOrders(peripheral, woId, action)