fix: optimize the logic for saving scroll position

This commit is contained in:
Nedifinita
2025-08-01 17:35:34 +08:00
parent be8dc6bc72
commit 39bb8cb440
6 changed files with 141 additions and 54 deletions

View File

@@ -12,8 +12,8 @@ android {
applicationId = "org.noxylva.lbjconsole" applicationId = "org.noxylva.lbjconsole"
minSdk = 29 minSdk = 29
targetSdk = 35 targetSdk = 35
versionCode = 7 versionCode = 8
versionName = "0.0.7" versionName = "0.0.8"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@@ -52,6 +52,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import org.json.JSONObject import org.json.JSONObject
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.noxylva.lbjconsole.model.TrainRecord import org.noxylva.lbjconsole.model.TrainRecord
@@ -418,9 +419,11 @@ class MainActivity : ComponentActivity() {
deviceName = settingsDeviceName, deviceName = settingsDeviceName,
onDeviceNameChange = { newName -> settingsDeviceName = newName }, onDeviceNameChange = { newName -> settingsDeviceName = newName },
onApplySettings = { onApplySettings = {
saveSettings() if (targetDeviceName != settingsDeviceName) {
targetDeviceName = settingsDeviceName targetDeviceName = settingsDeviceName
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}") Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
saveSettings()
}
}, },
appVersion = getAppVersion(), appVersion = getAppVersion(),
locoInfoUtil = locoInfoUtil, locoInfoUtil = locoInfoUtil,
@@ -795,28 +798,30 @@ class MainActivity : ComponentActivity() {
private fun saveSettings() { private fun saveSettings() {
val editor = settingsPrefs.edit() lifecycleScope.launch(Dispatchers.IO) {
.putString("device_name", settingsDeviceName) val editor = settingsPrefs.edit()
.putInt("current_tab", currentTab) .putString("device_name", settingsDeviceName)
.putBoolean("history_edit_mode", historyEditMode) .putInt("current_tab", currentTab)
.putString("history_selected_records", historySelectedRecords.joinToString(",")) .putBoolean("history_edit_mode", historyEditMode)
.putString("history_expanded_states", historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";")) .putString("history_selected_records", historySelectedRecords.joinToString(","))
.putInt("history_scroll_position", historyScrollPosition) .putString("history_expanded_states", historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"))
.putInt("history_scroll_offset", historyScrollOffset) .putInt("history_scroll_position", historyScrollPosition)
.putInt("settings_scroll_position", settingsScrollPosition) .putInt("history_scroll_offset", historyScrollOffset)
.putFloat("map_zoom_level", mapZoomLevel.toFloat()) .putInt("settings_scroll_position", settingsScrollPosition)
.putBoolean("map_railway_visible", mapRailwayLayerVisible) .putFloat("map_zoom_level", mapZoomLevel.toFloat())
.putString("specified_device_address", specifiedDeviceAddress) .putBoolean("map_railway_visible", mapRailwayLayerVisible)
.putString("search_order_list", searchOrderList.joinToString(",")) .putString("specified_device_address", specifiedDeviceAddress)
.putBoolean("auto_connect_enabled", autoConnectEnabled) .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.apply()
editor.putFloat("map_center_lat", lat.toFloat()) Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
editor.putFloat("map_center_lon", lon.toFloat())
} }
editor.apply()
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
} }
override fun onResume() { override fun onResume() {

View File

@@ -7,9 +7,10 @@ data class MergeSettings(
) )
enum class GroupBy(val displayName: String) { enum class GroupBy(val displayName: String) {
TRAIN_AND_LOCO("车次号+机车号"), TRAIN_ONLY("车次号"),
TRAIN_ONLY("仅车次"), LOCO_ONLY("机车"),
LOCO_ONLY("机车号") TRAIN_OR_LOCO("车次号或机车号"),
TRAIN_AND_LOCO("车次号与机车号")
} }
enum class TimeWindow(val displayName: String, val seconds: Long?) { 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? { fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? {
return when (groupBy) { return when (groupBy) {
GroupBy.TRAIN_AND_LOCO -> {
val train = record.train.trim()
val loco = record.loco.trim()
if (train.isNotEmpty() && train != "<NUL>" &&
loco.isNotEmpty() && loco != "<NUL>") {
"${train}_${loco}"
} else null
}
GroupBy.TRAIN_ONLY -> { GroupBy.TRAIN_ONLY -> {
val train = record.train.trim() val train = record.train.trim()
if (train.isNotEmpty() && train != "<NUL>") train else null if (train.isNotEmpty() && train != "<NUL>") train else null
@@ -39,5 +32,22 @@ fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? {
val loco = record.loco.trim() val loco = record.loco.trim()
if (loco.isNotEmpty() && loco != "<NUL>") loco else null if (loco.isNotEmpty() && loco != "<NUL>") loco else null
} }
GroupBy.TRAIN_OR_LOCO -> {
val train = record.train.trim()
val loco = record.loco.trim()
when {
train.isNotEmpty() && train != "<NUL>" -> train
loco.isNotEmpty() && loco != "<NUL>" -> loco
else -> null
}
}
GroupBy.TRAIN_AND_LOCO -> {
val train = record.train.trim()
val loco = record.loco.trim()
if (train.isNotEmpty() && train != "<NUL>" &&
loco.isNotEmpty() && loco != "<NUL>") {
"${train}_${loco}"
} else null
}
} }
} }

View File

@@ -234,26 +234,79 @@ class TrainRecordManager(private val context: Context) {
} }
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> { private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
val groupedRecords = mutableMapOf<String, MutableList<TrainRecord>>()
val currentTime = Date() 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<String, MutableList<TrainRecord>>()
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<TrainRecord>): List<MergedTrainRecord> {
val groups = mutableListOf<MutableList<TrainRecord>>()
records.forEach { record -> records.forEach { record ->
val groupKey = generateGroupKey(record, settings.groupBy) val train = record.train.trim()
if (groupKey != null) { val loco = record.loco.trim()
val withinTimeWindow = settings.timeWindow.seconds?.let { windowSeconds ->
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds if ((train.isEmpty() || train == "<NUL>") && (loco.isEmpty() || loco == "<NUL>")) {
} ?: true return@forEach
}
if (withinTimeWindow) {
groupedRecords.getOrPut(groupKey) { mutableListOf() }.add(record) var foundGroup: MutableList<TrainRecord>? = null
for (group in groups) {
val shouldMerge = group.any { existingRecord ->
val existingTrain = existingRecord.train.trim()
val existingLoco = existingRecord.loco.trim()
(train.isNotEmpty() && train != "<NUL>" && train == existingTrain) ||
(loco.isNotEmpty() && loco != "<NUL>" && 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) { if (groupRecords.size >= 2) {
val sortedRecords = groupRecords.sortedBy { it.timestamp } val sortedRecords = groupRecords.sortedBy { it.timestamp }
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!! val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
MergedTrainRecord( MergedTrainRecord(
groupKey = groupKey, groupKey = groupKey,
records = sortedRecords, records = sortedRecords,

View File

@@ -1,5 +1,6 @@
package org.noxylva.lbjconsole.ui.screens package org.noxylva.lbjconsole.ui.screens
import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -862,6 +863,7 @@ fun HistoryScreen(
) { ) {
val refreshKey = latestRecord?.timestamp?.time ?: 0 val refreshKey = latestRecord?.timestamp?.time ?: 0
var wasAtTopBeforeUpdate by remember { mutableStateOf(false) }
var isInEditMode by remember(editMode) { mutableStateOf(editMode) } var isInEditMode by remember(editMode) { mutableStateOf(editMode) }
val selectedRecordsList = remember(selectedRecords) { val selectedRecordsList = remember(selectedRecords) {
@@ -941,6 +943,22 @@ fun HistoryScreen(
onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) 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()) { Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {

View File

@@ -15,6 +15,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import org.noxylva.lbjconsole.model.MergeSettings import org.noxylva.lbjconsole.model.MergeSettings
import org.noxylva.lbjconsole.model.GroupBy import org.noxylva.lbjconsole.model.GroupBy
import org.noxylva.lbjconsole.model.TimeWindow import org.noxylva.lbjconsole.model.TimeWindow
@@ -46,17 +47,16 @@ fun SettingsScreen(
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
LaunchedEffect(scrollPosition) { LaunchedEffect(scrollPosition) {
scrollState.scrollTo(scrollPosition) if (scrollState.value != scrollPosition) {
scrollState.scrollTo(scrollPosition)
}
} }
LaunchedEffect(scrollState.value) { LaunchedEffect(scrollState.value) {
delay(50)
onScrollPositionChange(scrollState.value) onScrollPositionChange(scrollState.value)
} }
LaunchedEffect(deviceName) {
onApplySettings()
}
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
@@ -198,12 +198,13 @@ fun SettingsScreen(
} }
val context = LocalContext.current val context = LocalContext.current
var backgroundServiceEnabled by remember { val notificationService = remember(context) { NotificationService(context) }
var backgroundServiceEnabled by remember(context) {
mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context)) mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context))
} }
val notificationService = remember { NotificationService(context) } var notificationEnabled by remember(context, notificationService) {
var notificationEnabled by remember {
mutableStateOf(notificationService.isNotificationEnabled()) mutableStateOf(notificationService.isNotificationEnabled())
} }