diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5641752..1b4852e 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 = 7 - versionName = "0.0.7" + versionCode = 8 + versionName = "0.0.8" 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 7cd7228..8527e73 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/MainActivity.kt @@ -52,6 +52,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers import org.json.JSONObject import org.osmdroid.config.Configuration import org.noxylva.lbjconsole.model.TrainRecord @@ -418,9 +419,11 @@ class MainActivity : ComponentActivity() { deviceName = settingsDeviceName, onDeviceNameChange = { newName -> settingsDeviceName = newName }, onApplySettings = { - saveSettings() - targetDeviceName = settingsDeviceName - Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}") + if (targetDeviceName != settingsDeviceName) { + targetDeviceName = settingsDeviceName + Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}") + saveSettings() + } }, appVersion = getAppVersion(), locoInfoUtil = locoInfoUtil, @@ -795,28 +798,30 @@ class MainActivity : ComponentActivity() { private fun saveSettings() { - val editor = settingsPrefs.edit() - .putString("device_name", 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) - .putInt("settings_scroll_position", settingsScrollPosition) - .putFloat("map_zoom_level", mapZoomLevel.toFloat()) - .putBoolean("map_railway_visible", mapRailwayLayerVisible) - .putString("specified_device_address", specifiedDeviceAddress) - .putString("search_order_list", searchOrderList.joinToString(",")) - .putBoolean("auto_connect_enabled", autoConnectEnabled) + lifecycleScope.launch(Dispatchers.IO) { + val editor = settingsPrefs.edit() + .putString("device_name", 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) + .putInt("settings_scroll_position", settingsScrollPosition) + .putFloat("map_zoom_level", mapZoomLevel.toFloat()) + .putBoolean("map_railway_visible", mapRailwayLayerVisible) + .putString("specified_device_address", specifiedDeviceAddress) + .putString("search_order_list", searchOrderList.joinToString(",")) + .putBoolean("auto_connect_enabled", autoConnectEnabled) + + mapCenterPosition?.let { (lat, lon) -> + editor.putFloat("map_center_lat", lat.toFloat()) + editor.putFloat("map_center_lon", lon.toFloat()) + } - 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}") } - - editor.apply() - Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}") } override fun onResume() { diff --git a/app/src/main/java/org/noxylva/lbjconsole/model/MergeSettings.kt b/app/src/main/java/org/noxylva/lbjconsole/model/MergeSettings.kt index 65d9d44..ea50b17 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/model/MergeSettings.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/model/MergeSettings.kt @@ -7,9 +7,10 @@ data class MergeSettings( ) enum class GroupBy(val displayName: String) { - TRAIN_AND_LOCO("车次号+机车号"), - TRAIN_ONLY("仅车次号"), - LOCO_ONLY("仅机车号") + TRAIN_ONLY("车次号"), + LOCO_ONLY("机车号"), + TRAIN_OR_LOCO("车次号或机车号"), + TRAIN_AND_LOCO("车次号与机车号") } enum class TimeWindow(val displayName: String, val seconds: Long?) { @@ -23,14 +24,6 @@ enum class TimeWindow(val displayName: String, val seconds: Long?) { fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? { return when (groupBy) { - GroupBy.TRAIN_AND_LOCO -> { - val train = record.train.trim() - val loco = record.loco.trim() - if (train.isNotEmpty() && train != "" && - loco.isNotEmpty() && loco != "") { - "${train}_${loco}" - } else null - } GroupBy.TRAIN_ONLY -> { val train = record.train.trim() if (train.isNotEmpty() && train != "") train else null @@ -39,5 +32,22 @@ fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? { val loco = record.loco.trim() if (loco.isNotEmpty() && loco != "") loco else null } + GroupBy.TRAIN_OR_LOCO -> { + val train = record.train.trim() + val loco = record.loco.trim() + when { + train.isNotEmpty() && train != "" -> train + loco.isNotEmpty() && loco != "" -> loco + else -> null + } + } + GroupBy.TRAIN_AND_LOCO -> { + val train = record.train.trim() + val loco = record.loco.trim() + if (train.isNotEmpty() && train != "" && + loco.isNotEmpty() && loco != "") { + "${train}_${loco}" + } else null + } } } \ No newline at end of file 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 5eb65ed..89dca85 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/model/TrainRecordManager.kt @@ -234,26 +234,79 @@ class TrainRecordManager(private val context: Context) { } private fun processRecordsForMerging(records: List, settings: MergeSettings): List { - val groupedRecords = mutableMapOf>() val currentTime = Date() + val validRecords = records.filter { record -> + settings.timeWindow.seconds?.let { windowSeconds -> + (currentTime.time - record.timestamp.time) / 1000 <= windowSeconds + } ?: true + } + + return when (settings.groupBy) { + GroupBy.TRAIN_OR_LOCO -> processTrainOrLocoMerging(validRecords) + else -> { + val groupedRecords = mutableMapOf>() + validRecords.forEach { record -> + val groupKey = generateGroupKey(record, settings.groupBy) + if (groupKey != null) { + groupedRecords.getOrPut(groupKey) { mutableListOf() }.add(record) + } + } + + groupedRecords.mapNotNull { (groupKey, groupRecords) -> + if (groupRecords.size >= 2) { + val sortedRecords = groupRecords.sortedBy { it.timestamp } + val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!! + MergedTrainRecord( + groupKey = groupKey, + records = sortedRecords, + latestRecord = latestRecord + ) + } else null + }.sortedByDescending { it.latestRecord.timestamp } + } + } + } + + private fun processTrainOrLocoMerging(records: List): List { + val groups = mutableListOf>() records.forEach { record -> - val groupKey = generateGroupKey(record, settings.groupBy) - if (groupKey != null) { - val withinTimeWindow = settings.timeWindow.seconds?.let { windowSeconds -> - (currentTime.time - record.timestamp.time) / 1000 <= windowSeconds - } ?: true - - if (withinTimeWindow) { - groupedRecords.getOrPut(groupKey) { mutableListOf() }.add(record) + val train = record.train.trim() + val loco = record.loco.trim() + + if ((train.isEmpty() || train == "") && (loco.isEmpty() || loco == "")) { + return@forEach + } + + var foundGroup: MutableList? = null + + for (group in groups) { + val shouldMerge = group.any { existingRecord -> + val existingTrain = existingRecord.train.trim() + val existingLoco = existingRecord.loco.trim() + + (train.isNotEmpty() && train != "" && train == existingTrain) || + (loco.isNotEmpty() && loco != "" && loco == existingLoco) } + + if (shouldMerge) { + foundGroup = group + break + } + } + + if (foundGroup != null) { + foundGroup.add(record) + } else { + groups.add(mutableListOf(record)) } } - return groupedRecords.mapNotNull { (groupKey, groupRecords) -> + return groups.mapNotNull { groupRecords -> if (groupRecords.size >= 2) { val sortedRecords = groupRecords.sortedBy { it.timestamp } val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!! + val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}" MergedTrainRecord( groupKey = groupKey, records = sortedRecords, 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 fe712a1..eeb60bc 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 @@ -1,5 +1,6 @@ package org.noxylva.lbjconsole.ui.screens +import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.background @@ -862,6 +863,7 @@ fun HistoryScreen( ) { val refreshKey = latestRecord?.timestamp?.time ?: 0 + var wasAtTopBeforeUpdate by remember { mutableStateOf(false) } var isInEditMode by remember(editMode) { mutableStateOf(editMode) } val selectedRecordsList = remember(selectedRecords) { @@ -941,6 +943,22 @@ fun HistoryScreen( onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) } } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + if (!isInEditMode && filteredRecords.isNotEmpty()) { + wasAtTopBeforeUpdate = listState.firstVisibleItemIndex == 0 && listState.firstVisibleItemScrollOffset <= 100 + } + } + + LaunchedEffect(refreshKey) { + if (refreshKey > 0 && !isInEditMode && filteredRecords.isNotEmpty() && wasAtTopBeforeUpdate) { + try { + listState.animateScrollToItem(0, 0) + } catch (e: Exception) { + listState.scrollToItem(0, 0) + } + } + } Box(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) { diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt index 506cedc..8cb370a 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay import org.noxylva.lbjconsole.model.MergeSettings import org.noxylva.lbjconsole.model.GroupBy import org.noxylva.lbjconsole.model.TimeWindow @@ -46,17 +47,16 @@ fun SettingsScreen( val scrollState = rememberScrollState() LaunchedEffect(scrollPosition) { - scrollState.scrollTo(scrollPosition) + if (scrollState.value != scrollPosition) { + scrollState.scrollTo(scrollPosition) + } } LaunchedEffect(scrollState.value) { + delay(50) onScrollPositionChange(scrollState.value) } - LaunchedEffect(deviceName) { - onApplySettings() - } - Column( modifier = Modifier .fillMaxSize() @@ -198,12 +198,13 @@ fun SettingsScreen( } val context = LocalContext.current - var backgroundServiceEnabled by remember { + val notificationService = remember(context) { NotificationService(context) } + + var backgroundServiceEnabled by remember(context) { mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context)) } - val notificationService = remember { NotificationService(context) } - var notificationEnabled by remember { + var notificationEnabled by remember(context, notificationService) { mutableStateOf(notificationService.isNotificationEnabled()) }