fix: various issues
This commit is contained in:
114
.github/workflows/android-release.yml
vendored
Normal file
114
.github/workflows/android-release.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
name: Android Release Build
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Build Debug APK
|
||||
run: ./gradlew assembleDebug
|
||||
|
||||
- name: Build Release APK
|
||||
run: ./gradlew assembleRelease
|
||||
env:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
|
||||
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
|
||||
|
||||
- name: Upload Debug APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug-apk
|
||||
path: app/build/outputs/apk/debug/*.apk
|
||||
|
||||
- name: Upload Release APK
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apk
|
||||
path: app/build/outputs/apk/release/*.apk
|
||||
|
||||
- name: Create Release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
app/build/outputs/apk/debug/*.apk
|
||||
app/build/outputs/apk/release/*.apk
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
|
||||
- name: Cache Gradle packages
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Run tests
|
||||
run: ./gradlew test
|
||||
|
||||
- name: Run lint
|
||||
run: ./gradlew lint
|
||||
|
||||
- name: Upload test results
|
||||
uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: test-results
|
||||
path: |
|
||||
app/build/reports/tests/
|
||||
app/build/reports/lint-results.html
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,15 +1,15 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.idea/caches
|
||||
.idea/libraries
|
||||
.idea/modules.xml
|
||||
.idea/workspace.xml
|
||||
.idea/navEditor.xml
|
||||
.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
build
|
||||
captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
local.properties
|
||||
|
||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
||||
LBJ_Console
|
||||
LBJ Receiver
|
||||
4
LICENSE
4
LICENSE
@@ -631,7 +631,7 @@ to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
LBJ_Console is an Android app designed to receive and display LBJ (Locomotive Bell Jingling) messages via BLE from the SX1276_Receive_LBJ device.
|
||||
LBJ Console is an Android app designed to receive and display LBJ (Locomotive Bell Jingling) messages via BLE from the SX1276_Receive_LBJ device.
|
||||
Copyright (C) 2025 undef-i
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
LBJ_Console Copyright (C) 2025 undef-i
|
||||
LBJ Console Copyright (C) 2025 undef-i
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
14
README.md
14
README.md
@@ -1,7 +1,15 @@
|
||||
# LBJ_Console
|
||||
# LBJ Console
|
||||
|
||||
LBJ_Console is an Android app designed to receive and display LBJ messages via BLE from the [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device.
|
||||
LBJ Console is an Android app designed to receive and display LBJ messages via BLE from the [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device.
|
||||
|
||||
## Roadmap
|
||||
- Tab state persistence
|
||||
- Record filtering (train number, time range)
|
||||
- Record management page optimization
|
||||
- Optional train merge by locomotive/number
|
||||
- Offline data storage
|
||||
- Add record timestamps
|
||||
|
||||
# License
|
||||
|
||||
This project is licensed under the GNU General Public License v3.0 (GPLv3). This license ensures that the software remains free and open source, requiring that any modifications or derivative works must also be released under the same license terms.
|
||||
This project is licensed under the GNU General Public License v3.0 (GPLv3). This license ensures that the software remains free and open source, requiring that any modifications or derivative works must also be released under the same license terms.
|
||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/build
|
||||
@@ -5,13 +5,13 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "receiver.lbj"
|
||||
namespace = "org.noxylva.lbjconsole"
|
||||
compileSdk = 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "receiver.lbj"
|
||||
applicationId = "org.noxylva.lbjconsole"
|
||||
minSdk = 29
|
||||
targetSdk = 34
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package receiver.lbj
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("receiver.lbj", appContext.packageName)
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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 ->
|
||||
@@ -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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package receiver.lbj.model
|
||||
package org.noxylva.lbjconsole.model
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
@@ -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(
|
||||
@@ -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(
|
||||
@@ -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.*
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -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()
|
||||
@@ -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.*
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package receiver.lbj.ui.theme
|
||||
package org.noxylva.lbjconsole.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
@@ -1,4 +1,4 @@
|
||||
package receiver.lbj.util
|
||||
package org.noxylva.lbjconsole.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
@@ -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("应用设备名称")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
<resources>
|
||||
<string name="app_name">LBJ Receiver</string>
|
||||
<string name="app_name">LBJ Console</string>
|
||||
</resources>
|
||||
@@ -1,13 +0,0 @@
|
||||
package receiver.lbj
|
||||
|
||||
import org.junit.Test
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
|
||||
class ExampleUnitTest {
|
||||
@Test
|
||||
fun addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user