refactor: optimize record management and UI interaction logic
- Move the loading and saving operations of TrainRecordManager to the IO goroutine for execution - Optimize the data structure of recentRecords in MainActivity to be a mutableStateList - Improve the interaction effect and device connection status display of ConnectionDialog - Delete the MergedHistoryScreen file that is no longer in use - Increase the number of threads for map tile downloads and file system operations
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,4 +18,5 @@ local.properties
|
|||||||
.*.bat
|
.*.bat
|
||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
*.base64
|
*.base64
|
||||||
|
docs
|
||||||
@@ -45,6 +45,16 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
private var lastKnownDeviceAddress: String? = null
|
private var lastKnownDeviceAddress: String? = null
|
||||||
private var connectionAttempts = 0
|
private var connectionAttempts = 0
|
||||||
private var isReconnecting = false
|
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() {
|
private val leScanCallback = object : ScanCallback() {
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
@@ -82,18 +92,25 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
scanCallback?.invoke(device)
|
scanCallback?.invoke(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetDeviceName != null && !isConnected && !isReconnecting) {
|
if (!isConnected && !isReconnecting && !isDialogOpen && !isAutoConnectBlocked) {
|
||||||
if (deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)) {
|
val deviceAddress = device.address
|
||||||
Log.i(TAG, "Found target device: $deviceName, auto-connecting")
|
val isSpecifiedDevice = specifiedDeviceAddress == deviceAddress
|
||||||
lastKnownDeviceAddress = device.address
|
val isTargetDevice = targetDeviceName != null && deviceName != null && deviceName.equals(targetDeviceName, ignoreCase = true)
|
||||||
connectImmediately(device.address)
|
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) {
|
override fun onScanFailed(errorCode: Int) {
|
||||||
@@ -279,7 +296,66 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission")
|
@SuppressLint("MissingPermission")
|
||||||
fun disconnect() {
|
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 -> {
|
BluetoothProfile.STATE_CONNECTED -> {
|
||||||
isConnected = true
|
isConnected = true
|
||||||
isReconnecting = false
|
isReconnecting = false
|
||||||
|
isManualDisconnect = false
|
||||||
connectionAttempts = 0
|
connectionAttempts = 0
|
||||||
Log.i(TAG, "Connected to GATT server")
|
Log.i(TAG, "Connected to GATT server")
|
||||||
|
|
||||||
handler.post { connectionStateCallback?.invoke(true) }
|
handler.post { connectionStateCallback?.invoke(true) }
|
||||||
|
|
||||||
|
deviceAddress?.let { address ->
|
||||||
|
handler.post { connectionSuccessCallback?.invoke(address) }
|
||||||
|
}
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
try {
|
try {
|
||||||
@@ -406,17 +487,20 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
BluetoothProfile.STATE_DISCONNECTED -> {
|
BluetoothProfile.STATE_DISCONNECTED -> {
|
||||||
isConnected = false
|
isConnected = false
|
||||||
isReconnecting = false
|
isReconnecting = false
|
||||||
Log.i(TAG, "Disconnected from GATT server")
|
Log.i(TAG, "Disconnected from GATT server, manual=$isManualDisconnect")
|
||||||
|
|
||||||
handler.post { connectionStateCallback?.invoke(false) }
|
handler.post {
|
||||||
|
connectionStateCallback?.invoke(false)
|
||||||
|
if (!isManualDisconnect) {
|
||||||
if (!deviceAddress.isNullOrBlank() && autoReconnect) {
|
connectionLostCallback?.invoke()
|
||||||
handler.post {
|
|
||||||
Log.d(TAG, "Immediate reconnection after disconnect")
|
|
||||||
connect(deviceAddress!!, connectionStateCallback)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
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 getConnectionAttempts(): Int = connectionAttempts
|
||||||
|
|
||||||
fun getLastKnownDeviceAddress(): String? = lastKnownDeviceAddress
|
fun getLastKnownDeviceAddress(): String? = lastKnownDeviceAddress
|
||||||
@@ -638,9 +802,16 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
fun disconnectAndCleanup() {
|
fun disconnectAndCleanup() {
|
||||||
isConnected = false
|
isConnected = false
|
||||||
autoReconnect = false
|
autoReconnect = false
|
||||||
|
highFrequencyReconnect = false
|
||||||
|
isManualDisconnect = false
|
||||||
|
isAutoConnectBlocked = false
|
||||||
|
stopHighFrequencyReconnect()
|
||||||
|
stopScan()
|
||||||
|
|
||||||
bluetoothGatt?.let { gatt ->
|
bluetoothGatt?.let { gatt ->
|
||||||
try {
|
try {
|
||||||
gatt.disconnect()
|
gatt.disconnect()
|
||||||
|
Thread.sleep(200)
|
||||||
gatt.close()
|
gatt.close()
|
||||||
Log.d(TAG, "GATT connection cleaned up")
|
Log.d(TAG, "GATT connection cleaned up")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -650,6 +821,14 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
deviceAddress = null
|
deviceAddress = null
|
||||||
connectionAttempts = 0
|
connectionAttempts = 0
|
||||||
|
|
||||||
|
dataBuffer.clear()
|
||||||
|
connectionStateCallback = null
|
||||||
|
statusCallback = null
|
||||||
|
trainInfoCallback = null
|
||||||
|
connectionLostCallback = null
|
||||||
|
connectionSuccessCallback = null
|
||||||
|
|
||||||
Log.d(TAG, "BLE client fully disconnected and cleaned up")
|
Log.d(TAG, "BLE client fully disconnected and cleaned up")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,12 +24,14 @@ import androidx.compose.animation.*
|
|||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.*
|
import androidx.compose.material.icons.filled.*
|
||||||
import androidx.compose.material.icons.filled.LocationOn
|
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.TrainRecordManager
|
||||||
import org.noxylva.lbjconsole.model.MergeSettings
|
import org.noxylva.lbjconsole.model.MergeSettings
|
||||||
import org.noxylva.lbjconsole.ui.screens.HistoryScreen
|
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.MapScreen
|
||||||
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
|
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
|
||||||
|
|
||||||
@@ -74,7 +76,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
|
|
||||||
private var deviceStatus by mutableStateOf("未连接")
|
private var deviceStatus by mutableStateOf("未连接")
|
||||||
private var deviceAddress by mutableStateOf("")
|
private var deviceAddress by mutableStateOf<String?>(null)
|
||||||
private var isScanning by mutableStateOf(false)
|
private var isScanning by mutableStateOf(false)
|
||||||
private var foundDevices by mutableStateOf(listOf<BluetoothDevice>())
|
private var foundDevices by mutableStateOf(listOf<BluetoothDevice>())
|
||||||
private var scanResults = mutableListOf<ScanResult>()
|
private var scanResults = mutableListOf<ScanResult>()
|
||||||
@@ -82,7 +84,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
private var showConnectionDialog by mutableStateOf(false)
|
private var showConnectionDialog by mutableStateOf(false)
|
||||||
private var lastUpdateTime by mutableStateOf<Date?>(null)
|
private var lastUpdateTime by mutableStateOf<Date?>(null)
|
||||||
private var latestRecord by mutableStateOf<TrainRecord?>(null)
|
private var latestRecord by mutableStateOf<TrainRecord?>(null)
|
||||||
private var recentRecords by mutableStateOf<List<TrainRecord>>(emptyList())
|
private var recentRecords = mutableStateListOf<TrainRecord>()
|
||||||
|
|
||||||
|
|
||||||
private var filterTrain by mutableStateOf("")
|
private var filterTrain by mutableStateOf("")
|
||||||
@@ -110,6 +112,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
|
|
||||||
private var targetDeviceName = "LBJReceiver"
|
private var targetDeviceName = "LBJReceiver"
|
||||||
|
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
|
||||||
|
private var searchOrderList by mutableStateOf(listOf<String>())
|
||||||
|
private var showDisconnectButton by mutableStateOf(false)
|
||||||
|
|
||||||
|
|
||||||
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
|
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
|
||||||
@@ -204,6 +209,27 @@ class MainActivity : ComponentActivity() {
|
|||||||
handleTrainInfo(jsonData)
|
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 {
|
lifecycleScope.launch {
|
||||||
try {
|
try {
|
||||||
@@ -227,8 +253,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
osmdroidBasePath = osmCacheDir
|
osmdroidBasePath = osmCacheDir
|
||||||
osmdroidTileCache = tileCache
|
osmdroidTileCache = tileCache
|
||||||
expirationOverrideDuration = 86400000L * 7
|
expirationOverrideDuration = 86400000L * 7
|
||||||
tileDownloadThreads = 2
|
tileDownloadThreads = 4
|
||||||
tileFileSystemThreads = 2
|
tileFileSystemThreads = 4
|
||||||
|
|
||||||
setUserAgentValue("LBJReceiver/1.0")
|
setUserAgentValue("LBJReceiver/1.0")
|
||||||
}
|
}
|
||||||
@@ -259,7 +285,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentTab = tab
|
currentTab = tab
|
||||||
saveSettings()
|
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,
|
latestRecord = latestRecord,
|
||||||
@@ -270,7 +313,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
Log.d(TAG, "Record clicked train=${record.train}")
|
Log.d(TAG, "Record clicked train=${record.train}")
|
||||||
},
|
},
|
||||||
onClearMonitorLog = {
|
onClearMonitorLog = {
|
||||||
recentRecords = emptyList()
|
recentRecords.clear()
|
||||||
temporaryStatusMessage = null
|
temporaryStatusMessage = null
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -337,7 +380,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
onClearRecords = {
|
onClearRecords = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
trainRecordManager.clearRecords()
|
trainRecordManager.clearRecords()
|
||||||
recentRecords = emptyList()
|
recentRecords.clear()
|
||||||
latestRecord = null
|
latestRecord = null
|
||||||
temporaryStatusMessage = null
|
temporaryStatusMessage = null
|
||||||
}
|
}
|
||||||
@@ -367,12 +410,24 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (showConnectionDialog) {
|
if (showConnectionDialog) {
|
||||||
|
LaunchedEffect(showConnectionDialog) {
|
||||||
|
bleClient.setDialogOpen(true)
|
||||||
|
if (!bleClient.isConnected() && !isScanning) {
|
||||||
|
foundDevices = emptyList()
|
||||||
|
startScan()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ConnectionDialog(
|
ConnectionDialog(
|
||||||
isScanning = isScanning,
|
isScanning = isScanning,
|
||||||
devices = foundDevices,
|
devices = foundDevices,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
showConnectionDialog = false
|
showConnectionDialog = false
|
||||||
stopScan()
|
stopScan()
|
||||||
|
bleClient.resetManualDisconnectState()
|
||||||
|
if (!bleClient.isConnected()) {
|
||||||
|
startAutoScanAndConnect()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onScan = {
|
onScan = {
|
||||||
if (isScanning) {
|
if (isScanning) {
|
||||||
@@ -383,9 +438,25 @@ class MainActivity : ComponentActivity() {
|
|||||||
},
|
},
|
||||||
onConnect = { device ->
|
onConnect = { device ->
|
||||||
showConnectionDialog = false
|
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) {
|
private fun connectToDevice(device: BluetoothDevice) {
|
||||||
|
bleClient.setAutoConnectBlocked(false)
|
||||||
deviceStatus = "正在连接..."
|
deviceStatus = "正在连接..."
|
||||||
Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}")
|
Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}")
|
||||||
|
|
||||||
@@ -412,9 +484,17 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (connected) {
|
if (connected) {
|
||||||
deviceStatus = "已连接"
|
deviceStatus = "已连接"
|
||||||
temporaryStatusMessage = null
|
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"}")
|
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
|
||||||
} else {
|
} else {
|
||||||
deviceStatus = "连接失败,正在重试..."
|
deviceStatus = "连接失败,正在重试..."
|
||||||
|
showDisconnectButton = false
|
||||||
Log.e(TAG, "Connection failed, auto-retry enabled for name=${device.name ?: "Unknown"}")
|
Log.e(TAG, "Connection failed, auto-retry enabled for name=${device.name ?: "Unknown"}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,6 +503,43 @@ class MainActivity : ComponentActivity() {
|
|||||||
deviceAddress = device.address
|
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) {
|
private fun handleTrainInfo(jsonData: JSONObject) {
|
||||||
Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...")
|
Log.d(TAG, "Received train data=${jsonData.toString().take(50)}...")
|
||||||
@@ -445,10 +562,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
latestRecord = record
|
latestRecord = record
|
||||||
|
|
||||||
val newList = mutableListOf<TrainRecord>()
|
recentRecords.removeAll { it.train == record.train && it.time == record.time }
|
||||||
newList.add(record)
|
recentRecords.add(0, record)
|
||||||
newList.addAll(recentRecords.filterNot { it.train == record.train && it.time == record.time })
|
if (recentRecords.size > 10) {
|
||||||
recentRecords = newList.take(10)
|
recentRecords.removeRange(10, recentRecords.size)
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(TAG, "Updated UI train=${record.train}")
|
Log.d(TAG, "Updated UI train=${record.train}")
|
||||||
forceUiRefresh()
|
forceUiRefresh()
|
||||||
@@ -544,17 +662,20 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
isScanning = true
|
isScanning = true
|
||||||
foundDevices = emptyList()
|
foundDevices = emptyList()
|
||||||
|
|
||||||
val targetDeviceName = if (settingsDeviceName.isNotBlank() && settingsDeviceName != "LBJReceiver") {
|
val targetDeviceName = if (settingsDeviceName.isNotBlank() && settingsDeviceName != "LBJReceiver") {
|
||||||
settingsDeviceName
|
settingsDeviceName
|
||||||
} else {
|
} else {
|
||||||
null
|
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 ->
|
bleClient.scanDevices(targetDeviceName) { device ->
|
||||||
if (!foundDevices.any { it.address == device.address }) {
|
runOnUiThread {
|
||||||
Log.d(TAG, "Found device name=${device.name ?: "Unknown"} address=${device.address}")
|
if (!foundDevices.any { it.address == device.address }) {
|
||||||
foundDevices = foundDevices + device
|
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
|
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)
|
.putInt("settings_scroll_position", settingsScrollPosition)
|
||||||
.putFloat("map_zoom_level", mapZoomLevel.toFloat())
|
.putFloat("map_zoom_level", mapZoomLevel.toFloat())
|
||||||
.putBoolean("map_railway_visible", mapRailwayLayerVisible)
|
.putBoolean("map_railway_visible", mapRailwayLayerVisible)
|
||||||
|
.putString("specified_device_address", specifiedDeviceAddress)
|
||||||
|
.putString("search_order_list", searchOrderList.joinToString(","))
|
||||||
|
|
||||||
mapCenterPosition?.let { (lat, lon) ->
|
mapCenterPosition?.let { (lat, lon) ->
|
||||||
editor.putFloat("map_center_lat", lat.toFloat())
|
editor.putFloat("map_center_lat", lat.toFloat())
|
||||||
@@ -657,9 +791,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
Log.d(TAG, "App resumed")
|
Log.d(TAG, "App resumed")
|
||||||
|
|
||||||
|
bleClient.setHighFrequencyReconnect(true)
|
||||||
|
|
||||||
if (hasBluetoothPermissions() && !bleClient.isConnected()) {
|
if (hasBluetoothPermissions() && !bleClient.isConnected()) {
|
||||||
Log.d(TAG, "App resumed and not connected, starting auto scan")
|
Log.d(TAG, "App resumed and not connected, starting auto scan")
|
||||||
startAutoScanAndConnect()
|
startAutoScanAndConnect()
|
||||||
|
} else if (bleClient.isConnected()) {
|
||||||
|
showDisconnectButton = true
|
||||||
|
deviceStatus = "已连接"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -669,6 +808,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
if (isFinishing) {
|
if (isFinishing) {
|
||||||
bleClient.disconnectAndCleanup()
|
bleClient.disconnectAndCleanup()
|
||||||
Log.d(TAG, "App finishing, BLE cleaned up")
|
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")
|
Log.d(TAG, "App paused, settings saved")
|
||||||
}
|
}
|
||||||
@@ -683,6 +825,11 @@ fun MainContent(
|
|||||||
currentTab: Int,
|
currentTab: Int,
|
||||||
onTabChange: (Int) -> Unit,
|
onTabChange: (Int) -> Unit,
|
||||||
onConnectClick: () -> Unit,
|
onConnectClick: () -> Unit,
|
||||||
|
onDisconnectClick: () -> Unit,
|
||||||
|
showDisconnectButton: Boolean,
|
||||||
|
specifiedDeviceAddress: String?,
|
||||||
|
searchOrderList: List<String>,
|
||||||
|
onSpecifiedDeviceSelected: (String?) -> Unit,
|
||||||
|
|
||||||
|
|
||||||
latestRecord: TrainRecord?,
|
latestRecord: TrainRecord?,
|
||||||
@@ -794,22 +941,31 @@ fun MainContent(
|
|||||||
if (historyEditMode && currentTab == 0) {
|
if (historyEditMode && currentTab == 0) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
val totalSelectedCount = historySelectedRecords.sumOf { selectedId ->
|
val totalSelectedCount = run {
|
||||||
allRecords.find { item ->
|
val processedMergedRecords = mutableSetOf<String>()
|
||||||
when (item) {
|
var count = 0
|
||||||
is TrainRecord -> item.uniqueId == selectedId
|
|
||||||
is org.noxylva.lbjconsole.model.MergedTrainRecord ->
|
historySelectedRecords.forEach { selectedId ->
|
||||||
item.records.any { it.uniqueId == selectedId }
|
val foundItem = allRecords.find { item ->
|
||||||
else -> false
|
when (item) {
|
||||||
}
|
is TrainRecord -> item.uniqueId == selectedId
|
||||||
}?.let { item ->
|
is org.noxylva.lbjconsole.model.MergedTrainRecord -> item.records.any { it.uniqueId == selectedId }
|
||||||
when (item) {
|
else -> false
|
||||||
is TrainRecord -> 1
|
}
|
||||||
is org.noxylva.lbjconsole.model.MergedTrainRecord -> item.records.size
|
}
|
||||||
else -> 0
|
|
||||||
}
|
when (foundItem) {
|
||||||
} ?: 0
|
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(
|
Text(
|
||||||
"已选择 $totalSelectedCount 条记录",
|
"已选择 $totalSelectedCount 条记录",
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
@@ -833,34 +989,34 @@ fun MainContent(
|
|||||||
val recordsToDelete = mutableSetOf<TrainRecord>()
|
val recordsToDelete = mutableSetOf<TrainRecord>()
|
||||||
val idToRecordMap = mutableMapOf<String, TrainRecord>()
|
val idToRecordMap = mutableMapOf<String, TrainRecord>()
|
||||||
val idToMergedRecordMap = mutableMapOf<String, org.noxylva.lbjconsole.model.MergedTrainRecord>()
|
val idToMergedRecordMap = mutableMapOf<String, org.noxylva.lbjconsole.model.MergedTrainRecord>()
|
||||||
|
|
||||||
allRecords.forEach { item ->
|
allRecords.forEach { item ->
|
||||||
when (item) {
|
when (item) {
|
||||||
is TrainRecord -> {
|
is TrainRecord -> {
|
||||||
idToRecordMap[item.uniqueId] = item
|
idToRecordMap[item.uniqueId] = item
|
||||||
}
|
}
|
||||||
is org.noxylva.lbjconsole.model.MergedTrainRecord -> {
|
is org.noxylva.lbjconsole.model.MergedTrainRecord -> {
|
||||||
item.records.forEach { record ->
|
item.records.forEach { record ->
|
||||||
idToRecordMap[record.uniqueId] = record
|
idToRecordMap[record.uniqueId] = record
|
||||||
idToMergedRecordMap[record.uniqueId] = item
|
idToMergedRecordMap[record.uniqueId] = item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val processedMergedRecords = mutableSetOf<org.noxylva.lbjconsole.model.MergedTrainRecord>()
|
val processedMergedRecordKeys = mutableSetOf<String>()
|
||||||
|
|
||||||
historySelectedRecords.forEach { selectedId ->
|
historySelectedRecords.forEach { selectedId ->
|
||||||
val mergedRecord = idToMergedRecordMap[selectedId]
|
val mergedRecord = idToMergedRecordMap[selectedId]
|
||||||
if (mergedRecord != null && !processedMergedRecords.contains(mergedRecord)) {
|
if (mergedRecord != null && !processedMergedRecordKeys.contains(mergedRecord.groupKey)) {
|
||||||
recordsToDelete.addAll(mergedRecord.records)
|
recordsToDelete.addAll(mergedRecord.records)
|
||||||
processedMergedRecords.add(mergedRecord)
|
processedMergedRecordKeys.add(mergedRecord.groupKey)
|
||||||
} else if (mergedRecord == null) {
|
} else if (mergedRecord == null) {
|
||||||
idToRecordMap[selectedId]?.let { record ->
|
idToRecordMap[selectedId]?.let { record ->
|
||||||
recordsToDelete.add(record)
|
recordsToDelete.add(record)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeleteRecords(recordsToDelete.toList())
|
onDeleteRecords(recordsToDelete.toList())
|
||||||
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
||||||
@@ -940,7 +1096,10 @@ fun MainContent(
|
|||||||
mergeSettings = mergeSettings,
|
mergeSettings = mergeSettings,
|
||||||
onMergeSettingsChange = onMergeSettingsChange,
|
onMergeSettingsChange = onMergeSettingsChange,
|
||||||
scrollPosition = settingsScrollPosition,
|
scrollPosition = settingsScrollPosition,
|
||||||
onScrollPositionChange = onSettingsScrollPositionChange
|
onScrollPositionChange = onSettingsScrollPositionChange,
|
||||||
|
specifiedDeviceAddress = specifiedDeviceAddress,
|
||||||
|
searchOrderList = searchOrderList,
|
||||||
|
onSpecifiedDeviceSelected = onSpecifiedDeviceSelected
|
||||||
)
|
)
|
||||||
3 -> MapScreen(
|
3 -> MapScreen(
|
||||||
records = if (allRecords.isNotEmpty()) {
|
records = if (allRecords.isNotEmpty()) {
|
||||||
@@ -973,7 +1132,11 @@ fun ConnectionDialog(
|
|||||||
devices: List<BluetoothDevice>,
|
devices: List<BluetoothDevice>,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
onScan: () -> Unit,
|
onScan: () -> Unit,
|
||||||
onConnect: (BluetoothDevice) -> Unit
|
onConnect: (BluetoothDevice) -> Unit,
|
||||||
|
onDisconnect: () -> Unit = {},
|
||||||
|
isConnected: Boolean = false,
|
||||||
|
targetDeviceName: String = "LBJReceiver",
|
||||||
|
deviceAddress: String? = null
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -987,39 +1150,41 @@ fun ConnectionDialog(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Button(
|
if (!isConnected) {
|
||||||
onClick = onScan,
|
Button(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
onClick = onScan,
|
||||||
colors = ButtonDefaults.buttonColors(
|
modifier = Modifier.fillMaxWidth(),
|
||||||
containerColor = if (isScanning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
|
colors = ButtonDefaults.buttonColors(
|
||||||
)
|
containerColor = if (isScanning) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary
|
||||||
) {
|
)
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
) {
|
||||||
if (isScanning) {
|
Row(
|
||||||
CircularProgressIndicator(
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.size(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
strokeWidth = 2.dp,
|
) {
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
if (isScanning) {
|
||||||
)
|
CircularProgressIndicator(
|
||||||
} else {
|
modifier = Modifier.size(16.dp),
|
||||||
Icon(
|
strokeWidth = 2.dp,
|
||||||
imageVector = Icons.Default.Search,
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
contentDescription = null
|
)
|
||||||
|
} 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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
if (devices.isNotEmpty()) {
|
if (devices.isNotEmpty() && !isConnected) {
|
||||||
Text(
|
Text(
|
||||||
text = "发现 ${devices.size} 个设备",
|
text = "发现 ${devices.size} 个设备",
|
||||||
style = MaterialTheme.typography.titleSmall,
|
style = MaterialTheme.typography.titleSmall,
|
||||||
@@ -1031,20 +1196,39 @@ fun ConnectionDialog(
|
|||||||
modifier = Modifier.heightIn(max = 200.dp),
|
modifier = Modifier.heightIn(max = 200.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(devices) { device ->
|
items(devices.filter { !isConnected }) { device ->
|
||||||
var isPressed by remember { mutableStateOf(false) }
|
var isPressed by remember { mutableStateOf(false) }
|
||||||
|
var isHovered by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val cardScale by animateFloatAsState(
|
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(
|
animationSpec = tween(
|
||||||
durationMillis = 120,
|
durationMillis = 150,
|
||||||
easing = LinearEasing
|
easing = FastOutSlowInEasing
|
||||||
)
|
),
|
||||||
|
label = "cardElevation"
|
||||||
)
|
)
|
||||||
|
|
||||||
LaunchedEffect(isPressed) {
|
LaunchedEffect(isPressed) {
|
||||||
if (isPressed) {
|
if (isPressed) {
|
||||||
delay(100)
|
delay(120)
|
||||||
isPressed = false
|
isPressed = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1055,8 +1239,18 @@ fun ConnectionDialog(
|
|||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = cardScale
|
scaleX = cardScale
|
||||||
scaleY = 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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1098,7 +1292,9 @@ fun ConnectionDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!isScanning) {
|
} else if (isScanning && !isConnected) {
|
||||||
|
|
||||||
|
} else {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -1109,25 +1305,51 @@ fun ConnectionDialog(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.BluetoothSearching,
|
imageVector = if (isConnected) Icons.Default.Bluetooth else Icons.Default.BluetoothSearching,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(48.dp),
|
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))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "未发现设备",
|
text = if (isConnected) "设备已连接" else "未发现设备",
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = if (isConnected)
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "请确保设备已开启并处于可发现状态",
|
text = if (isConnected)
|
||||||
|
deviceAddress?.ifEmpty { "未知地址" } ?: "未知地址"
|
||||||
|
else
|
||||||
|
"请确保设备已开启并处于可发现状态",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f),
|
||||||
textAlign = TextAlign.Center
|
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("断开连接")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.content.Context
|
|||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import kotlinx.coroutines.*
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -26,13 +27,16 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
|
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
|
||||||
private val recordCount = AtomicInteger(0)
|
private val recordCount = AtomicInteger(0)
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
var mergeSettings = MergeSettings()
|
var mergeSettings = MergeSettings()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadRecords()
|
ioScope.launch {
|
||||||
loadMergeSettings()
|
loadRecords()
|
||||||
|
loadMergeSettings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -145,15 +149,17 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveRecords() {
|
private fun saveRecords() {
|
||||||
try {
|
ioScope.launch {
|
||||||
val jsonArray = JSONArray()
|
try {
|
||||||
for (record in trainRecords) {
|
val jsonArray = JSONArray()
|
||||||
jsonArray.put(record.toJSON())
|
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() {
|
private fun saveMergeSettings() {
|
||||||
try {
|
ioScope.launch {
|
||||||
val json = JSONObject().apply {
|
try {
|
||||||
put("enabled", mergeSettings.enabled)
|
val json = JSONObject().apply {
|
||||||
put("groupBy", mergeSettings.groupBy.name)
|
put("enabled", mergeSettings.enabled)
|
||||||
put("timeWindow", mergeSettings.timeWindow.name)
|
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}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 != "<NUL>" && trainTime != "NA" && trainTime != "<NA>") {
|
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<TrainRecord>,
|
|
||||||
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<TrainRecord>,
|
|
||||||
onRecordClick: (TrainRecord) -> Unit,
|
|
||||||
onFilterClick: () -> Unit,
|
|
||||||
onClearClick: () -> Unit,
|
|
||||||
onDeleteRecords: (List<TrainRecord>) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var selectedRecords by remember { mutableStateOf<MutableSet<TrainRecord>>(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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -55,42 +55,28 @@ fun TrainRecordItem(
|
|||||||
locoInfoUtil: LocoInfoUtil?,
|
locoInfoUtil: LocoInfoUtil?,
|
||||||
onRecordClick: (TrainRecord) -> Unit,
|
onRecordClick: (TrainRecord) -> Unit,
|
||||||
onToggleSelection: (TrainRecord) -> Unit,
|
onToggleSelection: (TrainRecord) -> Unit,
|
||||||
onLongClick: (TrainRecord) -> Unit
|
onLongClick: (TrainRecord) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val recordId = record.uniqueId
|
val recordId = record.uniqueId
|
||||||
val isExpanded = expandedStatesMap[recordId] == true
|
val isExpanded = expandedStatesMap[recordId] == true
|
||||||
|
|
||||||
val cardColor by animateColorAsState(
|
val cardColor = when {
|
||||||
targetValue = when {
|
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
else -> MaterialTheme.colorScheme.surface
|
||||||
else -> MaterialTheme.colorScheme.surface
|
}
|
||||||
},
|
|
||||||
animationSpec = tween(durationMillis = 200),
|
|
||||||
label = "cardColor"
|
|
||||||
)
|
|
||||||
|
|
||||||
val cardScale by animateFloatAsState(
|
val cardScale = if (isSelected) 0.98f else 1f
|
||||||
targetValue = if (isSelected) 0.98f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 150),
|
|
||||||
label = "cardScale"
|
|
||||||
)
|
|
||||||
|
|
||||||
val cardElevation by animateDpAsState(
|
val cardElevation = if (isSelected) 6.dp else 2.dp
|
||||||
targetValue = if (isSelected) 6.dp else 2.dp,
|
|
||||||
animationSpec = tween(durationMillis = 200),
|
|
||||||
label = "cardElevation"
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = cardScale
|
scaleX = cardScale
|
||||||
scaleY = cardScale
|
scaleY = cardScale
|
||||||
}
|
},
|
||||||
.animateContentSize(
|
|
||||||
animationSpec = tween(durationMillis = 150, easing = LinearEasing)
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = cardColor
|
containerColor = cardColor
|
||||||
@@ -290,18 +276,15 @@ fun TrainRecordItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isExpanded,
|
visible = isExpanded,
|
||||||
enter = expandVertically(animationSpec = tween(durationMillis = 300)) + fadeIn(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 = tween(durationMillis = 300)) + fadeOut(animationSpec = tween(durationMillis = 300))
|
exit = shrinkVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow))
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
val coordinates = remember { record.getCoordinates() }
|
val coordinates = remember { record.getCoordinates() }
|
||||||
|
|
||||||
if (coordinates != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (coordinates != null) {
|
if (coordinates != null) {
|
||||||
Box(
|
Box(
|
||||||
@@ -413,7 +396,8 @@ fun MergedTrainRecordItem(
|
|||||||
isInEditMode: Boolean = false,
|
isInEditMode: Boolean = false,
|
||||||
selectedRecords: List<TrainRecord> = emptyList(),
|
selectedRecords: List<TrainRecord> = emptyList(),
|
||||||
onToggleSelection: (TrainRecord) -> Unit = {},
|
onToggleSelection: (TrainRecord) -> Unit = {},
|
||||||
onLongClick: (TrainRecord) -> Unit = {}
|
onLongClick: (TrainRecord) -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val recordId = mergedRecord.groupKey
|
val recordId = mergedRecord.groupKey
|
||||||
val isExpanded = expandedStatesMap[recordId] == true
|
val isExpanded = expandedStatesMap[recordId] == true
|
||||||
@@ -421,37 +405,22 @@ fun MergedTrainRecordItem(
|
|||||||
|
|
||||||
val hasSelectedRecords = mergedRecord.records.any { selectedRecords.contains(it) }
|
val hasSelectedRecords = mergedRecord.records.any { selectedRecords.contains(it) }
|
||||||
|
|
||||||
val cardColor by animateColorAsState(
|
val cardColor = when {
|
||||||
targetValue = when {
|
hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer
|
||||||
hasSelectedRecords -> MaterialTheme.colorScheme.primaryContainer
|
else -> MaterialTheme.colorScheme.surface
|
||||||
else -> MaterialTheme.colorScheme.surface
|
}
|
||||||
},
|
|
||||||
animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing),
|
|
||||||
label = "mergedCardColor"
|
|
||||||
)
|
|
||||||
|
|
||||||
val cardScale by animateFloatAsState(
|
val cardScale = if (hasSelectedRecords) 0.98f else 1f
|
||||||
targetValue = if (hasSelectedRecords) 0.98f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 150, easing = LinearEasing),
|
|
||||||
label = "mergedCardScale"
|
|
||||||
)
|
|
||||||
|
|
||||||
val cardElevation by animateDpAsState(
|
val cardElevation = if (hasSelectedRecords) 6.dp else 2.dp
|
||||||
targetValue = if (hasSelectedRecords) 6.dp else 2.dp,
|
|
||||||
animationSpec = tween(durationMillis = 200, easing = LinearEasing),
|
|
||||||
label = "mergedCardElevation"
|
|
||||||
)
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
scaleX = cardScale
|
scaleX = cardScale
|
||||||
scaleY = cardScale
|
scaleY = cardScale
|
||||||
}
|
},
|
||||||
.animateContentSize(
|
|
||||||
animationSpec = tween(durationMillis = 150, easing = LinearEasing)
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = cardColor
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -752,22 +724,22 @@ fun MergedTrainRecordItem(
|
|||||||
},
|
},
|
||||||
update = { mapView -> mapView.invalidate() }
|
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))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
HorizontalDivider()
|
||||||
text = recordMap["position_info"] ?: "",
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
mergedRecord.records.filter { it != mergedRecord.latestRecord }.forEach { recordItem ->
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
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())
|
val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -858,6 +830,7 @@ fun MergedTrainRecordItem(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1020,6 +993,7 @@ fun HistoryScreen(
|
|||||||
when (item) {
|
when (item) {
|
||||||
is TrainRecord -> {
|
is TrainRecord -> {
|
||||||
TrainRecordItem(
|
TrainRecordItem(
|
||||||
|
modifier = Modifier,
|
||||||
record = item,
|
record = item,
|
||||||
isSelected = selectedRecordsList.contains(item),
|
isSelected = selectedRecordsList.contains(item),
|
||||||
isInEditMode = isInEditMode,
|
isInEditMode = isInEditMode,
|
||||||
@@ -1045,6 +1019,7 @@ fun HistoryScreen(
|
|||||||
}
|
}
|
||||||
is MergedTrainRecord -> {
|
is MergedTrainRecord -> {
|
||||||
MergedTrainRecordItem(
|
MergedTrainRecordItem(
|
||||||
|
modifier = Modifier,
|
||||||
mergedRecord = item,
|
mergedRecord = item,
|
||||||
expandedStatesMap = expandedStatesMap,
|
expandedStatesMap = expandedStatesMap,
|
||||||
locoInfoUtil = locoInfoUtil,
|
locoInfoUtil = locoInfoUtil,
|
||||||
|
|||||||
@@ -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<MergedTrainRecord>,
|
|
||||||
latestRecord: TrainRecord?,
|
|
||||||
lastUpdateTime: Date?,
|
|
||||||
temporaryStatusMessage: String? = null,
|
|
||||||
locoInfoUtil: LocoInfoUtil? = null,
|
|
||||||
onClearRecords: () -> Unit = {},
|
|
||||||
onRecordClick: (TrainRecord) -> Unit = {},
|
|
||||||
onClearLog: () -> Unit = {},
|
|
||||||
onDeleteRecords: (List<TrainRecord>) -> Unit = {},
|
|
||||||
onDeleteMergedRecord: (MergedTrainRecord) -> Unit = {},
|
|
||||||
editMode: Boolean = false,
|
|
||||||
selectedRecords: Set<String> = emptySet(),
|
|
||||||
expandedStates: Map<String, Boolean> = emptyMap(),
|
|
||||||
scrollPosition: Int = 0,
|
|
||||||
scrollOffset: Int = 0,
|
|
||||||
onStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit = { _, _, _, _, _ -> }
|
|
||||||
) {
|
|
||||||
var isInEditMode by remember(editMode) { mutableStateOf(editMode) }
|
|
||||||
val selectedRecordsList = remember(selectedRecords) {
|
|
||||||
mutableStateListOf<TrainRecord>().apply {
|
|
||||||
addAll(mergedRecords.flatMap { it.records }.filter {
|
|
||||||
selectedRecords.contains(it.uniqueId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val expandedStatesMap = remember(expandedStates) {
|
|
||||||
mutableStateMapOf<String, Boolean>().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 != "<NUL>") {
|
|
||||||
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 != "<NUL>"
|
|
||||||
|
|
||||||
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 != "<NUL>"
|
|
||||||
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 != "<NUL>") {
|
|
||||||
append(recordItem.route)
|
|
||||||
}
|
|
||||||
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
|
|
||||||
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 != "<NUL>" &&
|
|
||||||
!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()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,10 @@ fun SettingsScreen(
|
|||||||
mergeSettings: MergeSettings,
|
mergeSettings: MergeSettings,
|
||||||
onMergeSettingsChange: (MergeSettings) -> Unit,
|
onMergeSettingsChange: (MergeSettings) -> Unit,
|
||||||
scrollPosition: Int = 0,
|
scrollPosition: Int = 0,
|
||||||
onScrollPositionChange: (Int) -> Unit = {}
|
onScrollPositionChange: (Int) -> Unit = {},
|
||||||
|
specifiedDeviceAddress: String? = null,
|
||||||
|
searchOrderList: List<String> = emptyList(),
|
||||||
|
onSpecifiedDeviceSelected: (String?) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
@@ -95,6 +98,69 @@ fun SettingsScreen(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(12.dp)
|
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()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.clip(RoundedCornerShape(12.dp))
|
||||||
.clickable {
|
.clickable {
|
||||||
uriHandler.openUri("https://github.com/undef-i")
|
uriHandler.openUri("https://github.com/undef-i/LBJ_Console")
|
||||||
}
|
}
|
||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user