diff --git a/.gitignore b/.gitignore index f5fdcca..4a0e8bf 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,5 @@ local.properties .*.bat *.jks *.keystore -*.base64 \ No newline at end of file +*.base64 +docs \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/BLEClient.kt b/app/src/main/java/org/noxylva/lbjconsole/BLEClient.kt index d7bad09..d1cbb38 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/BLEClient.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/BLEClient.kt @@ -45,6 +45,16 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { private var lastKnownDeviceAddress: String? = null private var connectionAttempts = 0 private var isReconnecting = false + private var highFrequencyReconnect = true + private var reconnectHandler = Handler(Looper.getMainLooper()) + private var reconnectRunnable: Runnable? = null + private var connectionLostCallback: (() -> Unit)? = null + private var connectionSuccessCallback: ((String) -> Unit)? = null + private var specifiedDeviceAddress: String? = null + private var targetDeviceAddress: String? = null + private var isDialogOpen = false + private var isManualDisconnect = false + private var isAutoConnectBlocked = false private val leScanCallback = object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { @@ -82,18 +92,25 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { scanCallback?.invoke(device) } - if (targetDeviceName != null && !isConnected && !isReconnecting) { - if (deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)) { - Log.i(TAG, "Found target device: $deviceName, auto-connecting") - lastKnownDeviceAddress = device.address - connectImmediately(device.address) + if (!isConnected && !isReconnecting && !isDialogOpen && !isAutoConnectBlocked) { + val deviceAddress = device.address + val isSpecifiedDevice = specifiedDeviceAddress == deviceAddress + val isTargetDevice = targetDeviceName != null && deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true) + val isKnownDevice = lastKnownDeviceAddress == deviceAddress + val isSpecificTargetAddress = targetDeviceAddress == deviceAddress + + if (isSpecificTargetAddress || isSpecifiedDevice || (specifiedDeviceAddress == null && isTargetDevice) || (specifiedDeviceAddress == null && isKnownDevice)) { + val priority = when { + isSpecificTargetAddress -> "specific target address" + isSpecifiedDevice -> "specified device" + isTargetDevice -> "target device name" + else -> "known device" + } + Log.i(TAG, "Found device ($priority): $deviceName, auto-connecting") + lastKnownDeviceAddress = deviceAddress + connectImmediately(deviceAddress) } } - - if (lastKnownDeviceAddress == device.address && !isConnected && !isReconnecting) { - Log.i(TAG, "Found known device, reconnecting immediately") - connectImmediately(device.address) - } } override fun onScanFailed(errorCode: Int) { @@ -279,7 +296,66 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { @SuppressLint("MissingPermission") fun disconnect() { - bluetoothGatt?.disconnect() + Log.d(TAG, "Manual disconnect initiated") + isConnected = false + isManualDisconnect = true + isAutoConnectBlocked = true + stopHighFrequencyReconnect() + stopScan() + + bluetoothGatt?.let { gatt -> + try { + gatt.disconnect() + Thread.sleep(100) + gatt.close() + } catch (e: Exception) { + Log.e(TAG, "Disconnect error: ${e.message}") + } + } + bluetoothGatt = null + + dataBuffer.clear() + connectionStateCallback = null + + Log.d(TAG, "Manual disconnect - auto connect blocked, deviceAddress preserved: $deviceAddress") + } + + @SuppressLint("MissingPermission") + fun connectManually(address: String, onConnectionStateChange: ((Boolean) -> Unit)? = null): Boolean { + Log.d(TAG, "Manual connection to device: $address") + + stopScan() + stopHighFrequencyReconnect() + + isManualDisconnect = false + isAutoConnectBlocked = false + autoReconnect = true + highFrequencyReconnect = true + return connect(address, onConnectionStateChange) + } + + @SuppressLint("MissingPermission") + fun closeManually() { + Log.d(TAG, "Manual close - will restore auto reconnect") + + isConnected = false + isManualDisconnect = false + isAutoConnectBlocked = false + bluetoothGatt?.let { gatt -> + try { + gatt.disconnect() + gatt.close() + } catch (e: Exception) { + Log.e(TAG, "Close error: ${e.message}") + } + } + bluetoothGatt = null + deviceAddress = null + + autoReconnect = true + highFrequencyReconnect = true + + Log.d(TAG, "Auto reconnect mechanism restored and GATT cleaned up") } @@ -389,10 +465,15 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { BluetoothProfile.STATE_CONNECTED -> { isConnected = true isReconnecting = false + isManualDisconnect = false connectionAttempts = 0 Log.i(TAG, "Connected to GATT server") handler.post { connectionStateCallback?.invoke(true) } + + deviceAddress?.let { address -> + handler.post { connectionSuccessCallback?.invoke(address) } + } handler.post { try { @@ -406,17 +487,20 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { BluetoothProfile.STATE_DISCONNECTED -> { isConnected = false isReconnecting = false - Log.i(TAG, "Disconnected from GATT server") + Log.i(TAG, "Disconnected from GATT server, manual=$isManualDisconnect") - handler.post { connectionStateCallback?.invoke(false) } - - - if (!deviceAddress.isNullOrBlank() && autoReconnect) { - handler.post { - Log.d(TAG, "Immediate reconnection after disconnect") - connect(deviceAddress!!, connectionStateCallback) + handler.post { + connectionStateCallback?.invoke(false) + if (!isManualDisconnect) { + connectionLostCallback?.invoke() } } + + if (!deviceAddress.isNullOrBlank() && autoReconnect && highFrequencyReconnect && !isManualDisconnect) { + startHighFrequencyReconnect(deviceAddress!!) + } else if (isManualDisconnect) { + Log.d(TAG, "Manual disconnect - no auto reconnect") + } } } } @@ -630,6 +714,86 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { Log.d(TAG, "Auto reconnect set to: $enabled") } + fun setHighFrequencyReconnect(enabled: Boolean) { + highFrequencyReconnect = enabled + if (!enabled) { + stopHighFrequencyReconnect() + } + Log.d(TAG, "High frequency reconnect set to: $enabled") + } + + fun setConnectionLostCallback(callback: (() -> Unit)?) { + connectionLostCallback = callback + } + + fun setConnectionSuccessCallback(callback: ((String) -> Unit)?) { + connectionSuccessCallback = callback + } + + fun setSpecifiedDeviceAddress(address: String?) { + specifiedDeviceAddress = address + Log.d(TAG, "Set specified device address: $address") + } + + fun getSpecifiedDeviceAddress(): String? = specifiedDeviceAddress + + fun setDialogOpen(isOpen: Boolean) { + isDialogOpen = isOpen + Log.d(TAG, "Dialog open state set to: $isOpen") + } + + fun setAutoConnectBlocked(blocked: Boolean) { + isAutoConnectBlocked = blocked + Log.d(TAG, "Auto connect blocked set to: $blocked") + } + + fun resetManualDisconnectState() { + isManualDisconnect = false + isAutoConnectBlocked = false + Log.d(TAG, "Manual disconnect state reset - auto reconnect enabled") + } + + fun setTargetDeviceAddress(address: String?) { + targetDeviceAddress = address + Log.d(TAG, "Set target device address: $address") + } + + fun getTargetDeviceAddress(): String? = targetDeviceAddress + + private fun startHighFrequencyReconnect(address: String) { + stopHighFrequencyReconnect() + + Log.d(TAG, "Starting high frequency reconnect for: $address") + + reconnectRunnable = Runnable { + if (!isConnected && autoReconnect && highFrequencyReconnect) { + Log.d(TAG, "High frequency reconnect attempt ${connectionAttempts + 1} for: $address") + connect(address, connectionStateCallback) + + if (!isConnected) { + val delay = when { + connectionAttempts < 10 -> 100L + connectionAttempts < 30 -> 200L + connectionAttempts < 60 -> 500L + else -> 1000L + } + + reconnectHandler.postDelayed(reconnectRunnable!!, delay) + } + } + } + + reconnectHandler.post(reconnectRunnable!!) + } + + private fun stopHighFrequencyReconnect() { + reconnectRunnable?.let { + reconnectHandler.removeCallbacks(it) + reconnectRunnable = null + Log.d(TAG, "Stopped high frequency reconnect") + } + } + fun getConnectionAttempts(): Int = connectionAttempts fun getLastKnownDeviceAddress(): String? = lastKnownDeviceAddress @@ -638,9 +802,16 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { fun disconnectAndCleanup() { isConnected = false autoReconnect = false + highFrequencyReconnect = false + isManualDisconnect = false + isAutoConnectBlocked = false + stopHighFrequencyReconnect() + stopScan() + bluetoothGatt?.let { gatt -> try { gatt.disconnect() + Thread.sleep(200) gatt.close() Log.d(TAG, "GATT connection cleaned up") } catch (e: Exception) { @@ -650,6 +821,14 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() { bluetoothGatt = null deviceAddress = null connectionAttempts = 0 + + dataBuffer.clear() + connectionStateCallback = null + statusCallback = null + trainInfoCallback = null + connectionLostCallback = null + connectionSuccessCallback = null + Log.d(TAG, "BLE client fully disconnected and cleaned up") } } \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt index 220b126..a9e03d7 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt @@ -24,12 +24,14 @@ import androidx.compose.animation.* import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.LocationOn @@ -55,7 +57,7 @@ import org.noxylva.lbjconsole.model.TrainRecord import org.noxylva.lbjconsole.model.TrainRecordManager import org.noxylva.lbjconsole.model.MergeSettings import org.noxylva.lbjconsole.ui.screens.HistoryScreen -import org.noxylva.lbjconsole.ui.screens.MergedHistoryScreen + import org.noxylva.lbjconsole.ui.screens.MapScreen import org.noxylva.lbjconsole.ui.screens.SettingsScreen @@ -74,7 +76,7 @@ class MainActivity : ComponentActivity() { private var deviceStatus by mutableStateOf("未连接") - private var deviceAddress by mutableStateOf("") + private var deviceAddress by mutableStateOf(null) private var isScanning by mutableStateOf(false) private var foundDevices by mutableStateOf(listOf()) private var scanResults = mutableListOf() @@ -82,7 +84,7 @@ class MainActivity : ComponentActivity() { private var showConnectionDialog by mutableStateOf(false) private var lastUpdateTime by mutableStateOf(null) private var latestRecord by mutableStateOf(null) - private var recentRecords by mutableStateOf>(emptyList()) + private var recentRecords = mutableStateListOf() private var filterTrain by mutableStateOf("") @@ -110,6 +112,9 @@ class MainActivity : ComponentActivity() { private var targetDeviceName = "LBJReceiver" + private var specifiedDeviceAddress by mutableStateOf(null) + private var searchOrderList by mutableStateOf(listOf()) + private var showDisconnectButton by mutableStateOf(false) private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) } @@ -204,6 +209,27 @@ class MainActivity : ComponentActivity() { handleTrainInfo(jsonData) } + bleClient.setHighFrequencyReconnect(true) + bleClient.setConnectionLostCallback { + runOnUiThread { + deviceStatus = "连接丢失,正在重连..." + showDisconnectButton = false + if (showConnectionDialog) { + foundDevices = emptyList() + startScan() + } + } + } + + bleClient.setConnectionSuccessCallback { address -> + runOnUiThread { + deviceAddress = address + deviceStatus = "已连接" + showDisconnectButton = true + Log.d(TAG, "Connection success callback: address=$address") + } + } + lifecycleScope.launch { try { @@ -227,8 +253,8 @@ class MainActivity : ComponentActivity() { osmdroidBasePath = osmCacheDir osmdroidTileCache = tileCache expirationOverrideDuration = 86400000L * 7 - tileDownloadThreads = 2 - tileFileSystemThreads = 2 + tileDownloadThreads = 4 + tileFileSystemThreads = 4 setUserAgentValue("LBJReceiver/1.0") } @@ -259,7 +285,24 @@ class MainActivity : ComponentActivity() { currentTab = tab saveSettings() }, - onConnectClick = { showConnectionDialog = true }, + onConnectClick = { + showConnectionDialog = true + }, + onDisconnectClick = { + bleClient.disconnectAndCleanup() + showDisconnectButton = false + deviceStatus = "已断开连接" + Log.d(TAG, "User disconnected device") + }, + showDisconnectButton = showDisconnectButton, + specifiedDeviceAddress = specifiedDeviceAddress, + searchOrderList = searchOrderList, + onSpecifiedDeviceSelected = { address -> + specifiedDeviceAddress = address + bleClient.setSpecifiedDeviceAddress(address) + saveSettings() + Log.d(TAG, "Set specified device address: $address") + }, latestRecord = latestRecord, @@ -270,7 +313,7 @@ class MainActivity : ComponentActivity() { Log.d(TAG, "Record clicked train=${record.train}") }, onClearMonitorLog = { - recentRecords = emptyList() + recentRecords.clear() temporaryStatusMessage = null }, @@ -337,7 +380,7 @@ class MainActivity : ComponentActivity() { onClearRecords = { scope.launch { trainRecordManager.clearRecords() - recentRecords = emptyList() + recentRecords.clear() latestRecord = null temporaryStatusMessage = null } @@ -367,12 +410,24 @@ class MainActivity : ComponentActivity() { ) if (showConnectionDialog) { + LaunchedEffect(showConnectionDialog) { + bleClient.setDialogOpen(true) + if (!bleClient.isConnected() && !isScanning) { + foundDevices = emptyList() + startScan() + } + } + ConnectionDialog( isScanning = isScanning, devices = foundDevices, onDismiss = { showConnectionDialog = false stopScan() + bleClient.resetManualDisconnectState() + if (!bleClient.isConnected()) { + startAutoScanAndConnect() + } }, onScan = { if (isScanning) { @@ -383,9 +438,25 @@ class MainActivity : ComponentActivity() { }, onConnect = { device -> showConnectionDialog = false - connectToDevice(device) - } + bleClient.setDialogOpen(false) + connectToDeviceManually(device) + }, + onDisconnect = { + bleClient.disconnect() + deviceStatus = "已断开连接" + deviceAddress = null + showDisconnectButton = false + Log.d(TAG, "Disconnected from device") + startScan() + }, + isConnected = bleClient.isConnected(), + targetDeviceName = settingsDeviceName, + deviceAddress = deviceAddress ) + } else { + LaunchedEffect(showConnectionDialog) { + bleClient.setDialogOpen(false) + } } @@ -396,6 +467,7 @@ class MainActivity : ComponentActivity() { private fun connectToDevice(device: BluetoothDevice) { + bleClient.setAutoConnectBlocked(false) deviceStatus = "正在连接..." Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}") @@ -412,9 +484,17 @@ class MainActivity : ComponentActivity() { if (connected) { deviceStatus = "已连接" temporaryStatusMessage = null + showDisconnectButton = true + + val newOrderList = listOf(device.address) + searchOrderList.filter { it != device.address } + searchOrderList = newOrderList.take(10) + saveSettings() + Log.d(TAG, "Updated search order list with: ${device.address}") + Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}") } else { deviceStatus = "连接失败,正在重试..." + showDisconnectButton = false Log.e(TAG, "Connection failed, auto-retry enabled for name=${device.name ?: "Unknown"}") } } @@ -423,6 +503,43 @@ class MainActivity : ComponentActivity() { deviceAddress = device.address } + private fun connectToDeviceManually(device: BluetoothDevice) { + bleClient.setAutoConnectBlocked(false) + deviceStatus = "正在连接..." + Log.d(TAG, "Manually connecting to device name=${device.name ?: "Unknown"} address=${device.address}") + + val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + val bluetoothAdapter = bluetoothManager.adapter + if (bluetoothAdapter == null || bluetoothAdapter.isEnabled != true) { + deviceStatus = "蓝牙未启用" + Log.e(TAG, "Bluetooth adapter unavailable or disabled") + return + } + + bleClient.connectManually(device.address) { connected -> + runOnUiThread { + if (connected) { + deviceStatus = "已连接" + temporaryStatusMessage = null + showDisconnectButton = true + + val newOrderList = listOf(device.address) + searchOrderList.filter { it != device.address } + searchOrderList = newOrderList.take(10) + saveSettings() + Log.d(TAG, "Updated search order list with: ${device.address}") + + Log.d(TAG, "Manually connected to device name=${device.name ?: "Unknown"}") + } else { + deviceStatus = "连接失败" + showDisconnectButton = false + Log.e(TAG, "Manual connection failed for name=${device.name ?: "Unknown"}") + } + } + } + + deviceAddress = device.address + } + private fun handleTrainInfo(jsonData: JSONObject) { Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...") @@ -445,10 +562,11 @@ class MainActivity : ComponentActivity() { latestRecord = record - val newList = mutableListOf() - newList.add(record) - newList.addAll(recentRecords.filterNot { it.train == record.train && it.time == record.time }) - recentRecords = newList.take(10) + recentRecords.removeAll { it.train == record.train && it.time == record.time } + recentRecords.add(0, record) + if (recentRecords.size > 10) { + recentRecords.removeRange(10, recentRecords.size) + } Log.d(TAG, "Updated UI train=${record.train}") forceUiRefresh() @@ -544,17 +662,20 @@ class MainActivity : ComponentActivity() { isScanning = true foundDevices = emptyList() + val targetDeviceName = if (settingsDeviceName.isNotBlank() && settingsDeviceName != "LBJReceiver") { settingsDeviceName } else { null } - Log.d(TAG, "Starting continuous BLE scan target=${targetDeviceName ?: "Any"} (settings=${settingsDeviceName})") + Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"} (settings=${settingsDeviceName})") bleClient.scanDevices(targetDeviceName) { device -> - if (!foundDevices.any { it.address == device.address }) { - Log.d(TAG, "Found device name=${device.name ?: "Unknown"} address=${device.address}") - foundDevices = foundDevices + device + runOnUiThread { + if (!foundDevices.any { it.address == device.address }) { + Log.d(TAG, "Found device name=${device.name ?: "Unknown"} address=${device.address}") + foundDevices = foundDevices + device + } } } } @@ -627,7 +748,18 @@ class MainActivity : ComponentActivity() { mergeSettings = trainRecordManager.mergeSettings - Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab}") + specifiedDeviceAddress = settingsPrefs.getString("specified_device_address", null) + + val searchOrderStr = settingsPrefs.getString("search_order_list", "") + searchOrderList = if (searchOrderStr.isNullOrEmpty()) { + emptyList() + } else { + searchOrderStr.split(",").filter { it.isNotBlank() } + } + + bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress) + + Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size}") } @@ -643,6 +775,8 @@ class MainActivity : ComponentActivity() { .putInt("settings_scroll_position", settingsScrollPosition) .putFloat("map_zoom_level", mapZoomLevel.toFloat()) .putBoolean("map_railway_visible", mapRailwayLayerVisible) + .putString("specified_device_address", specifiedDeviceAddress) + .putString("search_order_list", searchOrderList.joinToString(",")) mapCenterPosition?.let { (lat, lon) -> editor.putFloat("map_center_lat", lat.toFloat()) @@ -657,9 +791,14 @@ class MainActivity : ComponentActivity() { super.onResume() Log.d(TAG, "App resumed") + bleClient.setHighFrequencyReconnect(true) + if (hasBluetoothPermissions() && !bleClient.isConnected()) { Log.d(TAG, "App resumed and not connected, starting auto scan") startAutoScanAndConnect() + } else if (bleClient.isConnected()) { + showDisconnectButton = true + deviceStatus = "已连接" } } @@ -669,6 +808,9 @@ class MainActivity : ComponentActivity() { if (isFinishing) { bleClient.disconnectAndCleanup() Log.d(TAG, "App finishing, BLE cleaned up") + } else { + bleClient.setHighFrequencyReconnect(false) + Log.d(TAG, "App paused, reduced reconnect frequency") } Log.d(TAG, "App paused, settings saved") } @@ -683,6 +825,11 @@ fun MainContent( currentTab: Int, onTabChange: (Int) -> Unit, onConnectClick: () -> Unit, + onDisconnectClick: () -> Unit, + showDisconnectButton: Boolean, + specifiedDeviceAddress: String?, + searchOrderList: List, + onSpecifiedDeviceSelected: (String?) -> Unit, latestRecord: TrainRecord?, @@ -794,22 +941,31 @@ fun MainContent( if (historyEditMode && currentTab == 0) { TopAppBar( title = { - val totalSelectedCount = historySelectedRecords.sumOf { selectedId -> - allRecords.find { item -> - when (item) { - is TrainRecord -> item.uniqueId == selectedId - is org.noxylva.lbjconsole.model.MergedTrainRecord -> - item.records.any { it.uniqueId == selectedId } - else -> false - } - }?.let { item -> - when (item) { - is TrainRecord -> 1 - is org.noxylva.lbjconsole.model.MergedTrainRecord -> item.records.size - else -> 0 - } - } ?: 0 + val totalSelectedCount = run { + val processedMergedRecords = mutableSetOf() + var count = 0 + + historySelectedRecords.forEach { selectedId -> + val foundItem = allRecords.find { item -> + when (item) { + is TrainRecord -> item.uniqueId == selectedId + is org.noxylva.lbjconsole.model.MergedTrainRecord -> item.records.any { it.uniqueId == selectedId } + else -> false + } + } + + when (foundItem) { + is TrainRecord -> count += 1 + is org.noxylva.lbjconsole.model.MergedTrainRecord -> { + if (!processedMergedRecords.contains(foundItem.groupKey)) { + count += foundItem.records.size + processedMergedRecords.add(foundItem.groupKey) } + } + } + } + count + } Text( "已选择 $totalSelectedCount 条记录", color = MaterialTheme.colorScheme.onPrimary @@ -833,34 +989,34 @@ fun MainContent( val recordsToDelete = mutableSetOf() val idToRecordMap = mutableMapOf() val idToMergedRecordMap = mutableMapOf() - - allRecords.forEach { item -> - when (item) { - is TrainRecord -> { - idToRecordMap[item.uniqueId] = item - } - is org.noxylva.lbjconsole.model.MergedTrainRecord -> { - item.records.forEach { record -> - idToRecordMap[record.uniqueId] = record - idToMergedRecordMap[record.uniqueId] = item - } - } - } - } - - val processedMergedRecords = mutableSetOf() - - historySelectedRecords.forEach { selectedId -> - val mergedRecord = idToMergedRecordMap[selectedId] - if (mergedRecord != null && !processedMergedRecords.contains(mergedRecord)) { - recordsToDelete.addAll(mergedRecord.records) - processedMergedRecords.add(mergedRecord) - } else if (mergedRecord == null) { - idToRecordMap[selectedId]?.let { record -> - recordsToDelete.add(record) - } - } - } + + allRecords.forEach { item -> + when (item) { + is TrainRecord -> { + idToRecordMap[item.uniqueId] = item + } + is org.noxylva.lbjconsole.model.MergedTrainRecord -> { + item.records.forEach { record -> + idToRecordMap[record.uniqueId] = record + idToMergedRecordMap[record.uniqueId] = item + } + } + } + } + + val processedMergedRecordKeys = mutableSetOf() + + historySelectedRecords.forEach { selectedId -> + val mergedRecord = idToMergedRecordMap[selectedId] + if (mergedRecord != null && !processedMergedRecordKeys.contains(mergedRecord.groupKey)) { + recordsToDelete.addAll(mergedRecord.records) + processedMergedRecordKeys.add(mergedRecord.groupKey) + } else if (mergedRecord == null) { + idToRecordMap[selectedId]?.let { record -> + recordsToDelete.add(record) + } + } + } onDeleteRecords(recordsToDelete.toList()) onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset) @@ -940,7 +1096,10 @@ fun MainContent( mergeSettings = mergeSettings, onMergeSettingsChange = onMergeSettingsChange, scrollPosition = settingsScrollPosition, - onScrollPositionChange = onSettingsScrollPositionChange + onScrollPositionChange = onSettingsScrollPositionChange, + specifiedDeviceAddress = specifiedDeviceAddress, + searchOrderList = searchOrderList, + onSpecifiedDeviceSelected = onSpecifiedDeviceSelected ) 3 -> MapScreen( records = if (allRecords.isNotEmpty()) { @@ -973,7 +1132,11 @@ fun ConnectionDialog( devices: List, onDismiss: () -> Unit, onScan: () -> Unit, - onConnect: (BluetoothDevice) -> Unit + onConnect: (BluetoothDevice) -> Unit, + onDisconnect: () -> Unit = {}, + isConnected: Boolean = false, + targetDeviceName: String = "LBJReceiver", + deviceAddress: String? = null ) { AlertDialog( onDismissRequest = onDismiss, @@ -987,39 +1150,41 @@ fun ConnectionDialog( Column( modifier = Modifier.fillMaxWidth() ) { - Button( - onClick = onScan, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isScanning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary - ) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + if (!isConnected) { + Button( + onClick = onScan, + modifier = Modifier.fillMaxWidth(), + colors = ButtonDefaults.buttonColors( + containerColor = if (isScanning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary + ) ) { - if (isScanning) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary - ) - } else { - Icon( - imageVector = Icons.Default.Search, - contentDescription = null + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (isScanning) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + } else { + Icon( + imageVector = Icons.Default.Search, + contentDescription = null + ) + } + Text( + text = "扫描设备", + fontWeight = FontWeight.Medium ) } - Text( - text = if (isScanning) "扫描中..." else "扫描设备", - fontWeight = FontWeight.Medium - ) } } Spacer(modifier = Modifier.height(16.dp)) - if (devices.isNotEmpty()) { + if (devices.isNotEmpty() && !isConnected) { Text( text = "发现 ${devices.size} 个设备", style = MaterialTheme.typography.titleSmall, @@ -1031,20 +1196,39 @@ fun ConnectionDialog( modifier = Modifier.heightIn(max = 200.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { - items(devices) { device -> + items(devices.filter { !isConnected }) { device -> var isPressed by remember { mutableStateOf(false) } + var isHovered by remember { mutableStateOf(false) } val cardScale by animateFloatAsState( - targetValue = if (isPressed) 0.98f else 1f, + targetValue = when { + isPressed -> 0.96f + isHovered -> 1.02f + else -> 1f + }, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessLow + ), + label = "cardScale" + ) + + val cardElevation by animateDpAsState( + targetValue = when { + isPressed -> 1.dp + isHovered -> 6.dp + else -> 2.dp + }, animationSpec = tween( - durationMillis = 120, - easing = LinearEasing - ) + durationMillis = 150, + easing = FastOutSlowInEasing + ), + label = "cardElevation" ) LaunchedEffect(isPressed) { if (isPressed) { - delay(100) + delay(120) isPressed = false } } @@ -1055,8 +1239,18 @@ fun ConnectionDialog( .graphicsLayer { scaleX = cardScale scaleY = cardScale + } + .pointerInput(Unit) { + detectTapGestures( + onPress = { + isPressed = true + isHovered = true + tryAwaitRelease() + isHovered = false + } + ) }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + elevation = CardDefaults.cardElevation(defaultElevation = cardElevation) ) { Row( modifier = Modifier @@ -1098,7 +1292,9 @@ fun ConnectionDialog( } } } - } else if (!isScanning) { + } else if (isScanning && !isConnected) { + + } else { Box( modifier = Modifier .fillMaxWidth() @@ -1109,25 +1305,51 @@ fun ConnectionDialog( horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - imageVector = Icons.Default.BluetoothSearching, + imageVector = if (isConnected) Icons.Default.Bluetooth else Icons.Default.BluetoothSearching, contentDescription = null, modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + tint = if (isConnected) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "未发现设备", + text = if (isConnected) "设备已连接" else "未发现设备", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = if (isConnected) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "请确保设备已开启并处于可发现状态", + text = if (isConnected) + deviceAddress?.ifEmpty { "未知地址" } ?: "未知地址" + else + "请确保设备已开启并处于可发现状态", style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), textAlign = TextAlign.Center ) + if (isConnected) { + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = onDisconnect, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) + ) { + Icon( + imageVector = Icons.Default.BluetoothDisabled, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("断开连接") + } + } } } } diff --git a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt index 4cf1935..5eb65ed 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.SharedPreferences import android.os.Environment import android.util.Log +import kotlinx.coroutines.* import org.json.JSONArray import org.json.JSONObject import java.io.File @@ -26,13 +27,16 @@ class TrainRecordManager(private val context: Context) { private val trainRecords = CopyOnWriteArrayList() private val recordCount = AtomicInteger(0) private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) var mergeSettings = MergeSettings() private set init { - loadRecords() - loadMergeSettings() + ioScope.launch { + loadRecords() + loadMergeSettings() + } } @@ -145,15 +149,17 @@ class TrainRecordManager(private val context: Context) { } private fun saveRecords() { - try { - val jsonArray = JSONArray() - for (record in trainRecords) { - jsonArray.put(record.toJSON()) + ioScope.launch { + try { + val jsonArray = JSONArray() + for (record in trainRecords) { + jsonArray.put(record.toJSON()) + } + prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply() + Log.d(TAG, "Saved ${trainRecords.size} records") + } catch (e: Exception) { + Log.e(TAG, "Failed to save records: ${e.message}") } - prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply() - Log.d(TAG, "Saved ${trainRecords.size} records") - } catch (e: Exception) { - Log.e(TAG, "Failed to save records: ${e.message}") } } @@ -258,16 +264,18 @@ class TrainRecordManager(private val context: Context) { } private fun saveMergeSettings() { - try { - val json = JSONObject().apply { - put("enabled", mergeSettings.enabled) - put("groupBy", mergeSettings.groupBy.name) - put("timeWindow", mergeSettings.timeWindow.name) + ioScope.launch { + try { + val json = JSONObject().apply { + put("enabled", mergeSettings.enabled) + put("groupBy", mergeSettings.groupBy.name) + put("timeWindow", mergeSettings.timeWindow.name) + } + prefs.edit().putString(KEY_MERGE_SETTINGS, json.toString()).apply() + Log.d(TAG, "Saved merge settings") + } catch (e: Exception) { + Log.e(TAG, "Failed to save merge settings: ${e.message}") } - prefs.edit().putString(KEY_MERGE_SETTINGS, json.toString()).apply() - Log.d(TAG, "Saved merge settings") - } catch (e: Exception) { - Log.e(TAG, "Failed to save merge settings: ${e.message}") } } diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt deleted file mode 100644 index 7f5fb06..0000000 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt +++ /dev/null @@ -1,153 +0,0 @@ -package org.noxylva.lbjconsole.ui.components - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.material3.HorizontalDivider -import org.noxylva.lbjconsole.model.TrainRecord - -@Composable -fun TrainInfoCard( - trainRecord: TrainRecord, - modifier: Modifier = Modifier -) { - val recordMap = trainRecord.toMap() - - Card( - modifier = modifier - .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 6.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = recordMap["train"]?.toString() ?: "", - fontWeight = FontWeight.Bold, - fontSize = 16.sp - ) - - Spacer(modifier = Modifier.width(4.dp)) - - val directionStr = recordMap["direction"]?.toString() ?: "" - val directionColor = when(directionStr) { - "上行" -> MaterialTheme.colorScheme.primary - "下行" -> MaterialTheme.colorScheme.secondary - else -> MaterialTheme.colorScheme.onSurface - } - - Surface( - shape = RoundedCornerShape(4.dp), - color = directionColor.copy(alpha = 0.1f), - modifier = Modifier.padding(horizontal = 2.dp) - ) { - Text( - text = directionStr, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), - fontSize = 12.sp, - color = directionColor - ) - } - } - - Text( - text = run { - val trainTime = trainRecord.time.trim() - if (trainTime.isNotEmpty() && trainTime != "NUL" && trainTime != "" && trainTime != "NA" && trainTime != "") { - trainTime - } else { - val receivedTime = recordMap["receivedTimestamp"]?.toString() ?: "" - if (receivedTime.contains(" ")) { - receivedTime.split(" ")[1] - } else { - java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(trainRecord.receivedTimestamp) - } - } - }, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "速度: ${recordMap["speed"] ?: ""}", - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - - Text( - text = "位置: ${recordMap["position"] ?: ""}", - fontSize = 14.sp, - fontWeight = FontWeight.Medium - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - HorizontalDivider(thickness = 0.5.dp) - Spacer(modifier = Modifier.height(4.dp)) - - Row( - modifier = Modifier.fillMaxWidth() - ) { - Column(modifier = Modifier.weight(1f)) { - CompactInfoItem(label = "机车号", value = recordMap["loco"]?.toString() ?: "") - CompactInfoItem(label = "线路", value = recordMap["route"]?.toString() ?: "") - } - - Column(modifier = Modifier.weight(1f)) { - CompactInfoItem(label = "类型", value = recordMap["lbj_class"]?.toString() ?: "") - CompactInfoItem(label = "信号", value = recordMap["rssi"]?.toString() ?: "") - } - } - } - } -} - -@Composable -private fun CompactInfoItem( - label: String, - value: String, - modifier: Modifier = Modifier -) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(vertical = 2.dp) - ) { - Text( - text = "$label: ", - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = value, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt deleted file mode 100644 index a144c47..0000000 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt +++ /dev/null @@ -1,373 +0,0 @@ -package org.noxylva.lbjconsole.ui.components - -import androidx.compose.animation.* -import androidx.compose.animation.core.* -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Clear -import androidx.compose.material.icons.filled.FilterList -import androidx.compose.material.icons.filled.Share -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import org.noxylva.lbjconsole.model.TrainRecord -import java.text.SimpleDateFormat -import java.util.* - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun TrainRecordsList( - records: List, - onRecordClick: (TrainRecord) -> Unit, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - if (records.isEmpty()) { - Text( - text = "暂无历史记录", - modifier = Modifier.padding(16.dp), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - itemsIndexed(records, key = { _, record -> record.uniqueId }) { index, record -> - val animationDelay = (index * 30).coerceAtMost(200) - var isPressed by remember { mutableStateOf(false) } - - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.98f else 1f, - animationSpec = tween(durationMillis = 120) - ) - - val elevation by animateDpAsState( - targetValue = if (isPressed) 6.dp else 2.dp, - animationSpec = tween(durationMillis = 120) - ) - - LaunchedEffect(isPressed) { - if (isPressed) { - kotlinx.coroutines.delay(150) - isPressed = false - } - } - - Card( - modifier = Modifier - .fillMaxWidth() - .graphicsLayer { - scaleX = scale - scaleY = scale - } - .animateItemPlacement( - animationSpec = tween(durationMillis = 200) - ), - elevation = CardDefaults.cardElevation(defaultElevation = elevation) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true) - ) { - isPressed = true - onRecordClick(record) - } - .padding(8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - - Column { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = record.train, - fontWeight = FontWeight.Bold, - fontSize = 15.sp - ) - - Spacer(modifier = Modifier.width(4.dp)) - - - val directionText = when (record.direction) { - 1 -> "下行" - 3 -> "上行" - else -> "未知" - } - - val directionColor = when(record.direction) { - 1 -> MaterialTheme.colorScheme.secondary - 3 -> MaterialTheme.colorScheme.primary - else -> MaterialTheme.colorScheme.onSurface - } - - Surface( - color = directionColor.copy(alpha = 0.1f), - shape = MaterialTheme.shapes.small - ) { - Text( - text = directionText, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), - fontSize = 11.sp, - color = directionColor - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - - - Text( - text = "位置: ${record.position} km", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - - Column( - horizontalAlignment = Alignment.End - ) { - Text( - text = "${record.speed} km/h", - fontWeight = FontWeight.Medium, - fontSize = 14.sp - ) - - Spacer(modifier = Modifier.height(2.dp)) - - - val timeStr = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(record.timestamp) - Text( - text = timeStr, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - } - } -} - -@Composable -fun TrainRecordsListWithToolbar( - records: List, - onRecordClick: (TrainRecord) -> Unit, - onFilterClick: () -> Unit, - onClearClick: () -> Unit, - onDeleteRecords: (List) -> Unit, - modifier: Modifier = Modifier -) { - var selectedRecords by remember { mutableStateOf>(mutableSetOf()) } - var selectionMode by remember { mutableStateOf(false) } - - Column(modifier = modifier.fillMaxSize()) { - - @OptIn(ExperimentalMaterial3Api::class) - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp, - shadowElevation = 3.dp - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = if (selectionMode) "已选择 ${selectedRecords.size} 条" else "历史记录 (${records.size})", - style = MaterialTheme.typography.titleMedium - ) - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - if (selectionMode) { - TextButton( - onClick = { - if (selectedRecords.isNotEmpty()) { - onDeleteRecords(selectedRecords.toList()) - } - selectionMode = false - selectedRecords = mutableSetOf() - }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Text("删除") - } - TextButton(onClick = { - selectionMode = false - selectedRecords = mutableSetOf() - }) { - Text("取消") - } - } else { - IconButton(onClick = onFilterClick) { - Icon( - imageVector = Icons.Default.FilterList, - contentDescription = "筛选" - ) - } - } - } - } - } - - - LazyColumn( - modifier = Modifier.weight(1f), - contentPadding = PaddingValues(vertical = 4.dp, horizontal = 8.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(records.chunked(2)) { rowRecords -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - rowRecords.forEach { record -> - val isSelected = selectedRecords.contains(record) - Card( - modifier = Modifier - .weight(1f) - .clickable { - if (selectionMode) { - if (isSelected) { - selectedRecords.remove(record) - } else { - selectedRecords.add(record) - } - if (selectedRecords.isEmpty()) { - selectionMode = false - } - } else { - onRecordClick(record) - } - } - .padding(vertical = 2.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - - Column(modifier = Modifier.weight(1f)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - if (selectionMode) { - Checkbox( - checked = isSelected, - onCheckedChange = { checked -> - if (checked) { - selectedRecords.add(record) - } else { - selectedRecords.remove(record) - } - if (selectedRecords.isEmpty()) { - selectionMode = false - } - }, - modifier = Modifier.padding(end = 8.dp) - ) - } - - Text( - text = record.train, - fontWeight = FontWeight.Bold, - fontSize = 15.sp, - modifier = Modifier.weight(1f) - ) - - if (!selectionMode) { - IconButton( - onClick = { - selectionMode = true - selectedRecords = mutableSetOf(record) - }, - modifier = Modifier.size(32.dp) - ) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "删除", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.error - ) - } - } - } - - if (record.speed.isNotEmpty() || record.position.isNotEmpty()) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - if (record.speed.isNotEmpty()) { - Text( - text = "${record.speed} km/h", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - if (record.position.isNotEmpty()) { - Text( - text = "${record.position} km", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - val timeStr = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(record.timestamp) - Text( - text = timeStr, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt index e2b63bd..fe712a1 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt @@ -55,42 +55,28 @@ fun TrainRecordItem( locoInfoUtil: LocoInfoUtil?, onRecordClick: (TrainRecord) -> Unit, onToggleSelection: (TrainRecord) -> Unit, - onLongClick: (TrainRecord) -> Unit + onLongClick: (TrainRecord) -> Unit, + modifier: Modifier = Modifier ) { val recordId = record.uniqueId val isExpanded = expandedStatesMap[recordId] == true - val cardColor by animateColorAsState( - targetValue = when { - isSelected -> MaterialTheme.colorScheme.primaryContainer - else -> MaterialTheme.colorScheme.surface - }, - animationSpec = tween(durationMillis = 200), - label = "cardColor" - ) + val cardColor = when { + isSelected -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.surface + } - val cardScale by animateFloatAsState( - targetValue = if (isSelected) 0.98f else 1f, - animationSpec = tween(durationMillis = 150), - label = "cardScale" - ) + val cardScale = if (isSelected) 0.98f else 1f - val cardElevation by animateDpAsState( - targetValue = if (isSelected) 6.dp else 2.dp, - animationSpec = tween(durationMillis = 200), - label = "cardElevation" - ) + val cardElevation = if (isSelected) 6.dp else 2.dp Card( - modifier = Modifier + modifier = modifier .fillMaxWidth() .graphicsLayer { scaleX = cardScale scaleY = cardScale - } - .animateContentSize( - animationSpec = tween(durationMillis = 150, easing = LinearEasing) - ), + }, elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), colors = CardDefaults.cardColors( containerColor = cardColor @@ -290,18 +276,15 @@ fun TrainRecordItem( ) } } - + Spacer(modifier = Modifier.height(8.dp)) AnimatedVisibility( visible = isExpanded, - enter = expandVertically(animationSpec = tween(durationMillis = 300)) + fadeIn(animationSpec = tween(durationMillis = 300)), - exit = shrinkVertically(animationSpec = tween(durationMillis = 300)) + fadeOut(animationSpec = tween(durationMillis = 300)) + enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)), + exit = shrinkVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) ) { Column { val coordinates = remember { record.getCoordinates() } - if (coordinates != null) { - Spacer(modifier = Modifier.height(8.dp)) - } if (coordinates != null) { Box( @@ -413,7 +396,8 @@ fun MergedTrainRecordItem( isInEditMode: Boolean = false, selectedRecords: List = emptyList(), onToggleSelection: (TrainRecord) -> Unit = {}, - onLongClick: (TrainRecord) -> Unit = {} + onLongClick: (TrainRecord) -> Unit = {}, + modifier: Modifier = Modifier ) { val recordId = mergedRecord.groupKey val isExpanded = expandedStatesMap[recordId] == true @@ -421,37 +405,22 @@ fun MergedTrainRecordItem( val hasSelectedRecords = mergedRecord.records.any { selectedRecords.contains(it) } - val cardColor by animateColorAsState( - targetValue = when { - hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer - else -> MaterialTheme.colorScheme.surface - }, - animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing), - label = "mergedCardColor" - ) + val cardColor = when { + hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer + else -> MaterialTheme.colorScheme.surface + } - val cardScale by animateFloatAsState( - targetValue = if (hasSelectedRecords) 0.98f else 1f, - animationSpec = tween(durationMillis = 150, easing = LinearEasing), - label = "mergedCardScale" - ) + val cardScale = if (hasSelectedRecords) 0.98f else 1f - val cardElevation by animateDpAsState( - targetValue = if (hasSelectedRecords) 6.dp else 2.dp, - animationSpec = tween(durationMillis = 200, easing = LinearEasing), - label = "mergedCardElevation" - ) + val cardElevation = if (hasSelectedRecords) 6.dp else 2.dp Card( - modifier = Modifier + modifier = modifier .fillMaxWidth() .graphicsLayer { scaleX = cardScale scaleY = cardScale - } - .animateContentSize( - animationSpec = tween(durationMillis = 150, easing = LinearEasing) - ), + }, elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), colors = CardDefaults.cardColors( containerColor = cardColor @@ -661,15 +630,18 @@ fun MergedTrainRecordItem( ) } } + Spacer(modifier = Modifier.height(8.dp)) + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeIn(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)), + exit = shrinkVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + ) { + Column { + val coordinates = remember { latestRecord.getCoordinates() } - if (isExpanded) { - val coordinates = remember { latestRecord.getCoordinates() } - if (coordinates != null) { - Spacer(modifier = Modifier.height(8.dp)) - } - if (coordinates != null) { + if (coordinates != null) { Box( modifier = Modifier .fillMaxWidth() @@ -752,22 +724,22 @@ fun MergedTrainRecordItem( }, update = { mapView -> mapView.invalidate() } ) + } } - } - if (recordMap.containsKey("position_info")) { + if (recordMap.containsKey("position_info")) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = recordMap["position_info"] ?: "", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurface + ) + } + Spacer(modifier = Modifier.height(8.dp)) - Text( - text = recordMap["position_info"] ?: "", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - HorizontalDivider() - Spacer(modifier = Modifier.height(8.dp)) - - mergedRecord.records.filter { it != mergedRecord.latestRecord }.forEach { recordItem -> + HorizontalDivider() + Spacer(modifier = Modifier.height(8.dp)) + + mergedRecord.records.filter { it != mergedRecord.latestRecord }.forEach { recordItem -> val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) Column( @@ -858,6 +830,7 @@ fun MergedTrainRecordItem( color = MaterialTheme.colorScheme.onSurfaceVariant ) } + } } } } @@ -1020,6 +993,7 @@ fun HistoryScreen( when (item) { is TrainRecord -> { TrainRecordItem( + modifier = Modifier, record = item, isSelected = selectedRecordsList.contains(item), isInEditMode = isInEditMode, @@ -1045,6 +1019,7 @@ fun HistoryScreen( } is MergedTrainRecord -> { MergedTrainRecordItem( + modifier = Modifier, mergedRecord = item, expandedStatesMap = expandedStatesMap, locoInfoUtil = locoInfoUtil, diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MergedHistoryScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MergedHistoryScreen.kt deleted file mode 100644 index 63ca378..0000000 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MergedHistoryScreen.kt +++ /dev/null @@ -1,490 +0,0 @@ -package org.noxylva.lbjconsole.ui.screens - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView -import org.osmdroid.tileprovider.MapTileProviderBasic -import org.osmdroid.tileprovider.tilesource.TileSourceFactory -import org.osmdroid.tileprovider.tilesource.XYTileSource -import org.osmdroid.views.MapView -import org.osmdroid.views.overlay.Marker -import org.osmdroid.views.overlay.Polyline -import org.osmdroid.views.overlay.TilesOverlay -import org.noxylva.lbjconsole.model.MergedTrainRecord -import org.noxylva.lbjconsole.model.TrainRecord -import org.noxylva.lbjconsole.util.LocoInfoUtil -import java.text.SimpleDateFormat -import java.util.* - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) -@Composable -fun MergedHistoryScreen( - mergedRecords: List, - latestRecord: TrainRecord?, - lastUpdateTime: Date?, - temporaryStatusMessage: String? = null, - locoInfoUtil: LocoInfoUtil? = null, - onClearRecords: () -> Unit = {}, - onRecordClick: (TrainRecord) -> Unit = {}, - onClearLog: () -> Unit = {}, - onDeleteRecords: (List) -> Unit = {}, - onDeleteMergedRecord: (MergedTrainRecord) -> Unit = {}, - editMode: Boolean = false, - selectedRecords: Set = emptySet(), - expandedStates: Map = emptyMap(), - scrollPosition: Int = 0, - scrollOffset: Int = 0, - onStateChange: (Boolean, Set, Map, Int, Int) -> Unit = { _, _, _, _, _ -> } -) { - var isInEditMode by remember(editMode) { mutableStateOf(editMode) } - val selectedRecordsList = remember(selectedRecords) { - mutableStateListOf().apply { - addAll(mergedRecords.flatMap { it.records }.filter { - selectedRecords.contains(it.uniqueId) - }) - } - } - val expandedStatesMap = remember(expandedStates) { - mutableStateMapOf().apply { putAll(expandedStates) } - } - - val listState = rememberLazyListState( - initialFirstVisibleItemIndex = scrollPosition, - initialFirstVisibleItemScrollOffset = scrollOffset - ) - - LaunchedEffect(isInEditMode, selectedRecordsList.size) { - val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet() - onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), - listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) - } - - Box(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .weight(1.0f) - ) { - if (mergedRecords.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - "暂无合并记录", - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.outline - ) - Text( - "请检查合并设置或等待更多数据", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f) - ) - } - } - } else { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(mergedRecords) { mergedRecord -> - MergedRecordCard( - mergedRecord = mergedRecord, - isExpanded = expandedStatesMap[mergedRecord.groupKey] == true, - onExpandToggle = { - expandedStatesMap[mergedRecord.groupKey] = - !(expandedStatesMap[mergedRecord.groupKey] ?: false) - }, - locoInfoUtil = locoInfoUtil - ) - } - } - } - } - } - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun MergedRecordCard( - mergedRecord: MergedTrainRecord, - isExpanded: Boolean, - onExpandToggle: () -> Unit, - locoInfoUtil: LocoInfoUtil? -) { - val record = mergedRecord.latestRecord - val recordMap = record.toMap(showDetailedTime = true) - - Card( - modifier = Modifier.fillMaxWidth(), - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - shape = RoundedCornerShape(8.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = onExpandToggle, - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true) - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - if (recordMap.containsKey("time")) { - Column { - recordMap["time"]?.split("\n")?.forEach { timeLine -> - Text( - text = timeLine, - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = "${record.rssi} dBm", - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(2.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - val trainDisplay = recordMap["train"]?.toString() ?: "未知列车" - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - Text( - text = trainDisplay, - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - color = MaterialTheme.colorScheme.primary - ) - - val directionText = when (record.direction) { - 1 -> "下" - 3 -> "上" - else -> "" - } - - if (directionText.isNotEmpty()) { - Surface( - shape = RoundedCornerShape(2.dp), - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.size(20.dp) - ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = directionText, - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.surface, - maxLines = 1, - modifier = Modifier.offset(y = (-2).dp) - ) - } - } - } - } - - val formattedInfo = when { - record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> { - val shortLoco = if (record.loco.length > 5) { - record.loco.takeLast(5) - } else { - record.loco - } - "${record.locoType}-${shortLoco}" - } - record.locoType.isNotEmpty() -> record.locoType - record.loco.isNotEmpty() -> record.loco - else -> "" - } - - if (formattedInfo.isNotEmpty() && formattedInfo != "") { - Text( - text = formattedInfo, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - val routeStr = record.route.trim() - val isValidRoute = routeStr.isNotEmpty() && !routeStr.all { it == '*' } - - val position = record.position.trim() - val isValidPosition = position.isNotEmpty() && - !position.all { it == '-' || it == '.' } && - position != "" - - if (isValidRoute || isValidPosition) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.height(24.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (isValidRoute) { - Text( - text = "$routeStr", - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.alignByBaseline() - ) - } - - if (isValidPosition) { - Text( - text = "${position}K", - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.alignByBaseline() - ) - } - } - } - - val speed = record.speed.trim() - val isValidSpeed = speed.isNotEmpty() && - !speed.all { it == '*' || it == '-' } && - speed != "NUL" && - speed != "" - if (isValidSpeed) { - Text( - text = "${speed} km/h", - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - - if (locoInfoUtil != null && record.locoType.isNotEmpty() && record.loco.isNotEmpty()) { - val locoInfoText = locoInfoUtil.getLocoInfoDisplay(record.locoType, record.loco) - if (locoInfoText != null) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = locoInfoText, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - - if (isExpanded) { - Spacer(modifier = Modifier.height(12.dp)) - Divider() - Spacer(modifier = Modifier.height(8.dp)) - - Text( - "记录详情", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary - ) - - Spacer(modifier = Modifier.height(8.dp)) - - mergedRecord.records.forEach { recordItem -> - val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - ) { - Text( - text = timeFormat.format(recordItem.timestamp), - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween - ) { - val locationText = buildString { - if (recordItem.route.isNotEmpty() && recordItem.route != "") { - append(recordItem.route) - } - if (recordItem.position.isNotEmpty() && recordItem.position != "") { - if (isNotEmpty()) append(" ") - append("${recordItem.position}K") - } - } - - Text( - text = locationText.ifEmpty { "位置未知" }, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - val directionText = when (recordItem.direction) { - 1 -> "下行" - 3 -> "上行" - else -> "未知" - } - Text( - text = directionText, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - val speedText = if (recordItem.speed.isNotEmpty() && - recordItem.speed != "" && - !recordItem.speed.all { it == '*' || it == '-' }) { - "${recordItem.speed}km/h" - } else { - "速度未知" - } - Text( - text = speedText, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - val coordinates = mergedRecord.getAllCoordinates() - val recordsWithCoordinates = mergedRecord.records.filter { it.getCoordinates() != null } - - if (coordinates.isNotEmpty()) { - Spacer(modifier = Modifier.height(12.dp)) - Text( - "行进路径 (${coordinates.size}/${mergedRecord.records.size} 个记录有位置信息)", - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.height(8.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(220.dp) - .clip(RoundedCornerShape(8.dp)), - contentAlignment = Alignment.Center - ) { - AndroidView( - factory = { context -> - MapView(context).apply { - setTileSource(TileSourceFactory.MAPNIK) - setMultiTouchControls(true) - zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER) - - try { - val railwayTileSource = XYTileSource( - "OpenRailwayMap", 8, 16, 256, ".png", - arrayOf( - "https://a.tiles.openrailwaymap.org/standard/", - "https://b.tiles.openrailwaymap.org/standard/", - "https://c.tiles.openrailwaymap.org/standard/" - ), - "© OpenRailwayMap contributors, © OpenStreetMap contributors" - ) - val railwayProvider = MapTileProviderBasic(context) - railwayProvider.tileSource = railwayTileSource - val railwayOverlay = TilesOverlay(railwayProvider, context) - overlays.add(railwayOverlay) - } catch (e: Exception) { - e.printStackTrace() - } - - if (coordinates.size > 1) { - val polyline = Polyline().apply { - setPoints(coordinates) - outlinePaint.color = android.graphics.Color.BLUE - outlinePaint.strokeWidth = 5f - } - overlays.add(polyline) - } - - coordinates.forEachIndexed { index, coord -> - val marker = Marker(this).apply { - position = coord - title = when (index) { - 0 -> "起点" - coordinates.lastIndex -> "终点" - else -> "经过点 ${index + 1}" - } - } - overlays.add(marker) - } - - val centerLat = coordinates.map { it.latitude }.average() - val centerLon = coordinates.map { it.longitude }.average() - controller.setCenter(org.osmdroid.util.GeoPoint(centerLat, centerLon)) - controller.setZoom(12.0) - } - }, - update = { mapView -> - mapView.invalidate() - } - ) - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt index 9991708..db82a13 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt @@ -31,7 +31,10 @@ fun SettingsScreen( mergeSettings: MergeSettings, onMergeSettingsChange: (MergeSettings) -> Unit, scrollPosition: Int = 0, - onScrollPositionChange: (Int) -> Unit = {} + onScrollPositionChange: (Int) -> Unit = {}, + specifiedDeviceAddress: String? = null, + searchOrderList: List = emptyList(), + onSpecifiedDeviceSelected: (String?) -> Unit = {} ) { val uriHandler = LocalUriHandler.current val scrollState = rememberScrollState() @@ -95,6 +98,69 @@ fun SettingsScreen( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(12.dp) ) + + if (searchOrderList.isNotEmpty()) { + var deviceAddressExpanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = deviceAddressExpanded, + onExpandedChange = { deviceAddressExpanded = !deviceAddressExpanded } + ) { + OutlinedTextField( + value = specifiedDeviceAddress ?: "无", + onValueChange = {}, + readOnly = true, + label = { Text("指定设备地址") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.LocationOn, + contentDescription = null + ) + }, + trailingIcon = { + ExposedDropdownMenuDefaults.TrailingIcon(expanded = deviceAddressExpanded) + }, + modifier = Modifier + .fillMaxWidth() + .menuAnchor(), + shape = RoundedCornerShape(12.dp) + ) + ExposedDropdownMenu( + expanded = deviceAddressExpanded, + onDismissRequest = { deviceAddressExpanded = false } + ) { + DropdownMenuItem( + text = { Text("无") }, + onClick = { + onSpecifiedDeviceSelected(null) + deviceAddressExpanded = false + } + ) + searchOrderList.forEach { address -> + DropdownMenuItem( + text = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(address) + if (address == specifiedDeviceAddress) { + Spacer(modifier = Modifier.width(8.dp)) + Icon( + imageVector = Icons.Default.Check, + contentDescription = "已指定", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(16.dp) + ) + } + } + }, + onClick = { + onSpecifiedDeviceSelected(address) + deviceAddressExpanded = false + } + ) + } + } + } + } } } @@ -237,7 +303,7 @@ fun SettingsScreen( .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .clickable { - uriHandler.openUri("https://github.com/undef-i") + uriHandler.openUri("https://github.com/undef-i/LBJ_Console") } .padding(12.dp) )