fix: various issues

This commit is contained in:
Nedifinita
2025-07-14 23:15:01 +08:00
parent 644374cc41
commit d3d8e19715
28 changed files with 462 additions and 251 deletions

View File

@@ -4,12 +4,11 @@
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" android:usesPermissionFlags="neverForLocation"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

View File

@@ -1,17 +1,22 @@
package receiver.lbj
package org.noxylva.lbjconsole
import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.BluetoothLeScanner
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import java.nio.charset.StandardCharsets
import org.json.JSONObject
import java.util.*
class BLEClient(private val context: Context) : BluetoothGattCallback(),
BluetoothAdapter.LeScanCallback {
class BLEClient(private val context: Context) : BluetoothGattCallback() {
companion object {
const val TAG = "LBJ_BT"
const val SCAN_PERIOD = 10000L
@@ -35,6 +40,24 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
private var trainInfoCallback: ((JSONObject) -> Unit)? = null
private var handler = Handler(Looper.getMainLooper())
private var targetDeviceName: String? = null
private var bluetoothLeScanner: BluetoothLeScanner? = null
private val leScanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val deviceName = device.name
if (targetDeviceName != null) {
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
return
}
}
scanCallback?.invoke(device)
}
override fun onScanFailed(errorCode: Int) {
Log.e(TAG, "BLE scan failed code=$errorCode")
}
}
fun setTrainInfoCallback(callback: (JSONObject) -> Unit) {
@@ -42,8 +65,28 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
}
private fun hasBluetoothPermissions(): Boolean {
val bluetoothPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
} else {
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH) == PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_ADMIN) == PackageManager.PERMISSION_GRANTED
}
val locationPermissions = ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
ContextCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
return bluetoothPermissions && locationPermissions
}
@SuppressLint("MissingPermission")
fun scanDevices(targetDeviceName: String? = null, callback: (BluetoothDevice) -> Unit) {
if (!hasBluetoothPermissions()) {
Log.e(TAG, "Bluetooth permissions not granted")
return
}
try {
scanCallback = callback
this.targetDeviceName = targetDeviceName
@@ -57,13 +100,19 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
return
}
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
if (bluetoothLeScanner == null) {
Log.e(TAG, "BluetoothLeScanner unavailable")
return
}
handler.postDelayed({
stopScan()
}, SCAN_PERIOD)
isScanning = true
Log.d(TAG, "Starting BLE scan target=${targetDeviceName ?: "Any"}")
bluetoothAdapter.startLeScan(this)
bluetoothLeScanner?.startScan(leScanCallback)
} catch (e: SecurityException) {
Log.e(TAG, "Scan security error: ${e.message}")
} catch (e: Exception) {
@@ -75,28 +124,25 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
@SuppressLint("MissingPermission")
fun stopScan() {
if (isScanning) {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
bluetoothAdapter.stopLeScan(this)
bluetoothLeScanner?.stopScan(leScanCallback)
isScanning = false
}
}
override fun onLeScan(device: BluetoothDevice, rssi: Int, scanRecord: ByteArray) {
val deviceName = device.name
if (targetDeviceName != null) {
if (deviceName == null || !deviceName.equals(targetDeviceName, ignoreCase = true)) {
return
}
}
scanCallback?.invoke(device)
}
@SuppressLint("MissingPermission")
fun connect(address: String, onConnectionStateChange: ((Boolean) -> Unit)? = null): Boolean {
Log.d(TAG, "Attempting to connect to device: $address")
if (!hasBluetoothPermissions()) {
Log.e(TAG, "Bluetooth permissions not granted")
handler.post { onConnectionStateChange?.invoke(false) }
return false
}
if (address.isBlank()) {
Log.e(TAG, "Connection failed empty address")
handler.post { onConnectionStateChange?.invoke(false) }
@@ -109,6 +155,18 @@ class BLEClient(private val context: Context) : BluetoothGattCallback(),
handler.post { onConnectionStateChange?.invoke(false) }
return false
}
if (!bluetoothAdapter.isEnabled) {
Log.e(TAG, "Bluetooth adapter is disabled")
handler.post { onConnectionStateChange?.invoke(false) }
return false
}
if (isConnected) {
Log.w(TAG, "Already connected to device")
handler.post { onConnectionStateChange?.invoke(true) }
return true
}
bluetoothGatt?.close()

View File

@@ -1,4 +1,4 @@
package receiver.lbj
package org.noxylva.lbjconsole
import android.Manifest
import android.bluetooth.BluetoothAdapter
@@ -6,7 +6,7 @@ import android.bluetooth.BluetoothDevice
import android.content.Context
import android.content.Intent
import java.io.File
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
@@ -19,36 +19,30 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.LocationOn
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.FileProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.json.JSONObject
import org.osmdroid.config.Configuration
import receiver.lbj.model.TrainRecord
import receiver.lbj.model.TrainRecordManager
import receiver.lbj.ui.screens.HistoryScreen
import receiver.lbj.ui.screens.MapScreen
import receiver.lbj.ui.screens.SettingsScreen
import receiver.lbj.ui.screens.MapScreen
import receiver.lbj.ui.theme.LBJReceiverTheme
import receiver.lbj.util.LocoInfoUtil
import org.noxylva.lbjconsole.model.TrainRecord
import org.noxylva.lbjconsole.model.TrainRecordManager
import org.noxylva.lbjconsole.ui.screens.HistoryScreen
import org.noxylva.lbjconsole.ui.screens.MapScreen
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
import org.noxylva.lbjconsole.ui.theme.LBJReceiverTheme
import org.noxylva.lbjconsole.util.LocoInfoUtil
import java.util.*
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
@@ -80,7 +74,10 @@ class MainActivity : ComponentActivity() {
private var temporaryStatusMessage by mutableStateOf<String?>(null)
private var targetDeviceName = "LBJReceiver"
private var targetDeviceName = "LBJReceiver"
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
private val requestPermissions = registerForActivityResult(
@@ -133,17 +130,31 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
requestPermissions.launch(arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
loadSettings()
val permissions = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
permissions.addAll(arrayOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_ADVERTISE
))
} else {
permissions.addAll(arrayOf(
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
))
}
permissions.addAll(arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
Manifest.permission.ACCESS_COARSE_LOCATION
))
requestPermissions.launch(permissions.toTypedArray())
bleClient.setTrainInfoCallback { jsonData ->
handleTrainInfo(jsonData)
@@ -263,17 +274,36 @@ class MainActivity : ComponentActivity() {
deviceName = settingsDeviceName,
onDeviceNameChange = { newName -> settingsDeviceName = newName },
onApplySettings = {
saveSettings()
targetDeviceName = settingsDeviceName
Toast.makeText(this, "设备名称 '${settingsDeviceName}' 已保存,下次连接时生效", Toast.LENGTH_LONG).show()
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
},
locoInfoUtil = locoInfoUtil
)
// 显示连接对话框
if (showConnectionDialog) {
ConnectionDialog(
isScanning = isScanning,
devices = foundDevices,
onDismiss = {
showConnectionDialog = false
stopScan()
},
onScan = {
if (isScanning) {
stopScan()
} else {
startScan()
}
},
onConnect = { device ->
showConnectionDialog = false
connectToDevice(device)
}
)
}
}
}
}
@@ -284,13 +314,23 @@ class MainActivity : ComponentActivity() {
deviceStatus = "正在连接..."
Log.d(TAG, "Connecting to device name=${device.name ?: "Unknown"} address=${device.address}")
// 检查蓝牙适配器状态
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
deviceStatus = "蓝牙未启用"
Log.e(TAG, "Bluetooth adapter unavailable or disabled")
return
}
bleClient.connect(device.address) { connected ->
if (connected) {
deviceStatus = "已连接"
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
} else {
deviceStatus = "连接失败或已断开连接"
Log.e(TAG, "Connection failed name=${device.name ?: "Unknown"}")
runOnUiThread {
if (connected) {
deviceStatus = "已连接"
Log.d(TAG, "Connected to device name=${device.name ?: "Unknown"}")
} else {
deviceStatus = "连接失败或已断开连接"
Log.e(TAG, "Connection failed name=${device.name ?: "Unknown"}")
}
}
}
@@ -382,6 +422,19 @@ class MainActivity : ComponentActivity() {
private fun startScan() {
val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
if (bluetoothAdapter == null) {
Log.e(TAG, "Bluetooth adapter unavailable")
deviceStatus = "设备不支持蓝牙"
return
}
if (!bluetoothAdapter.isEnabled) {
Log.e(TAG, "Bluetooth adapter disabled")
deviceStatus = "请启用蓝牙"
return
}
isScanning = true
foundDevices = emptyList()
val targetDeviceName = settingsDeviceName.ifBlank { null }
@@ -396,8 +449,11 @@ class MainActivity : ComponentActivity() {
Log.d(TAG, "Found target=$targetDeviceName, connecting")
stopScan()
connectToDevice(device)
} else if (!foundDevices.any { it.address == device.address }) {
showConnectionDialog = true
} else {
// 如果没有指定目标设备名称,或者找到的设备不是目标设备,显示连接对话框
if (targetDeviceName == null) {
showConnectionDialog = true
}
}
}
}
@@ -414,6 +470,21 @@ class MainActivity : ComponentActivity() {
private fun updateDeviceList() {
foundDevices = scanResults.map { it.device }
}
private fun loadSettings() {
settingsDeviceName = settingsPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver"
targetDeviceName = settingsDeviceName
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName}")
}
private fun saveSettings() {
settingsPrefs.edit()
.putString("device_name", settingsDeviceName)
.apply()
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName}")
}
}
@OptIn(ExperimentalMaterial3Api::class)
@@ -478,7 +549,7 @@ fun MainContent(
Scaffold(
topBar = {
TopAppBar(
title = { Text("LBJReceiver") },
title = { Text("LBJ Console") },
actions = {
timeSinceLastUpdate.value?.let { time ->

View File

@@ -1,11 +1,10 @@
package receiver.lbj.model
package org.noxylva.lbjconsole.model
import android.util.Log
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.*
import org.osmdroid.util.GeoPoint
import receiver.lbj.util.LocationUtils
import org.noxylva.lbjconsole.util.LocationUtils
class TrainRecord(jsonData: JSONObject? = null) {
companion object {

View File

@@ -1,4 +1,4 @@
package receiver.lbj.model
package org.noxylva.lbjconsole.model
import android.content.Context
import android.content.SharedPreferences

View File

@@ -1,4 +1,4 @@
package receiver.lbj.ui.components
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@@ -16,7 +16,7 @@ import androidx.compose.ui.window.DialogProperties
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.Marker
import receiver.lbj.model.TrainRecord
import org.noxylva.lbjconsole.model.TrainRecord
@Composable
fun TrainDetailDialog(

View File

@@ -1,4 +1,4 @@
package receiver.lbj.ui.components
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -10,7 +10,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.material3.HorizontalDivider
import receiver.lbj.model.TrainRecord
import org.noxylva.lbjconsole.model.TrainRecord
@Composable
fun TrainInfoCard(

View File

@@ -1,4 +1,4 @@
package receiver.lbj.ui.components
package org.noxylva.lbjconsole.ui.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -9,7 +9,6 @@ import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.material3.TopAppBarDefaults.topAppBarColors
import androidx.compose.runtime.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.ui.Alignment
@@ -18,7 +17,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import receiver.lbj.model.TrainRecord
import org.noxylva.lbjconsole.model.TrainRecord
import java.text.SimpleDateFormat
import java.util.*

View File

@@ -1,10 +1,5 @@
package receiver.lbj.ui.screens
package org.noxylva.lbjconsole.ui.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.background
@@ -17,18 +12,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.SignalCellular4Bar
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.geometry.Offset
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -42,8 +30,8 @@ import org.osmdroid.views.overlay.Marker
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import org.osmdroid.views.overlay.TilesOverlay
import receiver.lbj.model.TrainRecord
import receiver.lbj.util.LocoInfoUtil
import org.noxylva.lbjconsole.model.TrainRecord
import org.noxylva.lbjconsole.util.LocoInfoUtil
import java.text.SimpleDateFormat
import java.util.*

View File

@@ -1,25 +1,15 @@
package receiver.lbj.ui.screens
package org.noxylva.lbjconsole.ui.screens
import android.Manifest
import android.content.Context
import receiver.lbj.R
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.location.Location
import android.location.LocationListener
import android.util.Log
import android.location.LocationManager
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.MyLocation
import androidx.compose.material.icons.filled.Layers
import androidx.compose.material3.*
@@ -36,19 +26,15 @@ import androidx.lifecycle.LifecycleEventObserver
import kotlinx.coroutines.launch
import org.osmdroid.config.Configuration
import org.osmdroid.tileprovider.MapTileProviderBasic
import org.osmdroid.tileprovider.tilesource.OnlineTileSourceBase
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
import org.osmdroid.tileprovider.tilesource.XYTileSource
import org.osmdroid.util.GeoPoint
import org.osmdroid.views.MapView
import org.osmdroid.views.overlay.*
import org.osmdroid.views.overlay.compass.CompassOverlay
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import receiver.lbj.model.TrainRecord
import org.noxylva.lbjconsole.model.TrainRecord
import java.io.File
import java.util.*
@Composable
@@ -184,13 +170,7 @@ fun MapScreen(
mapView.invalidate()
if (!isMapInitialized && validRecords.isNotEmpty()) {
validRecords.firstOrNull()?.getCoordinates()?.let { point ->
mapView.controller.setZoom(12.0)
mapView.controller.setCenter(point)
isMapInitialized = true
}
}
}
@@ -258,7 +238,7 @@ fun MapScreen(
val railwayTileSource = XYTileSource(
"OpenRailwayMap",
0, 24, // 穷尽所有可能的缩放级别
0, 24,
256,
".png",
arrayOf(
@@ -297,7 +277,15 @@ fun MapScreen(
}
controller.setZoom(10.0)
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
controller.setZoom(12.0)
}
} else {
controller.setCenter(defaultPosition)
controller.setZoom(10.0)
}
try {
@@ -307,28 +295,8 @@ fun MapScreen(
locationUpdateMinTime = 1000
}
val myLocationOverlay = MyLocationNewOverlay(locationProvider, this).apply {
// 使用我们的自定义人形图标
// 使用原生位置/雷达图标
val personDrawable = ctx.resources.getDrawable(android.R.drawable.ic_menu_mylocation, ctx.theme)
// 设置为黑色
personDrawable.setTint(Color.BLACK)
val bitmap = Bitmap.createBitmap(
personDrawable.intrinsicWidth,
personDrawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
personDrawable.setBounds(0, 0, canvas.width, canvas.height)
personDrawable.draw(canvas)
setPersonIcon(bitmap)
// 设置中心对齐
setPersonAnchor(0.5f, 0.5f)
isDrawAccuracyEnabled = false
enableMyLocation()
runOnFirstFix {
@@ -337,21 +305,40 @@ fun MapScreen(
currentLocation = GeoPoint(location.latitude, location.longitude)
if (!isMapInitialized) {
controller.animateTo(location)
isMapInitialized = true
}
controller.setCenter(location)
controller.setZoom(15.0)
isMapInitialized = true
Log.d("MapScreen", "Map initialized with GPS position: $location")
}
} ?: run {
if (!isMapInitialized) {
controller.animateTo(defaultPosition)
}
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) {
controller.animateTo(defaultPosition)
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { lastPoint ->
controller.setCenter(lastPoint)
controller.setZoom(12.0)
isMapInitialized = true
Log.d("MapScreen", "Map fallback to last record position: $lastPoint")
}
} else {
controller.setCenter(defaultPosition)
isMapInitialized = true
}
}
}
}
@@ -425,7 +412,7 @@ fun MapScreen(
overlay.enableFollowLocation()
overlay.enableMyLocation()
overlay.myLocation?.let { location ->
mapViewRef.value?.controller?.animateTo(location)
mapViewRef.value?.controller?.setCenter(location)
}
}
},
@@ -467,14 +454,22 @@ fun MapScreen(
FloatingActionButton(
onClick = {
mapViewRef.value?.let { mapView ->
if (validRecords.isNotEmpty()) {
validRecords.firstOrNull()?.getCoordinates()?.let { point ->
mapView.controller.animateTo(point)
mapView.controller.setZoom(12.0)
myLocationOverlayRef.value?.myLocation?.let { gpsLocation ->
mapView.controller.setCenter(gpsLocation)
mapView.controller.setZoom(15.0)
Log.d("MapScreen", "Refresh button: GPS position used")
} ?: run {
if (validRecords.isNotEmpty()) {
validRecords.lastOrNull()?.getCoordinates()?.let { point ->
mapView.controller.setCenter(point)
mapView.controller.setZoom(12.0)
Log.d("MapScreen", "Refresh button: last record position used")
}
} else {
mapView.controller.setCenter(defaultPosition)
mapView.controller.setZoom(10.0)
Log.d("MapScreen", "Refresh button: default position used")
}
} else {
mapView.controller.animateTo(defaultPosition)
mapView.controller.setZoom(10.0)
}
}
onCenterMap()

View File

@@ -1,9 +1,7 @@
package receiver.lbj.ui.screens
package org.noxylva.lbjconsole.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -14,8 +12,8 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import receiver.lbj.model.TrainRecord
import receiver.lbj.ui.components.TrainDetailDialog
import org.noxylva.lbjconsole.model.TrainRecord
import org.noxylva.lbjconsole.ui.components.TrainDetailDialog
import java.text.SimpleDateFormat
import java.util.*

View File

@@ -0,0 +1,57 @@
package org.noxylva.lbjconsole.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
deviceName: String,
onDeviceNameChange: (String) -> Unit,
onApplySettings: () -> Unit
) {
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
OutlinedTextField(
value = deviceName,
onValueChange = onDeviceNameChange,
label = { Text("蓝牙设备名称") },
modifier = Modifier.fillMaxWidth(),
)
Button(
onClick = onApplySettings,
modifier = Modifier.fillMaxWidth()
) {
Text("应用设置")
}
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = "LBJ Console v0.0.1 by undef-i",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.clickable {
uriHandler.openUri("https://github.com/undef-i")
}
)
}
}

View File

@@ -1,4 +1,4 @@
package receiver.lbj.ui.theme
package org.noxylva.lbjconsole.ui.theme
import androidx.compose.ui.graphics.Color

View File

@@ -1,8 +1,6 @@
package receiver.lbj.ui.theme
package org.noxylva.lbjconsole.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme

View File

@@ -1,4 +1,4 @@
package receiver.lbj.ui.theme
package org.noxylva.lbjconsole.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle

View File

@@ -1,8 +1,7 @@
package receiver.lbj.util
package org.noxylva.lbjconsole.util
import android.util.Log
import org.osmdroid.util.GeoPoint
import kotlin.math.abs
object LocationUtils {

View File

@@ -1,4 +1,4 @@
package receiver.lbj.util
package org.noxylva.lbjconsole.util
import android.content.Context
import android.util.Log

View File

@@ -1,38 +0,0 @@
package receiver.lbj.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
deviceName: String,
onDeviceNameChange: (String) -> Unit,
onApplySettings: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text("设置", style = MaterialTheme.typography.headlineMedium)
OutlinedTextField(
value = deviceName,
onValueChange = onDeviceNameChange,
label = { Text("蓝牙设备名称") },
modifier = Modifier.fillMaxWidth()
)
Button(onClick = onApplySettings, modifier = Modifier.fillMaxWidth()) {
Text("应用设备名称")
}
}
}

View File

@@ -1,3 +1,3 @@
<resources>
<string name="app_name">LBJ Receiver</string>
<string name="app_name">LBJ Console</string>
</resources>