feat: add timestamp logging for received messages, optimize page details
This commit is contained in:
@@ -3,12 +3,9 @@
|
||||
LBJ Console is an Android app designed to receive and display LBJ messages via BLE from the [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device.
|
||||
|
||||
## Roadmap
|
||||
- Tab state persistence
|
||||
- Record filtering (train number, time range)
|
||||
- Record management page optimization
|
||||
- Optional train merge by locomotive/number
|
||||
- Offline data storage
|
||||
- Add record timestamps
|
||||
|
||||
# License
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
applicationId = "org.noxylva.lbjconsole"
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 2
|
||||
versionName = "0.0.2"
|
||||
versionCode = 3
|
||||
versionName = "0.0.3"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -72,7 +72,17 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
|
||||
private var settingsDeviceName by mutableStateOf("LBJReceiver")
|
||||
private var temporaryStatusMessage by mutableStateOf<String?>(null)
|
||||
private var temporaryStatusMessage by mutableStateOf<String?>(null)
|
||||
|
||||
|
||||
private var historyEditMode by mutableStateOf(false)
|
||||
private var historySelectedRecords by mutableStateOf<Set<String>>(emptySet())
|
||||
private var historyExpandedStates by mutableStateOf<Map<String, Boolean>>(emptyMap())
|
||||
private var historyScrollPosition by mutableStateOf(0)
|
||||
private var historyScrollOffset by mutableStateOf(0)
|
||||
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
||||
private var mapZoomLevel by mutableStateOf(10.0)
|
||||
private var mapRailwayLayerVisible by mutableStateOf(true)
|
||||
|
||||
|
||||
private var targetDeviceName = "LBJReceiver"
|
||||
@@ -205,6 +215,8 @@ class MainActivity : ComponentActivity() {
|
||||
Log.e(TAG, "OSM cache config failed", e)
|
||||
}
|
||||
|
||||
saveSettings()
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
LBJReceiverTheme {
|
||||
@@ -216,7 +228,10 @@ class MainActivity : ComponentActivity() {
|
||||
isConnected = bleClient.isConnected(),
|
||||
isScanning = isScanning,
|
||||
currentTab = currentTab,
|
||||
onTabChange = { tab -> currentTab = tab },
|
||||
onTabChange = { tab ->
|
||||
currentTab = tab
|
||||
saveSettings()
|
||||
},
|
||||
onConnectClick = { showConnectionDialog = true },
|
||||
|
||||
|
||||
@@ -239,6 +254,32 @@ class MainActivity : ComponentActivity() {
|
||||
filterTrain = filterTrain,
|
||||
filterRoute = filterRoute,
|
||||
filterDirection = filterDirection,
|
||||
|
||||
|
||||
historyEditMode = historyEditMode,
|
||||
historySelectedRecords = historySelectedRecords,
|
||||
historyExpandedStates = historyExpandedStates,
|
||||
historyScrollPosition = historyScrollPosition,
|
||||
historyScrollOffset = historyScrollOffset,
|
||||
onHistoryStateChange = { editMode, selectedRecords, expandedStates, scrollPosition, scrollOffset ->
|
||||
historyEditMode = editMode
|
||||
historySelectedRecords = selectedRecords
|
||||
historyExpandedStates = expandedStates
|
||||
historyScrollPosition = scrollPosition
|
||||
historyScrollOffset = scrollOffset
|
||||
saveSettings()
|
||||
},
|
||||
|
||||
|
||||
mapCenterPosition = mapCenterPosition,
|
||||
mapZoomLevel = mapZoomLevel,
|
||||
mapRailwayLayerVisible = mapRailwayLayerVisible,
|
||||
onMapStateChange = { centerPos, zoomLevel, railwayVisible ->
|
||||
mapCenterPosition = centerPos
|
||||
mapZoomLevel = zoomLevel
|
||||
mapRailwayLayerVisible = railwayVisible
|
||||
saveSettings()
|
||||
},
|
||||
onFilterChange = { train, route, direction ->
|
||||
filterTrain = train
|
||||
filterRoute = route
|
||||
@@ -259,11 +300,7 @@ class MainActivity : ComponentActivity() {
|
||||
temporaryStatusMessage = null
|
||||
}
|
||||
},
|
||||
onExportRecords = {
|
||||
scope.launch {
|
||||
exportRecordsToCSV()
|
||||
}
|
||||
},
|
||||
|
||||
onDeleteRecords = { records ->
|
||||
scope.launch {
|
||||
val deletedCount = trainRecordManager.deleteRecords(records)
|
||||
@@ -294,7 +331,6 @@ class MainActivity : ComponentActivity() {
|
||||
locoInfoUtil = locoInfoUtil
|
||||
)
|
||||
|
||||
// 显示连接对话框
|
||||
if (showConnectionDialog) {
|
||||
ConnectionDialog(
|
||||
isScanning = isScanning,
|
||||
@@ -326,7 +362,6 @@ class MainActivity : ComponentActivity() {
|
||||
deviceStatus = "正在连接..."
|
||||
Log.d(TAG, "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) {
|
||||
@@ -397,30 +432,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
|
||||
private fun exportRecordsToCSV() {
|
||||
val records = trainRecordManager.getFilteredRecords()
|
||||
val file = trainRecordManager.exportToCsv(records)
|
||||
if (file != null) {
|
||||
try {
|
||||
|
||||
val uri = FileProvider.getUriForFile(
|
||||
this,
|
||||
"${applicationContext.packageName}.provider",
|
||||
file
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/csv"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
startActivity(Intent.createChooser(intent, "分享CSV文件"))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "CSV export failed: ${e.message}")
|
||||
Toast.makeText(this, "导出失败: ${e.message}", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "导出CSV文件失败", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun updateTemporaryStatusMessage(message: String) {
|
||||
@@ -464,7 +476,6 @@ class MainActivity : ComponentActivity() {
|
||||
stopScan()
|
||||
connectToDevice(device)
|
||||
} else {
|
||||
// 如果没有指定目标设备名称,或者找到的设备不是目标设备,显示连接对话框
|
||||
if (targetDeviceName == null) {
|
||||
showConnectionDialog = true
|
||||
}
|
||||
@@ -489,15 +500,69 @@ class MainActivity : ComponentActivity() {
|
||||
private fun loadSettings() {
|
||||
settingsDeviceName = settingsPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver"
|
||||
targetDeviceName = settingsDeviceName
|
||||
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName}")
|
||||
|
||||
|
||||
currentTab = settingsPrefs.getInt("current_tab", 0)
|
||||
historyEditMode = settingsPrefs.getBoolean("history_edit_mode", false)
|
||||
|
||||
val selectedRecordsStr = settingsPrefs.getString("history_selected_records", "")
|
||||
historySelectedRecords = if (selectedRecordsStr.isNullOrEmpty()) {
|
||||
emptySet()
|
||||
} else {
|
||||
selectedRecordsStr.split(",").toSet()
|
||||
}
|
||||
|
||||
val expandedStatesStr = settingsPrefs.getString("history_expanded_states", "")
|
||||
historyExpandedStates = if (expandedStatesStr.isNullOrEmpty()) {
|
||||
emptyMap()
|
||||
} else {
|
||||
expandedStatesStr.split(";").mapNotNull { pair ->
|
||||
val parts = pair.split(":")
|
||||
if (parts.size == 2) parts[0] to (parts[1] == "true") else null
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
historyScrollPosition = settingsPrefs.getInt("history_scroll_position", 0)
|
||||
historyScrollOffset = settingsPrefs.getInt("history_scroll_offset", 0)
|
||||
|
||||
val centerLat = settingsPrefs.getFloat("map_center_lat", Float.NaN)
|
||||
val centerLon = settingsPrefs.getFloat("map_center_lon", Float.NaN)
|
||||
mapCenterPosition = if (!centerLat.isNaN() && !centerLon.isNaN()) {
|
||||
centerLat.toDouble() to centerLon.toDouble()
|
||||
} else null
|
||||
|
||||
mapZoomLevel = settingsPrefs.getFloat("map_zoom_level", 10.0f).toDouble()
|
||||
mapRailwayLayerVisible = settingsPrefs.getBoolean("map_railway_visible", true)
|
||||
|
||||
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab}")
|
||||
}
|
||||
|
||||
|
||||
private fun saveSettings() {
|
||||
settingsPrefs.edit()
|
||||
val editor = settingsPrefs.edit()
|
||||
.putString("device_name", settingsDeviceName)
|
||||
.apply()
|
||||
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName}")
|
||||
.putInt("current_tab", currentTab)
|
||||
.putBoolean("history_edit_mode", historyEditMode)
|
||||
.putString("history_selected_records", historySelectedRecords.joinToString(","))
|
||||
.putString("history_expanded_states", historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"))
|
||||
.putInt("history_scroll_position", historyScrollPosition)
|
||||
.putInt("history_scroll_offset", historyScrollOffset)
|
||||
.putFloat("map_zoom_level", mapZoomLevel.toFloat())
|
||||
.putBoolean("map_railway_visible", mapRailwayLayerVisible)
|
||||
|
||||
mapCenterPosition?.let { (lat, lon) ->
|
||||
editor.putFloat("map_center_lat", lat.toFloat())
|
||||
editor.putFloat("map_center_lon", lon.toFloat())
|
||||
}
|
||||
|
||||
editor.apply()
|
||||
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
saveSettings()
|
||||
Log.d(TAG, "App paused, settings saved")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,7 +593,7 @@ fun MainContent(
|
||||
onFilterChange: (String, String, String) -> Unit,
|
||||
onClearFilter: () -> Unit,
|
||||
onClearRecords: () -> Unit,
|
||||
onExportRecords: () -> Unit,
|
||||
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit,
|
||||
|
||||
|
||||
@@ -538,7 +603,21 @@ fun MainContent(
|
||||
appVersion: String,
|
||||
|
||||
|
||||
locoInfoUtil: LocoInfoUtil
|
||||
locoInfoUtil: LocoInfoUtil,
|
||||
|
||||
|
||||
historyEditMode: Boolean,
|
||||
historySelectedRecords: Set<String>,
|
||||
historyExpandedStates: Map<String, Boolean>,
|
||||
historyScrollPosition: Int,
|
||||
historyScrollOffset: Int,
|
||||
onHistoryStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit,
|
||||
|
||||
|
||||
mapCenterPosition: Pair<Double, Double>?,
|
||||
mapZoomLevel: Double,
|
||||
mapRailwayLayerVisible: Boolean,
|
||||
onMapStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit
|
||||
) {
|
||||
val statusColor = if (isConnected) Color(0xFF4CAF50) else Color(0xFFFF5722)
|
||||
|
||||
@@ -633,10 +712,16 @@ fun MainContent(
|
||||
temporaryStatusMessage = temporaryStatusMessage,
|
||||
locoInfoUtil = locoInfoUtil,
|
||||
onClearRecords = onClearRecords,
|
||||
onExportRecords = onExportRecords,
|
||||
|
||||
onRecordClick = onRecordClick,
|
||||
onClearLog = onClearMonitorLog,
|
||||
onDeleteRecords = onDeleteRecords
|
||||
onDeleteRecords = onDeleteRecords,
|
||||
editMode = historyEditMode,
|
||||
selectedRecords = historySelectedRecords,
|
||||
expandedStates = historyExpandedStates,
|
||||
scrollPosition = historyScrollPosition,
|
||||
scrollOffset = historyScrollOffset,
|
||||
onStateChange = onHistoryStateChange
|
||||
)
|
||||
2 -> SettingsScreen(
|
||||
deviceName = deviceName,
|
||||
@@ -646,7 +731,10 @@ fun MainContent(
|
||||
)
|
||||
3 -> MapScreen(
|
||||
records = if (allRecords.isNotEmpty()) allRecords else recentRecords,
|
||||
onCenterMap = {}
|
||||
centerPosition = mapCenterPosition,
|
||||
zoomLevel = mapZoomLevel,
|
||||
railwayLayerVisible = mapRailwayLayerVisible,
|
||||
onStateChange = onMapStateChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
}
|
||||
|
||||
var timestamp: Date = Date()
|
||||
var receivedTimestamp: Date = Date()
|
||||
var train: String = ""
|
||||
var direction: Int = 0
|
||||
var speed: String = ""
|
||||
@@ -34,6 +35,17 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
|
||||
timestamp = Date(jsonData.getLong("timestamp"))
|
||||
}
|
||||
|
||||
if (jsonData.has("receivedTimestamp")) {
|
||||
receivedTimestamp = Date(jsonData.getLong("receivedTimestamp"))
|
||||
} else {
|
||||
receivedTimestamp = if (jsonData.has("timestamp")) {
|
||||
Date(jsonData.getLong("timestamp"))
|
||||
} else {
|
||||
Date()
|
||||
}
|
||||
}
|
||||
|
||||
updateFromJson(it)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to initialize TrainRecord from JSON: ${e.message}")
|
||||
@@ -96,7 +108,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
!trimmed.all { it == '*' }
|
||||
}
|
||||
|
||||
fun toMap(): Map<String, String> {
|
||||
fun toMap(showDetailedTime: Boolean = false): Map<String, String> {
|
||||
val directionText = when (direction) {
|
||||
1 -> "下行"
|
||||
3 -> "上行"
|
||||
@@ -114,12 +126,32 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
|
||||
val map = mutableMapOf<String, String>()
|
||||
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
map["timestamp"] = dateFormat.format(timestamp)
|
||||
map["receivedTimestamp"] = dateFormat.format(receivedTimestamp)
|
||||
|
||||
|
||||
if (trainDisplay.isNotEmpty()) map["train"] = trainDisplay
|
||||
if (directionText != "未知") map["direction"] = directionText
|
||||
if (isValidValue(speed)) map["speed"] = "速度: ${speed.trim()} km/h"
|
||||
if (isValidValue(position)) map["position"] = "位置: ${position.trim()} km"
|
||||
if (isValidValue(time)) map["time"] = "列车时间: ${time.trim()}"
|
||||
val timeToDisplay = if (showDetailedTime) {
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss", java.util.Locale.getDefault())
|
||||
if (isValidValue(time)) {
|
||||
"列车时间: $time\n接收时间: ${dateFormat.format(receivedTimestamp)}"
|
||||
} else {
|
||||
dateFormat.format(receivedTimestamp)
|
||||
}
|
||||
} else {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val diffInSec = (currentTime - receivedTimestamp.time) / 1000
|
||||
when {
|
||||
diffInSec < 60 -> "${diffInSec}秒前"
|
||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
||||
else -> "${diffInSec / 3600}小时前"
|
||||
}
|
||||
}
|
||||
map["time"] = timeToDisplay
|
||||
if (isValidValue(loco)) map["loco"] = "机车号: ${loco.trim()}"
|
||||
if (isValidValue(locoType)) map["loco_type"] = "型号: ${locoType.trim()}"
|
||||
if (isValidValue(route)) map["route"] = "线路: ${route.trim()}"
|
||||
@@ -135,6 +167,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
||||
fun toJSON(): JSONObject {
|
||||
val json = JSONObject()
|
||||
json.put("timestamp", timestamp.time)
|
||||
json.put("receivedTimestamp", receivedTimestamp.time)
|
||||
json.put("train", train)
|
||||
json.put("dir", direction)
|
||||
json.put("speed", speed)
|
||||
|
||||
@@ -38,6 +38,7 @@ class TrainRecordManager(private val context: Context) {
|
||||
|
||||
fun addRecord(jsonData: JSONObject): TrainRecord {
|
||||
val record = TrainRecord(jsonData)
|
||||
record.receivedTimestamp = Date()
|
||||
trainRecords.add(0, record)
|
||||
|
||||
|
||||
@@ -170,44 +171,7 @@ class TrainRecordManager(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun exportToCsv(records: List<TrainRecord>): File? {
|
||||
try {
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val fileName = "train_records_$timeStamp.csv"
|
||||
|
||||
|
||||
val downloadsDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||
val file = File(downloadsDir, fileName)
|
||||
|
||||
FileWriter(file).use { writer ->
|
||||
|
||||
writer.append("时间戳,列车号,列车类型,方向,速度,位置,时间,机车号,机车类型,路线,位置信息,信号强度\n")
|
||||
|
||||
|
||||
for (record in records) {
|
||||
val map = record.toMap()
|
||||
writer.append(map["timestamp"]).append(",")
|
||||
writer.append(map["train"]).append(",")
|
||||
writer.append(map["lbj_class"]).append(",")
|
||||
writer.append(map["direction"]).append(",")
|
||||
writer.append(map["speed"]?.replace(" km/h", "") ?: "").append(",")
|
||||
writer.append(map["position"]?.replace(" km", "") ?: "").append(",")
|
||||
writer.append(map["time"]).append(",")
|
||||
writer.append(map["loco"]).append(",")
|
||||
writer.append(map["loco_type"]).append(",")
|
||||
writer.append(map["route"]).append(",")
|
||||
writer.append(map["position_info"]).append(",")
|
||||
writer.append(map["rssi"]?.replace(" dBm", "") ?: "").append("\n")
|
||||
}
|
||||
}
|
||||
|
||||
return file
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error exporting to CSV: ${e.message}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun getRecordCount(): Int {
|
||||
|
||||
@@ -68,7 +68,19 @@ fun TrainInfoCard(
|
||||
}
|
||||
|
||||
Text(
|
||||
text = recordMap["timestamp"]?.toString()?.split(" ")?.getOrNull(1) ?: "",
|
||||
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
|
||||
)
|
||||
@@ -138,4 +150,4 @@ private fun CompactInfoItem(
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +140,6 @@ fun TrainRecordsListWithToolbar(
|
||||
records: List<TrainRecord>,
|
||||
onRecordClick: (TrainRecord) -> Unit,
|
||||
onFilterClick: () -> Unit,
|
||||
onExportClick: () -> Unit,
|
||||
onClearClick: () -> Unit,
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
@@ -198,12 +197,6 @@ fun TrainRecordsListWithToolbar(
|
||||
contentDescription = "筛选"
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onExportClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "导出"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ 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.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -44,18 +46,33 @@ fun HistoryScreen(
|
||||
temporaryStatusMessage: String? = null,
|
||||
locoInfoUtil: LocoInfoUtil? = null,
|
||||
onClearRecords: () -> Unit = {},
|
||||
onExportRecords: () -> Unit = {},
|
||||
onRecordClick: (TrainRecord) -> Unit = {},
|
||||
onClearLog: () -> Unit = {},
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit = {}
|
||||
onDeleteRecords: (List<TrainRecord>) -> 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 = { _, _, _, _, _ -> }
|
||||
) {
|
||||
|
||||
val refreshKey = latestRecord?.timestamp?.time ?: 0
|
||||
|
||||
var isInEditMode by remember { mutableStateOf(false) }
|
||||
val selectedRecords = remember { mutableStateListOf<TrainRecord>() }
|
||||
|
||||
val expandedStates = remember { mutableStateMapOf<String, Boolean>() }
|
||||
var isInEditMode by remember(editMode) { mutableStateOf(editMode) }
|
||||
val selectedRecordsList = remember(selectedRecords) {
|
||||
mutableStateListOf<TrainRecord>().apply {
|
||||
addAll(records.filter { selectedRecords.contains(it.timestamp.time.toString()) })
|
||||
}
|
||||
}
|
||||
val expandedStatesMap = remember(expandedStates) {
|
||||
mutableStateMapOf<String, Boolean>().apply { putAll(expandedStates) }
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = scrollPosition,
|
||||
initialFirstVisibleItemScrollOffset = scrollOffset
|
||||
)
|
||||
|
||||
|
||||
val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
|
||||
@@ -79,11 +96,32 @@ fun HistoryScreen(
|
||||
|
||||
fun exitEditMode() {
|
||||
isInEditMode = false
|
||||
selectedRecords.clear()
|
||||
selectedRecordsList.clear()
|
||||
onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedRecords.size) {
|
||||
if (selectedRecords.isEmpty() && isInEditMode) {
|
||||
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
LaunchedEffect(expandedStatesMap.toMap()) {
|
||||
if (!isInEditMode) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||
if (!isInEditMode) {
|
||||
delay(300)
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(selectedRecordsList.size) {
|
||||
if (selectedRecordsList.isEmpty() && isInEditMode) {
|
||||
exitEditMode()
|
||||
}
|
||||
}
|
||||
@@ -126,11 +164,12 @@ fun HistoryScreen(
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(filteredRecords) { record ->
|
||||
val isSelected = selectedRecords.contains(record)
|
||||
val isSelected = selectedRecordsList.contains(record)
|
||||
val cardColor = when {
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||
else -> MaterialTheme.colorScheme.surface
|
||||
@@ -151,14 +190,14 @@ fun HistoryScreen(
|
||||
onClick = {
|
||||
if (isInEditMode) {
|
||||
if (isSelected) {
|
||||
selectedRecords.remove(record)
|
||||
selectedRecordsList.remove(record)
|
||||
} else {
|
||||
selectedRecords.add(record)
|
||||
selectedRecordsList.add(record)
|
||||
}
|
||||
} else {
|
||||
val id = record.timestamp.time.toString()
|
||||
expandedStates[id] =
|
||||
!(expandedStates[id] ?: false)
|
||||
expandedStatesMap[id] =
|
||||
!(expandedStatesMap[id] ?: false)
|
||||
if (record == latestRecord) {
|
||||
onRecordClick(record)
|
||||
}
|
||||
@@ -167,8 +206,8 @@ fun HistoryScreen(
|
||||
onLongClick = {
|
||||
if (!isInEditMode) {
|
||||
isInEditMode = true
|
||||
selectedRecords.clear()
|
||||
selectedRecords.add(record)
|
||||
selectedRecordsList.clear()
|
||||
selectedRecordsList.add(record)
|
||||
}
|
||||
},
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
@@ -180,7 +219,9 @@ fun HistoryScreen(
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
val recordMap = record.toMap()
|
||||
val recordId = record.timestamp.time.toString()
|
||||
val isExpanded = expandedStatesMap[recordId] == true
|
||||
val recordMap = record.toMap(showDetailedTime = isExpanded)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -266,7 +307,7 @@ fun HistoryScreen(
|
||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
||||
Text(
|
||||
text = timeLine,
|
||||
fontSize = 12.sp,
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
@@ -342,8 +383,7 @@ fun HistoryScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val recordId = record.timestamp.time.toString()
|
||||
if (expandedStates[recordId] == true) {
|
||||
if (isExpanded) {
|
||||
val coordinates = remember { record.getCoordinates() }
|
||||
|
||||
if (coordinates != null) {
|
||||
@@ -527,15 +567,15 @@ fun HistoryScreen(
|
||||
)
|
||||
}
|
||||
Text(
|
||||
"已选择 ${selectedRecords.size} 条记录",
|
||||
"已选择 ${selectedRecordsList.size} 条记录",
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (selectedRecords.isNotEmpty()) {
|
||||
onDeleteRecords(selectedRecords.toList())
|
||||
if (selectedRecordsList.isNotEmpty()) {
|
||||
onDeleteRecords(selectedRecordsList.toList())
|
||||
exitEditMode()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,9 @@ import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.*
|
||||
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
|
||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
|
||||
import org.osmdroid.events.MapListener
|
||||
import org.osmdroid.events.ScrollEvent
|
||||
import org.osmdroid.events.ZoomEvent
|
||||
import org.noxylva.lbjconsole.model.TrainRecord
|
||||
import java.io.File
|
||||
|
||||
@@ -41,7 +44,11 @@ import java.io.File
|
||||
fun MapScreen(
|
||||
records: List<TrainRecord>,
|
||||
onCenterMap: () -> Unit = {},
|
||||
onLocationError: (String) -> Unit = {}
|
||||
onLocationError: (String) -> Unit = {},
|
||||
centerPosition: Pair<Double, Double>? = null,
|
||||
zoomLevel: Double = 10.0,
|
||||
railwayLayerVisible: Boolean = true,
|
||||
onStateChange: (Pair<Double, Double>?, Double, Boolean) -> Unit = { _, _, _ -> }
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
@@ -90,7 +97,7 @@ fun MapScreen(
|
||||
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
|
||||
var dialogPosition by remember { mutableStateOf<GeoPoint?>(null) }
|
||||
|
||||
var railwayLayerVisible by remember { mutableStateOf(true) }
|
||||
var railwayLayerVisibleState by remember(railwayLayerVisible) { mutableStateOf(railwayLayerVisible) }
|
||||
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
@@ -277,14 +284,21 @@ fun MapScreen(
|
||||
}
|
||||
|
||||
|
||||
if (validRecords.isNotEmpty()) {
|
||||
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||
controller.setCenter(lastPoint)
|
||||
controller.setZoom(12.0)
|
||||
centerPosition?.let { (lat, lon) ->
|
||||
controller.setCenter(GeoPoint(lat, lon))
|
||||
controller.setZoom(zoomLevel)
|
||||
isMapInitialized = true
|
||||
Log.d("MapScreen", "Map initialized with saved state: lat=$lat, lon=$lon, zoom=$zoomLevel")
|
||||
} ?: run {
|
||||
if (validRecords.isNotEmpty()) {
|
||||
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||
controller.setCenter(lastPoint)
|
||||
controller.setZoom(12.0)
|
||||
}
|
||||
} else {
|
||||
controller.setCenter(defaultPosition)
|
||||
controller.setZoom(10.0)
|
||||
}
|
||||
} else {
|
||||
controller.setCenter(defaultPosition)
|
||||
controller.setZoom(10.0)
|
||||
}
|
||||
|
||||
|
||||
@@ -304,30 +318,30 @@ fun MapScreen(
|
||||
myLocation?.let { location ->
|
||||
currentLocation = GeoPoint(location.latitude, location.longitude)
|
||||
|
||||
if (!isMapInitialized) {
|
||||
controller.setCenter(location)
|
||||
controller.setZoom(15.0)
|
||||
isMapInitialized = true
|
||||
Log.d("MapScreen", "Map initialized with GPS position: $location")
|
||||
}
|
||||
if (!isMapInitialized && centerPosition == null) {
|
||||
controller.setCenter(location)
|
||||
controller.setZoom(15.0)
|
||||
isMapInitialized = true
|
||||
Log.d("MapScreen", "Map initialized with GPS position: $location")
|
||||
}
|
||||
} ?: run {
|
||||
if (!isMapInitialized) {
|
||||
if (validRecords.isNotEmpty()) {
|
||||
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||
controller.setCenter(lastPoint)
|
||||
controller.setZoom(12.0)
|
||||
isMapInitialized = true
|
||||
Log.d("MapScreen", "Map initialized with last record position: $lastPoint")
|
||||
}
|
||||
} else {
|
||||
controller.setCenter(defaultPosition)
|
||||
isMapInitialized = true
|
||||
}
|
||||
}
|
||||
if (!isMapInitialized && centerPosition == null) {
|
||||
if (validRecords.isNotEmpty()) {
|
||||
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||
controller.setCenter(lastPoint)
|
||||
controller.setZoom(12.0)
|
||||
isMapInitialized = true
|
||||
Log.d("MapScreen", "Map initialized with last record position: $lastPoint")
|
||||
}
|
||||
} else {
|
||||
controller.setCenter(defaultPosition)
|
||||
isMapInitialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
if (!isMapInitialized) {
|
||||
if (!isMapInitialized && centerPosition == null) {
|
||||
if (validRecords.isNotEmpty()) {
|
||||
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
|
||||
controller.setCenter(lastPoint)
|
||||
@@ -357,6 +371,31 @@ fun MapScreen(
|
||||
setAlignBottom(true)
|
||||
setLineWidth(2.0f)
|
||||
}.also { overlays.add(it) }
|
||||
|
||||
|
||||
addMapListener(object : MapListener {
|
||||
override fun onScroll(event: ScrollEvent?): Boolean {
|
||||
val center = mapCenter
|
||||
val zoom = zoomLevelDouble
|
||||
onStateChange(
|
||||
center.latitude to center.longitude,
|
||||
zoom,
|
||||
railwayLayerVisibleState
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onZoom(event: ZoomEvent?): Boolean {
|
||||
val center = mapCenter
|
||||
val zoom = zoomLevelDouble
|
||||
onStateChange(
|
||||
center.latitude to center.longitude,
|
||||
zoom,
|
||||
railwayLayerVisibleState
|
||||
)
|
||||
return true
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
onLocationError("Map component initialization failed: ${e.localizedMessage}")
|
||||
@@ -381,7 +420,7 @@ fun MapScreen(
|
||||
|
||||
coroutineScope.launch {
|
||||
updateMarkers()
|
||||
updateRailwayLayerVisibility(railwayLayerVisible)
|
||||
updateRailwayLayerVisibility(railwayLayerVisibleState)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -430,15 +469,26 @@ fun MapScreen(
|
||||
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
railwayLayerVisible = !railwayLayerVisible
|
||||
updateRailwayLayerVisibility(railwayLayerVisible)
|
||||
railwayLayerVisibleState = !railwayLayerVisibleState
|
||||
updateRailwayLayerVisibility(railwayLayerVisibleState)
|
||||
|
||||
|
||||
mapViewRef.value?.let { mapView ->
|
||||
val center = mapView.mapCenter
|
||||
val zoom = mapView.zoomLevelDouble
|
||||
onStateChange(
|
||||
center.latitude to center.longitude,
|
||||
zoom,
|
||||
railwayLayerVisibleState
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier.size(40.dp),
|
||||
containerColor = if (railwayLayerVisible)
|
||||
containerColor = if (railwayLayerVisibleState)
|
||||
MaterialTheme.colorScheme.primary.copy(alpha = 0.9f)
|
||||
else
|
||||
MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.9f),
|
||||
contentColor = if (railwayLayerVisible)
|
||||
contentColor = if (railwayLayerVisibleState)
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
else
|
||||
MaterialTheme.colorScheme.onPrimaryContainer
|
||||
|
||||
Reference in New Issue
Block a user