Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39bb8cb440 | ||
|
|
be8dc6bc72 | ||
|
|
cd3128c24b |
@@ -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"
|
||||
}
|
||||
|
||||
@@ -275,6 +275,33 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
||||
fun isConnected(): Boolean {
|
||||
return isConnected
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun checkActualConnectionState(): Boolean {
|
||||
bluetoothGatt?.let { gatt ->
|
||||
try {
|
||||
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
||||
val connectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)
|
||||
val isActuallyConnected = connectedDevices.any { it.address == deviceAddress }
|
||||
|
||||
if (isActuallyConnected && !isConnected) {
|
||||
Log.d(TAG, "Found existing GATT connection, updating internal state")
|
||||
isConnected = true
|
||||
return true
|
||||
} else if (!isActuallyConnected && isConnected) {
|
||||
Log.d(TAG, "GATT connection lost, updating internal state")
|
||||
isConnected = false
|
||||
return false
|
||||
}
|
||||
|
||||
return isActuallyConnected
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error checking actual connection state: ${e.message}")
|
||||
return isConnected
|
||||
}
|
||||
}
|
||||
return isConnected
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
|
||||
@@ -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
|
||||
@@ -117,6 +118,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var specifiedDeviceAddress by mutableStateOf<String?>(null)
|
||||
private var searchOrderList by mutableStateOf(listOf<String>())
|
||||
private var showDisconnectButton by mutableStateOf(false)
|
||||
private var autoConnectEnabled by mutableStateOf(true)
|
||||
|
||||
|
||||
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
|
||||
@@ -313,6 +315,12 @@ class MainActivity : ComponentActivity() {
|
||||
saveSettings()
|
||||
Log.d(TAG, "Set specified device address: $address")
|
||||
},
|
||||
autoConnectEnabled = autoConnectEnabled,
|
||||
onAutoConnectEnabledChange = { enabled ->
|
||||
autoConnectEnabled = enabled
|
||||
saveSettings()
|
||||
Log.d(TAG, "Auto connect enabled: $enabled")
|
||||
},
|
||||
|
||||
|
||||
latestRecord = latestRecord,
|
||||
@@ -411,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,
|
||||
@@ -615,6 +625,11 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
|
||||
private fun startAutoScanAndConnect() {
|
||||
if (!autoConnectEnabled) {
|
||||
Log.d(TAG, "Auto connect disabled, skipping auto scan")
|
||||
return
|
||||
}
|
||||
|
||||
Log.d(TAG, "Starting auto scan and connect")
|
||||
|
||||
if (!hasBluetoothPermissions()) {
|
||||
@@ -774,34 +789,39 @@ class MainActivity : ComponentActivity() {
|
||||
searchOrderStr.split(",").filter { it.isNotBlank() }
|
||||
}
|
||||
|
||||
autoConnectEnabled = settingsPrefs.getBoolean("auto_connect_enabled", true)
|
||||
|
||||
bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress)
|
||||
|
||||
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size}")
|
||||
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size} autoConnect=${autoConnectEnabled}")
|
||||
}
|
||||
|
||||
|
||||
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(","))
|
||||
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() {
|
||||
@@ -810,12 +830,20 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
bleClient.setHighFrequencyReconnect(true)
|
||||
|
||||
if (hasBluetoothPermissions() && !bleClient.isConnected()) {
|
||||
Log.d(TAG, "App resumed and not connected, starting auto scan")
|
||||
startAutoScanAndConnect()
|
||||
} else if (bleClient.isConnected()) {
|
||||
showDisconnectButton = true
|
||||
deviceStatus = "已连接"
|
||||
if (hasBluetoothPermissions()) {
|
||||
val actuallyConnected = bleClient.checkActualConnectionState()
|
||||
|
||||
if (actuallyConnected) {
|
||||
showDisconnectButton = true
|
||||
deviceStatus = "已连接"
|
||||
Log.d(TAG, "App resumed - connection verified")
|
||||
} else if (autoConnectEnabled) {
|
||||
Log.d(TAG, "App resumed and not connected, starting auto scan")
|
||||
startAutoScanAndConnect()
|
||||
} else {
|
||||
deviceStatus = "未连接"
|
||||
showDisconnectButton = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -847,6 +875,8 @@ fun MainContent(
|
||||
specifiedDeviceAddress: String?,
|
||||
searchOrderList: List<String>,
|
||||
onSpecifiedDeviceSelected: (String?) -> Unit,
|
||||
autoConnectEnabled: Boolean,
|
||||
onAutoConnectEnabledChange: (Boolean) -> Unit,
|
||||
|
||||
|
||||
latestRecord: TrainRecord?,
|
||||
@@ -1118,7 +1148,9 @@ fun MainContent(
|
||||
onScrollPositionChange = onSettingsScrollPositionChange,
|
||||
specifiedDeviceAddress = specifiedDeviceAddress,
|
||||
searchOrderList = searchOrderList,
|
||||
onSpecifiedDeviceSelected = onSpecifiedDeviceSelected
|
||||
onSpecifiedDeviceSelected = onSpecifiedDeviceSelected,
|
||||
autoConnectEnabled = autoConnectEnabled,
|
||||
onAutoConnectEnabledChange = onAutoConnectEnabledChange
|
||||
)
|
||||
3 -> MapScreen(
|
||||
records = if (allRecords.isNotEmpty()) {
|
||||
|
||||
@@ -8,11 +8,14 @@ import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.RemoteViews
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import org.json.JSONObject
|
||||
import org.noxylva.lbjconsole.model.TrainRecord
|
||||
|
||||
|
||||
class NotificationService(private val context: Context) {
|
||||
companion object {
|
||||
const val TAG = "NotificationService"
|
||||
@@ -86,12 +89,7 @@ class NotificationService(private val context: Context) {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val directionText = when (trainRecord.direction) {
|
||||
1 -> "下行"
|
||||
3 -> "上行"
|
||||
else -> "未知"
|
||||
}
|
||||
|
||||
val remoteViews = RemoteViews(context.packageName, R.layout.notification_train_record)
|
||||
val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) {
|
||||
"${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}"
|
||||
} else if (isValidValue(trainRecord.lbjClass)) {
|
||||
@@ -99,26 +97,83 @@ class NotificationService(private val context: Context) {
|
||||
} else if (isValidValue(trainRecord.train)) {
|
||||
trainRecord.train.trim()
|
||||
} else "列车"
|
||||
|
||||
val title = trainDisplay
|
||||
val content = buildString {
|
||||
append(directionText)
|
||||
if (isValidValue(trainRecord.route)) {
|
||||
append("\n线路: ${trainRecord.route.trim()}")
|
||||
}
|
||||
if (isValidValue(trainRecord.speed)) {
|
||||
append("\n速度: ${trainRecord.speed.trim()} km/h")
|
||||
}
|
||||
if (isValidValue(trainRecord.position)) {
|
||||
append("\n位置: ${trainRecord.position.trim()} km")
|
||||
}
|
||||
remoteViews.setTextViewText(R.id.notification_train_number, trainDisplay)
|
||||
|
||||
val directionText = when (trainRecord.direction) {
|
||||
1 -> "下"
|
||||
3 -> "上"
|
||||
else -> ""
|
||||
}
|
||||
if (directionText.isNotEmpty()) {
|
||||
remoteViews.setTextViewText(R.id.notification_direction, directionText)
|
||||
remoteViews.setViewVisibility(R.id.notification_direction, View.VISIBLE)
|
||||
} else {
|
||||
remoteViews.setViewVisibility(R.id.notification_direction, View.GONE)
|
||||
}
|
||||
|
||||
val locoInfo = when {
|
||||
isValidValue(trainRecord.locoType) && isValidValue(trainRecord.loco) -> {
|
||||
val shortLoco = if (trainRecord.loco.length > 5) {
|
||||
trainRecord.loco.takeLast(5)
|
||||
} else {
|
||||
trainRecord.loco
|
||||
}
|
||||
"${trainRecord.locoType}-${shortLoco}"
|
||||
}
|
||||
isValidValue(trainRecord.locoType) -> trainRecord.locoType
|
||||
isValidValue(trainRecord.loco) -> trainRecord.loco
|
||||
else -> ""
|
||||
}
|
||||
if (locoInfo.isNotEmpty()) {
|
||||
remoteViews.setTextViewText(R.id.notification_loco_info, locoInfo)
|
||||
remoteViews.setViewVisibility(R.id.notification_loco_info, View.VISIBLE)
|
||||
} else {
|
||||
remoteViews.setViewVisibility(R.id.notification_loco_info, View.GONE)
|
||||
}
|
||||
|
||||
if (isValidValue(trainRecord.route)) {
|
||||
remoteViews.setTextViewText(R.id.notification_route, trainRecord.route.trim())
|
||||
remoteViews.setViewVisibility(R.id.notification_route, View.VISIBLE)
|
||||
} else {
|
||||
remoteViews.setViewVisibility(R.id.notification_route, View.GONE)
|
||||
}
|
||||
|
||||
if (isValidValue(trainRecord.position)) {
|
||||
remoteViews.setTextViewText(R.id.notification_position, "${trainRecord.position.trim()}K")
|
||||
remoteViews.setViewVisibility(R.id.notification_position, View.VISIBLE)
|
||||
} else {
|
||||
remoteViews.setViewVisibility(R.id.notification_position, View.GONE)
|
||||
}
|
||||
|
||||
if (isValidValue(trainRecord.speed)) {
|
||||
remoteViews.setTextViewText(R.id.notification_speed, "${trainRecord.speed.trim()} km/h")
|
||||
remoteViews.setViewVisibility(R.id.notification_speed, View.VISIBLE)
|
||||
} else {
|
||||
remoteViews.setViewVisibility(R.id.notification_speed, View.GONE)
|
||||
}
|
||||
|
||||
remoteViews.setOnClickPendingIntent(R.id.notification_train_number, pendingIntent)
|
||||
|
||||
val summaryParts = mutableListOf<String>()
|
||||
|
||||
val routeAndDirection = when {
|
||||
isValidValue(trainRecord.route) && directionText.isNotEmpty() -> "${trainRecord.route.trim()}${directionText}行"
|
||||
isValidValue(trainRecord.route) -> trainRecord.route.trim()
|
||||
directionText.isNotEmpty() -> "${directionText}行"
|
||||
else -> null
|
||||
}
|
||||
|
||||
routeAndDirection?.let { summaryParts.add(it) }
|
||||
if (locoInfo.isNotEmpty()) summaryParts.add(locoInfo)
|
||||
|
||||
val summaryText = summaryParts.joinToString(" • ")
|
||||
|
||||
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(content))
|
||||
.setContentTitle(trainDisplay)
|
||||
.setContentText(summaryText)
|
||||
.setCustomContentView(remoteViews)
|
||||
.setCustomBigContentView(remoteViews)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true)
|
||||
@@ -131,7 +186,7 @@ class NotificationService(private val context: Context) {
|
||||
}
|
||||
|
||||
notificationManager.notify(notificationId, notification)
|
||||
Log.d(TAG, "Notification sent for train: ${trainRecord.train}")
|
||||
Log.d(TAG, "Custom notification sent for train: ${trainRecord.train}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to show notification: ${e.message}", e)
|
||||
|
||||
@@ -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 != "<NUL>" &&
|
||||
loco.isNotEmpty() && loco != "<NUL>") {
|
||||
"${train}_${loco}"
|
||||
} else null
|
||||
}
|
||||
GroupBy.TRAIN_ONLY -> {
|
||||
val train = record.train.trim()
|
||||
if (train.isNotEmpty() && train != "<NUL>") train else null
|
||||
@@ -39,5 +32,22 @@ fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? {
|
||||
val loco = record.loco.trim()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,26 +234,79 @@ class TrainRecordManager(private val context: Context) {
|
||||
}
|
||||
|
||||
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
|
||||
val groupedRecords = mutableMapOf<String, MutableList<TrainRecord>>()
|
||||
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 ->
|
||||
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 == "<NUL>") && (loco.isEmpty() || loco == "<NUL>")) {
|
||||
return@forEach
|
||||
}
|
||||
|
||||
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) {
|
||||
val sortedRecords = groupRecords.sortedBy { it.timestamp }
|
||||
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
|
||||
val groupKey = "${latestRecord.train}_OR_${latestRecord.loco}"
|
||||
MergedTrainRecord(
|
||||
groupKey = groupKey,
|
||||
records = sortedRecords,
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
@@ -38,23 +39,24 @@ fun SettingsScreen(
|
||||
onScrollPositionChange: (Int) -> Unit = {},
|
||||
specifiedDeviceAddress: String? = null,
|
||||
searchOrderList: List<String> = emptyList(),
|
||||
onSpecifiedDeviceSelected: (String?) -> Unit = {}
|
||||
onSpecifiedDeviceSelected: (String?) -> Unit = {},
|
||||
autoConnectEnabled: Boolean = true,
|
||||
onAutoConnectEnabledChange: (Boolean) -> Unit = {}
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
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()
|
||||
@@ -196,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())
|
||||
}
|
||||
|
||||
@@ -262,6 +265,29 @@ fun SettingsScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
"自动连接",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
"自动连接蓝牙设备",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = autoConnectEnabled,
|
||||
onCheckedChange = onAutoConnectEnabledChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
99
app/src/main/res/layout/notification_train_record.xml
Normal file
99
app/src/main/res/layout/notification_train_record.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_train_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="G1234"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:layout_marginEnd="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_direction"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:text="下"
|
||||
android:textSize="10sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/white"
|
||||
android:background="@android:color/black"
|
||||
android:gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_loco_info"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="CRH380D-1234"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_route"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="京沪高铁"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:layout_marginEnd="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_position"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="1234K"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/notification_speed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="300 km/h"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?android:attr/textColorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
Reference in New Issue
Block a user