3 Commits

Author SHA1 Message Date
Nedifinita
39bb8cb440 fix: optimize the logic for saving scroll position 2025-08-01 17:35:34 +08:00
Nedifinita
be8dc6bc72 feat: add custom train information notification layout 2025-07-26 17:31:09 +08:00
Nedifinita
cd3128c24b feat: add option for automatically connecting to Bluetooth devices 2025-07-26 17:08:08 +08:00
9 changed files with 405 additions and 85 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

@@ -276,6 +276,33 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
return isConnected 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") @SuppressLint("MissingPermission")
fun disconnect() { fun disconnect() {

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
@@ -117,6 +118,7 @@ class MainActivity : ComponentActivity() {
private var specifiedDeviceAddress by mutableStateOf<String?>(null) private var specifiedDeviceAddress by mutableStateOf<String?>(null)
private var searchOrderList by mutableStateOf(listOf<String>()) private var searchOrderList by mutableStateOf(listOf<String>())
private var showDisconnectButton by mutableStateOf(false) private var showDisconnectButton by mutableStateOf(false)
private var autoConnectEnabled by mutableStateOf(true)
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) } private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
@@ -313,6 +315,12 @@ class MainActivity : ComponentActivity() {
saveSettings() saveSettings()
Log.d(TAG, "Set specified device address: $address") Log.d(TAG, "Set specified device address: $address")
}, },
autoConnectEnabled = autoConnectEnabled,
onAutoConnectEnabledChange = { enabled ->
autoConnectEnabled = enabled
saveSettings()
Log.d(TAG, "Auto connect enabled: $enabled")
},
latestRecord = latestRecord, latestRecord = latestRecord,
@@ -411,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,
@@ -615,6 +625,11 @@ class MainActivity : ComponentActivity() {
private fun startAutoScanAndConnect() { private fun startAutoScanAndConnect() {
if (!autoConnectEnabled) {
Log.d(TAG, "Auto connect disabled, skipping auto scan")
return
}
Log.d(TAG, "Starting auto scan and connect") Log.d(TAG, "Starting auto scan and connect")
if (!hasBluetoothPermissions()) { if (!hasBluetoothPermissions()) {
@@ -774,34 +789,39 @@ class MainActivity : ComponentActivity() {
searchOrderStr.split(",").filter { it.isNotBlank() } searchOrderStr.split(",").filter { it.isNotBlank() }
} }
autoConnectEnabled = settingsPrefs.getBoolean("auto_connect_enabled", true)
bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress) 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() { 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)
.putString("search_order_list", searchOrderList.joinToString(","))
.putBoolean("auto_connect_enabled", autoConnectEnabled)
mapCenterPosition?.let { (lat, lon) -> mapCenterPosition?.let { (lat, lon) ->
editor.putFloat("map_center_lat", lat.toFloat()) editor.putFloat("map_center_lat", lat.toFloat())
editor.putFloat("map_center_lon", lon.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() { override fun onResume() {
@@ -810,12 +830,20 @@ class MainActivity : ComponentActivity() {
bleClient.setHighFrequencyReconnect(true) bleClient.setHighFrequencyReconnect(true)
if (hasBluetoothPermissions() && !bleClient.isConnected()) { if (hasBluetoothPermissions()) {
Log.d(TAG, "App resumed and not connected, starting auto scan") val actuallyConnected = bleClient.checkActualConnectionState()
startAutoScanAndConnect()
} else if (bleClient.isConnected()) { if (actuallyConnected) {
showDisconnectButton = true showDisconnectButton = true
deviceStatus = "已连接" 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?, specifiedDeviceAddress: String?,
searchOrderList: List<String>, searchOrderList: List<String>,
onSpecifiedDeviceSelected: (String?) -> Unit, onSpecifiedDeviceSelected: (String?) -> Unit,
autoConnectEnabled: Boolean,
onAutoConnectEnabledChange: (Boolean) -> Unit,
latestRecord: TrainRecord?, latestRecord: TrainRecord?,
@@ -1118,7 +1148,9 @@ fun MainContent(
onScrollPositionChange = onSettingsScrollPositionChange, onScrollPositionChange = onSettingsScrollPositionChange,
specifiedDeviceAddress = specifiedDeviceAddress, specifiedDeviceAddress = specifiedDeviceAddress,
searchOrderList = searchOrderList, searchOrderList = searchOrderList,
onSpecifiedDeviceSelected = onSpecifiedDeviceSelected onSpecifiedDeviceSelected = onSpecifiedDeviceSelected,
autoConnectEnabled = autoConnectEnabled,
onAutoConnectEnabledChange = onAutoConnectEnabledChange
) )
3 -> MapScreen( 3 -> MapScreen(
records = if (allRecords.isNotEmpty()) { records = if (allRecords.isNotEmpty()) {

View File

@@ -8,11 +8,14 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import org.json.JSONObject import org.json.JSONObject
import org.noxylva.lbjconsole.model.TrainRecord import org.noxylva.lbjconsole.model.TrainRecord
class NotificationService(private val context: Context) { class NotificationService(private val context: Context) {
companion object { companion object {
const val TAG = "NotificationService" const val TAG = "NotificationService"
@@ -86,12 +89,7 @@ class NotificationService(private val context: Context) {
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
) )
val directionText = when (trainRecord.direction) { val remoteViews = RemoteViews(context.packageName, R.layout.notification_train_record)
1 -> "下行"
3 -> "上行"
else -> "未知"
}
val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) { val trainDisplay = if (isValidValue(trainRecord.lbjClass) && isValidValue(trainRecord.train)) {
"${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}" "${trainRecord.lbjClass.trim()}${trainRecord.train.trim()}"
} else if (isValidValue(trainRecord.lbjClass)) { } else if (isValidValue(trainRecord.lbjClass)) {
@@ -99,26 +97,83 @@ class NotificationService(private val context: Context) {
} else if (isValidValue(trainRecord.train)) { } else if (isValidValue(trainRecord.train)) {
trainRecord.train.trim() trainRecord.train.trim()
} else "列车" } else "列车"
remoteViews.setTextViewText(R.id.notification_train_number, trainDisplay)
val title = trainDisplay val directionText = when (trainRecord.direction) {
val content = buildString { 1 -> ""
append(directionText) 3 -> ""
if (isValidValue(trainRecord.route)) { else -> ""
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")
}
} }
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) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)
.setContentTitle(title) .setContentTitle(trainDisplay)
.setContentText(content) .setContentText(summaryText)
.setStyle(NotificationCompat.BigTextStyle().bigText(content)) .setCustomContentView(remoteViews)
.setCustomBigContentView(remoteViews)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
@@ -131,7 +186,7 @@ class NotificationService(private val context: Context) {
} }
notificationManager.notify(notificationId, notification) 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) { } catch (e: Exception) {
Log.e(TAG, "Failed to show notification: ${e.message}", e) Log.e(TAG, "Failed to show notification: ${e.message}", e)

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
} ?: true
if (withinTimeWindow) { if ((train.isEmpty() || train == "<NUL>") && (loco.isEmpty() || loco == "<NUL>")) {
groupedRecords.getOrPut(groupKey) { mutableListOf() }.add(record) 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) { 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) {
@@ -942,6 +944,22 @@ fun HistoryScreen(
} }
} }
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()) {
Box( Box(

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
@@ -38,23 +39,24 @@ fun SettingsScreen(
onScrollPositionChange: (Int) -> Unit = {}, onScrollPositionChange: (Int) -> Unit = {},
specifiedDeviceAddress: String? = null, specifiedDeviceAddress: String? = null,
searchOrderList: List<String> = emptyList(), searchOrderList: List<String> = emptyList(),
onSpecifiedDeviceSelected: (String?) -> Unit = {} onSpecifiedDeviceSelected: (String?) -> Unit = {},
autoConnectEnabled: Boolean = true,
onAutoConnectEnabledChange: (Boolean) -> Unit = {}
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
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()
@@ -196,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())
} }
@@ -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
)
}
} }
} }

View 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>