From 4f2c1f0e2406524a875bfd6c3ac40565edc782c9 Mon Sep 17 00:00:00 2001 From: Nedifinita Date: Fri, 18 Jul 2025 23:53:55 +0800 Subject: [PATCH] feat: add timestamp logging for received messages, optimize page details --- README.md | 3 - app/build.gradle.kts | 4 +- .../org/noxylva/lbjconsole/MainActivity.kt | 174 +++++++++++++----- .../noxylva/lbjconsole/model/TrainRecord.kt | 37 +++- .../lbjconsole/model/TrainRecordManager.kt | 40 +--- .../lbjconsole/ui/components/TrainInfoCard.kt | 16 +- .../ui/components/TrainRecordsList.kt | 7 - .../lbjconsole/ui/screens/HistoryScreen.kt | 86 ++++++--- .../lbjconsole/ui/screens/MapScreen.kt | 118 ++++++++---- 9 files changed, 331 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 66f4eb3..edaf948 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6c454a9..cc7304b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" } diff --git a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt index 0809883..2080092 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt @@ -72,7 +72,17 @@ class MainActivity : ComponentActivity() { private var settingsDeviceName by mutableStateOf("LBJReceiver") - private var temporaryStatusMessage by mutableStateOf(null) + private var temporaryStatusMessage by mutableStateOf(null) + + + private var historyEditMode by mutableStateOf(false) + private var historySelectedRecords by mutableStateOf>(emptySet()) + private var historyExpandedStates by mutableStateOf>(emptyMap()) + private var historyScrollPosition by mutableStateOf(0) + private var historyScrollOffset by mutableStateOf(0) + private var mapCenterPosition by mutableStateOf?>(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) -> Unit, @@ -538,7 +603,21 @@ fun MainContent( appVersion: String, - locoInfoUtil: LocoInfoUtil + locoInfoUtil: LocoInfoUtil, + + + historyEditMode: Boolean, + historySelectedRecords: Set, + historyExpandedStates: Map, + historyScrollPosition: Int, + historyScrollOffset: Int, + onHistoryStateChange: (Boolean, Set, Map, Int, Int) -> Unit, + + + mapCenterPosition: Pair?, + mapZoomLevel: Double, + mapRailwayLayerVisible: Boolean, + onMapStateChange: (Pair?, 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 ) } } diff --git a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt index b7dae58..b0163d2 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecord.kt @@ -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 { + fun toMap(showDetailedTime: Boolean = false): Map { val directionText = when (direction) { 1 -> "下行" 3 -> "上行" @@ -114,12 +126,32 @@ class TrainRecord(jsonData: JSONObject? = null) { val map = mutableMapOf() + 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) diff --git a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt index 8884d1d..4c0882e 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt @@ -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): 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 { diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt index cf9b2f9..7f5fb06 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainInfoCard.kt @@ -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 != "" && trainTime != "NA" && trainTime != "") { + trainTime + } else { + val receivedTime = recordMap["receivedTimestamp"]?.toString() ?: "" + if (receivedTime.contains(" ")) { + receivedTime.split(" ")[1] + } else { + java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(trainRecord.receivedTimestamp) + } + } + }, fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -138,4 +150,4 @@ private fun CompactInfoItem( color = MaterialTheme.colorScheme.onSurface ) } -} \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt index 5bf7781..88488d5 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/components/TrainRecordsList.kt @@ -140,7 +140,6 @@ fun TrainRecordsListWithToolbar( records: List, onRecordClick: (TrainRecord) -> Unit, onFilterClick: () -> Unit, - onExportClick: () -> Unit, onClearClick: () -> Unit, onDeleteRecords: (List) -> Unit, modifier: Modifier = Modifier @@ -198,12 +197,6 @@ fun TrainRecordsListWithToolbar( contentDescription = "筛选" ) } - IconButton(onClick = onExportClick) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = "导出" - ) - } } } } diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt index a07f2c4..899776e 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/HistoryScreen.kt @@ -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) -> Unit = {} + onDeleteRecords: (List) -> Unit = {}, + editMode: Boolean = false, + selectedRecords: Set = emptySet(), + expandedStates: Map = emptyMap(), + scrollPosition: Int = 0, + scrollOffset: Int = 0, + onStateChange: (Boolean, Set, Map, Int, Int) -> Unit = { _, _, _, _, _ -> } ) { val refreshKey = latestRecord?.timestamp?.time ?: 0 - var isInEditMode by remember { mutableStateOf(false) } - val selectedRecords = remember { mutableStateListOf() } - - val expandedStates = remember { mutableStateMapOf() } + var isInEditMode by remember(editMode) { mutableStateOf(editMode) } + val selectedRecordsList = remember(selectedRecords) { + mutableStateListOf().apply { + addAll(records.filter { selectedRecords.contains(it.timestamp.time.toString()) }) + } + } + val expandedStatesMap = remember(expandedStates) { + mutableStateMapOf().apply { putAll(expandedStates) } + } + + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = scrollPosition, + initialFirstVisibleItemScrollOffset = scrollOffset + ) val timeSinceLastUpdate = remember { mutableStateOf(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() } } diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt index d9429fb..16c9856 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MapScreen.kt @@ -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, onCenterMap: () -> Unit = {}, - onLocationError: (String) -> Unit = {} + onLocationError: (String) -> Unit = {}, + centerPosition: Pair? = null, + zoomLevel: Double = 10.0, + railwayLayerVisible: Boolean = true, + onStateChange: (Pair?, Double, Boolean) -> Unit = { _, _, _ -> } ) { val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current @@ -90,7 +97,7 @@ fun MapScreen( var selectedRecord by remember { mutableStateOf(null) } var dialogPosition by remember { mutableStateOf(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