Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65bf7b52c6 | ||
|
|
4278de2a8d | ||
|
|
59e9987d7f | ||
|
|
4e97dcafd7 | ||
|
|
4cad3679a9 | ||
|
|
e6e7831b96 |
1
.gitignore
vendored
@@ -13,7 +13,6 @@ captures
|
|||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
local.properties
|
|
||||||
*.ps1
|
*.ps1
|
||||||
.*.bat
|
.*.bat
|
||||||
*.jks
|
*.jks
|
||||||
|
|||||||
17
README.md
@@ -1,8 +1,19 @@
|
|||||||
# 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 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) device 设备接收并显示列车预警消息,功能包括:
|
||||||
|
|
||||||
|
- 接收列车预警消息,支持可选的手机推送通知。
|
||||||
|
- 显示预警消息的 GPS 信息于地图。
|
||||||
|
- 基于内置数据文件显示机车配属和车次类型。
|
||||||
|
|
||||||
|
|
||||||
# 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.
|
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
|
||||||
|
- `loco_info.csv`:包含机车配属信息,格式为 `机车型号,机车编号起始值,机车编号结束值,所属铁路局及机务段,备注`。
|
||||||
|
- `train_info.csv`:包含车次类型信息,格式为 `正则表达式,车次类型`。
|
||||||
|
|
||||||
|
|
||||||
|
# 许可证
|
||||||
|
|
||||||
|
该项目采用 GNU 通用公共许可证 v3.0(GPLv3)授权。该许可证确保软件保持免费和开源,要求任何修改或衍生作品也必须在相同许可证条款下发布。
|
||||||
@@ -2,6 +2,7 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
alias(libs.plugins.ksp)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -12,8 +13,8 @@ android {
|
|||||||
applicationId = "org.noxylva.lbjconsole"
|
applicationId = "org.noxylva.lbjconsole"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 8
|
versionCode = 11
|
||||||
versionName = "0.0.8"
|
versionName = "0.1.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -59,6 +60,7 @@ android {
|
|||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
disable += "NullSafeMutableLiveData"
|
disable += "NullSafeMutableLiveData"
|
||||||
|
warning += "MissingPermission"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,4 +87,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation("org.osmdroid:osmdroid-android:6.1.16")
|
implementation("org.osmdroid:osmdroid-android:6.1.16")
|
||||||
implementation("org.osmdroid:osmdroid-mapsforge:6.1.16")
|
implementation("org.osmdroid:osmdroid-mapsforge:6.1.16")
|
||||||
|
|
||||||
|
implementation(libs.androidx.room.runtime)
|
||||||
|
implementation(libs.androidx.room.ktx)
|
||||||
|
ksp(libs.androidx.room.compiler)
|
||||||
|
implementation(libs.androidx.startup.runtime)
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
|
||||||
|
|
||||||
|
|||||||
82
app/src/main/assets/train_number_info.csv
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"^[Gg](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定高速动车组"
|
||||||
|
"^[Gg](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客高速动车组"
|
||||||
|
"^[Gg](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定高速动车组"
|
||||||
|
"^[Gg](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客高速动车组"
|
||||||
|
"^[Cc]([1-8]\d{3}|9000)$","图定城际动车组"
|
||||||
|
"^[Cc](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","临客城际动车组"
|
||||||
|
"^[Cc][1-9]\d{2}$","动力集中城际动车组"
|
||||||
|
"^[IDid](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定动车组"
|
||||||
|
"^[IDid](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客动车组"
|
||||||
|
"^[IDid](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定动车组"
|
||||||
|
"^[IDid](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客动车组"
|
||||||
|
"^[IDid](8([0-8]\d|9[0-8])|7(0[1-9]|[1-9]\d))$","动力集中动车组"
|
||||||
|
"^[IDid](300|[12]\d{2}|[1-9]\d?)$","跨局动力集中动车组"
|
||||||
|
"^[PZpz](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定直达特快旅客列车"
|
||||||
|
"^[PZpz](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客直达特快旅客列车"
|
||||||
|
"^[PZpz](9000|[6-8]\d{3}|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定直达特快旅客列车"
|
||||||
|
"^[PZpz](900[1-9]|90[1-9]\d|9[1-8]\d{2}|99[0-8]\d|999[0-8])$","管内临客直达特快旅客列车"
|
||||||
|
"^[QTqt](3000|[12]\d{3}|[1-9]\d{0,2})$","直通图定特快旅客列车"
|
||||||
|
"^[QTqt](300[1-9]|30[1-9]\d|3[1-8]\d{2}|39[0-8]\d|399[0-8])$","直通临客特快旅客列车"
|
||||||
|
"^[QTqt](4(00[1-9]|0[1-9]\d|[1-8]\d{2}|9[0-8]\d|99[0-8]))$","管内临客特快旅客列车"
|
||||||
|
"^[QTqt]([5-8]\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内图定特快旅客列车"
|
||||||
|
"^[WKwk](4000|[1-3]\d{3}|[1-9]\d{0,2})$","直通图定快速旅客列车"
|
||||||
|
"^[WKwk](400[1-9]|40[1-9]\d|4[1-8]\d{2}|49[0-8]\d|499[0-8])$","直通临客快速旅客列车"
|
||||||
|
"^[WKwk](6([0-8]\d{2}|9[0-8]\d|99[0-8])|500[1-9]|50[1-9]\d|5[1-9]\d{2})$","管内临客快速旅客列车"
|
||||||
|
"^[WKwk](8\d{3}|9([0-8]\d{2}|9[0-8]\d|99[0-8])|7(00[1-9]|0[1-9]\d|[1-9]\d{2}))$","管内图定快速旅客列车"
|
||||||
|
"^[Vv1](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨三局及以上图定普通旅客快车"
|
||||||
|
"^[Bb2](00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨两局图定普通旅客快车"
|
||||||
|
"^3(00[1-9]|0[1-9]\d|[1-9]\d{2})$","跨局临时普通旅客快车"
|
||||||
|
"^[Uu4](00[1-9]|0[1-9]\d|[1-9]\d{2})$","管内图定普通旅客快车"
|
||||||
|
"^[Xx5](000|1[9][9]|200|3[9][9]|400)$","管内图定普通旅客快车"
|
||||||
|
"^6(19[0-8]|1[0-8]\d|0[1-9]\d|00[1-9])$","直通普通旅客慢车"
|
||||||
|
"^(6(20[1-9]|2[1-9]\d|[3-9]\d{2})|7([0-4]\d{2}|5([0-8]\d|9[0-8])))$","管内普通旅客慢车"
|
||||||
|
"^(8([0-8]\d{2}|9[0-8]\d|99[0-8])|7(60[1-9]|6[1-9]\d|[7-9]\d{2}))$","通勤列车"
|
||||||
|
"^[Yy](500|[1-4]\d{2}|[1-9]\d?)$","跨局旅游列车"
|
||||||
|
"^[Yy](50[1-9]|5[1-9]\d|[6-9]\d{2})$","管内旅游列车"
|
||||||
|
"^[Ss][1-9]\d{0,3}$","市郊旅客列车"
|
||||||
|
"^[Ll](6([0-8]\d{2}|9[0-8]\d|99[0-8])|[1-5]\d{3}|[1-9]\d{0,2})$","直通临时旅客列车"
|
||||||
|
"^[Ll]([7-9]\d{3})$","管内临时旅客列车"
|
||||||
|
"^[Xx](19[0-8]|1[0-8]\d|[1-9]\d?)$","特快货物班列"
|
||||||
|
"^[Xx](39[0-8]|3[0-8]\d|2[1-9]\d|20[1-9])$","快速货物班列"
|
||||||
|
"^[Xx]2(40[1-9]|4[1-9]\d|[5-9]\d{2})$","直通货物快运列车"
|
||||||
|
"^[Xx]([4-9]\d{2}|4[1-9]\d|40[1-9])$","管内货物快运列车"
|
||||||
|
"^[Xx]8\d{3}$","中欧中亚集装箱班列"
|
||||||
|
"^[Xx]9([0-4]\d{2}|500)$","中亚集装箱班列"
|
||||||
|
"^[Xx]9(50[1-9]|5[1-9]\d|[6-9]\d{2})$","水铁联运班列"
|
||||||
|
"^[Xx][1-4]\d{4}$","加挂零散快运车辆货物列车"
|
||||||
|
"^1(000[1-9]|00[1-9]\d|0[1-9]\d{2}|[1-9]\d{3})$","技术直达列车"
|
||||||
|
"^2\d{4}$","直通货物列车"
|
||||||
|
"^3\d{4}$","区段摘挂列车"
|
||||||
|
"^4([0-3]\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$","摘挂列车"
|
||||||
|
"^4(500[1-9]|50[1-9]\d|5[1-9]\d{2}|[6-9]\d{3})$","小运转列车"
|
||||||
|
"^6\d{4}$","自备列车"
|
||||||
|
"^70\d{3}$","超限货物列车"
|
||||||
|
"^7([1-6]\d{3}|7([0-8]\d{2}|9[0-8]\d|99[0-8]))$","重载货物列车"
|
||||||
|
"^78\d{3}$","保温列车"
|
||||||
|
"^8(0\d{3}|1([0-8]\d{2}|9[0-8]\d|99[0-8]))$","普快货物班列"
|
||||||
|
"^8(200[1-9]|20[1-9]\d|2[1-9]\d{2}|[34]\d{3})$","煤炭直达列车"
|
||||||
|
"^85\d{3}$","石油直达列车"
|
||||||
|
"^86\d{3}$","始发直达列车"
|
||||||
|
"^87\d{3}$","空车直达列车"
|
||||||
|
"^(90\d{3}|91([0-8]\d{2}|9[0-8]\d|99[0-8]))$","军用列车"
|
||||||
|
"^50\d{3}$","客车单机"
|
||||||
|
"^51\d{3}$","货车单机"
|
||||||
|
"^52\d{3}$","小运转单机"
|
||||||
|
"^5(3\d{3}|4([0-8]\d{2}|9[0-8]\d|99[0-8]))$","补机列车"
|
||||||
|
"^55(300|[0-2]\d{2})$","普通客货试运转列车"
|
||||||
|
"^55(500|30[1-9]|3[1-9]\d|4\d{2})$","高速动车组试运转列车"
|
||||||
|
"^55(50[1-9]|5[1-9]\d|[6-9]\d{2})$","普通动车组试运转列车"
|
||||||
|
"^56\d{3}$","轻油动车与轨道车"
|
||||||
|
"^57\d{3}$","路用列车"
|
||||||
|
"^58(10[1-9]|1[1-9]\d|[2-8]\d{2}|9([0-8]\d|9[0-8]))$","救援列车"
|
||||||
|
"^DJ(400|[1-3]\d{2}|[1-9]\d?)$","动车组检测列车300直通"
|
||||||
|
"^DJ([4-9]\d{2}|40[1-9]|4[1-9]\d)$","动车组检测列车300管内"
|
||||||
|
"^DJ1(400|[0-3]\d{2})$","动车组检测列车250直通"
|
||||||
|
"^DJ1(40[1-9]|4[1-9]\d|[5-9]\d{2})$","动车组检测列车250管内"
|
||||||
|
"^DJ[56]\d{3}$","直通动车组确认列车"
|
||||||
|
"^DJ[78]\d{3}$","管内动车组确认列车"
|
||||||
|
"^[Ff][GDCZTKgdcztk]?\d{1,4}$","因故折返旅客列车"
|
||||||
|
"^0[GDCZTKgdcztk]\d{1,4}$","回送图定客车底"
|
||||||
|
"^00(100|[1-9]\d?)$","有火回送动车组车底"
|
||||||
|
"^00(10[1-9]|1[1-9]\d|2([0-8]\d|9[0-8]))$","无火回送动车组车底"
|
||||||
|
"^00(30[1-9]|3[1-9]\d|4([0-8]\d|9[0-8]))$","无火回送普速客车底"
|
||||||
|
@@ -534,7 +534,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
String(it, StandardCharsets.UTF_8)
|
String(it, StandardCharsets.UTF_8)
|
||||||
} ?: return
|
} ?: return
|
||||||
|
|
||||||
Log.d(TAG, "Received data len=${newData.length} preview=${newData.take(50)}")
|
Log.d(TAG, "Received data len=${newData.length} preview=${newData}")
|
||||||
|
|
||||||
|
|
||||||
dataBuffer.append(newData)
|
dataBuffer.append(newData)
|
||||||
@@ -640,7 +640,7 @@ class BLEClient(private val context: Context) : BluetoothGattCallback() {
|
|||||||
private fun processJsonString(jsonStr: String): Boolean {
|
private fun processJsonString(jsonStr: String): Boolean {
|
||||||
try {
|
try {
|
||||||
val jsonObject = JSONObject(jsonStr)
|
val jsonObject = JSONObject(jsonStr)
|
||||||
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr.take(50)}")
|
Log.d(TAG, "Parsed JSON len=${jsonStr.length} preview=${jsonStr}")
|
||||||
|
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
|
|||||||
@@ -62,9 +62,12 @@ import org.noxylva.lbjconsole.ui.screens.HistoryScreen
|
|||||||
|
|
||||||
import org.noxylva.lbjconsole.ui.screens.MapScreen
|
import org.noxylva.lbjconsole.ui.screens.MapScreen
|
||||||
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
|
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
|
||||||
|
import org.noxylva.lbjconsole.ui.screens.CardMapView
|
||||||
|
|
||||||
import org.noxylva.lbjconsole.ui.theme.LBJConsoleTheme
|
import org.noxylva.lbjconsole.ui.theme.LBJConsoleTheme
|
||||||
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
||||||
|
import org.noxylva.lbjconsole.util.TrainTypeUtil
|
||||||
|
import org.noxylva.lbjconsole.database.AppSettingsRepository
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import android.bluetooth.le.ScanCallback
|
import android.bluetooth.le.ScanCallback
|
||||||
@@ -75,7 +78,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
private val bleClient by lazy { BLEClient(this) }
|
private val bleClient by lazy { BLEClient(this) }
|
||||||
private val trainRecordManager by lazy { TrainRecordManager(this) }
|
private val trainRecordManager by lazy { TrainRecordManager(this) }
|
||||||
private val locoInfoUtil by lazy { LocoInfoUtil(this) }
|
private val locoInfoUtil by lazy { LocoInfoUtil(this) }
|
||||||
|
private val trainTypeUtil by lazy { TrainTypeUtil(this) }
|
||||||
private val notificationService by lazy { NotificationService(this) }
|
private val notificationService by lazy { NotificationService(this) }
|
||||||
|
private val appSettingsRepository by lazy { AppSettingsRepository(this) }
|
||||||
|
|
||||||
|
|
||||||
private var deviceStatus by mutableStateOf("未连接")
|
private var deviceStatus by mutableStateOf("未连接")
|
||||||
@@ -104,6 +109,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
private var historyExpandedStates by mutableStateOf<Map<String, Boolean>>(emptyMap())
|
private var historyExpandedStates by mutableStateOf<Map<String, Boolean>>(emptyMap())
|
||||||
private var historyScrollPosition by mutableStateOf(0)
|
private var historyScrollPosition by mutableStateOf(0)
|
||||||
private var historyScrollOffset by mutableStateOf(0)
|
private var historyScrollOffset by mutableStateOf(0)
|
||||||
|
private var historyCardMapStates by mutableStateOf<Map<String, CardMapView>>(emptyMap())
|
||||||
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
||||||
private var mapZoomLevel by mutableStateOf(10.0)
|
private var mapZoomLevel by mutableStateOf(10.0)
|
||||||
private var mapRailwayLayerVisible by mutableStateOf(true)
|
private var mapRailwayLayerVisible by mutableStateOf(true)
|
||||||
@@ -120,9 +126,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
private var showDisconnectButton by mutableStateOf(false)
|
private var showDisconnectButton by mutableStateOf(false)
|
||||||
private var autoConnectEnabled by mutableStateOf(true)
|
private var autoConnectEnabled by mutableStateOf(true)
|
||||||
|
|
||||||
|
|
||||||
private val settingsPrefs by lazy { getSharedPreferences("app_settings", Context.MODE_PRIVATE) }
|
|
||||||
|
|
||||||
private fun getAppVersion(): String {
|
private fun getAppVersion(): String {
|
||||||
return try {
|
return try {
|
||||||
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
val packageInfo = packageManager.getPackageInfo(packageName, 0)
|
||||||
@@ -246,9 +249,13 @@ class MainActivity : ComponentActivity() {
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Load locomotive data failed", e)
|
Log.e(TAG, "Load locomotive data failed", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
val osmCacheDir = File(cacheDir, "osm").apply { mkdirs() }
|
val osmCacheDir = File(cacheDir, "osm").apply { mkdirs() }
|
||||||
@@ -274,8 +281,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
saveSettings()
|
saveSettings()
|
||||||
|
|
||||||
if (SettingsActivity.isBackgroundServiceEnabled(this)) {
|
lifecycleScope.launch {
|
||||||
BackgroundService.startService(this)
|
if (SettingsActivity.isBackgroundServiceEnabled(this@MainActivity)) {
|
||||||
|
BackgroundService.startService(this@MainActivity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
@@ -294,6 +303,9 @@ class MainActivity : ComponentActivity() {
|
|||||||
isScanning = isScanning,
|
isScanning = isScanning,
|
||||||
currentTab = currentTab,
|
currentTab = currentTab,
|
||||||
onTabChange = { tab ->
|
onTabChange = { tab ->
|
||||||
|
if (currentTab == 2 && tab != 2) {
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
currentTab = tab
|
currentTab = tab
|
||||||
saveSettings()
|
saveSettings()
|
||||||
},
|
},
|
||||||
@@ -355,12 +367,14 @@ class MainActivity : ComponentActivity() {
|
|||||||
historyEditMode = historyEditMode,
|
historyEditMode = historyEditMode,
|
||||||
historySelectedRecords = historySelectedRecords,
|
historySelectedRecords = historySelectedRecords,
|
||||||
historyExpandedStates = historyExpandedStates,
|
historyExpandedStates = historyExpandedStates,
|
||||||
|
historyMapViewStates = historyCardMapStates,
|
||||||
historyScrollPosition = historyScrollPosition,
|
historyScrollPosition = historyScrollPosition,
|
||||||
historyScrollOffset = historyScrollOffset,
|
historyScrollOffset = historyScrollOffset,
|
||||||
onHistoryStateChange = { editMode, selectedRecords, expandedStates, scrollPosition, scrollOffset ->
|
onHistoryStateChange = { editMode, selectedRecords, expandedStates, mapStates, scrollPosition, scrollOffset ->
|
||||||
historyEditMode = editMode
|
historyEditMode = editMode
|
||||||
historySelectedRecords = selectedRecords
|
historySelectedRecords = selectedRecords
|
||||||
historyExpandedStates = expandedStates
|
historyExpandedStates = expandedStates
|
||||||
|
historyCardMapStates = mapStates
|
||||||
historyScrollPosition = scrollPosition
|
historyScrollPosition = scrollPosition
|
||||||
historyScrollOffset = scrollOffset
|
historyScrollOffset = scrollOffset
|
||||||
saveSettings()
|
saveSettings()
|
||||||
@@ -369,6 +383,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
settingsScrollPosition = settingsScrollPosition,
|
settingsScrollPosition = settingsScrollPosition,
|
||||||
onSettingsScrollPositionChange = { position ->
|
onSettingsScrollPositionChange = { position ->
|
||||||
|
android.util.Log.d(TAG, "Settings scroll position changed: $position")
|
||||||
settingsScrollPosition = position
|
settingsScrollPosition = position
|
||||||
saveSettings()
|
saveSettings()
|
||||||
},
|
},
|
||||||
@@ -427,6 +442,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
},
|
},
|
||||||
appVersion = getAppVersion(),
|
appVersion = getAppVersion(),
|
||||||
locoInfoUtil = locoInfoUtil,
|
locoInfoUtil = locoInfoUtil,
|
||||||
|
trainTypeUtil = trainTypeUtil,
|
||||||
onOpenSettings = {
|
onOpenSettings = {
|
||||||
val intent = Intent(this@MainActivity, SettingsActivity::class.java)
|
val intent = Intent(this@MainActivity, SettingsActivity::class.java)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
@@ -741,89 +757,101 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
|
|
||||||
private fun loadSettings() {
|
private fun loadSettings() {
|
||||||
settingsDeviceName = settingsPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver"
|
lifecycleScope.launch {
|
||||||
targetDeviceName = settingsDeviceName
|
try {
|
||||||
|
val settings = appSettingsRepository.getSettings()
|
||||||
|
|
||||||
|
settingsDeviceName = settings.deviceName
|
||||||
|
targetDeviceName = settingsDeviceName
|
||||||
|
currentTab = settings.currentTab
|
||||||
|
historyEditMode = settings.historyEditMode
|
||||||
|
|
||||||
currentTab = settingsPrefs.getInt("current_tab", 0)
|
historySelectedRecords = if (settings.historySelectedRecords.isEmpty()) {
|
||||||
historyEditMode = settingsPrefs.getBoolean("history_edit_mode", false)
|
emptySet()
|
||||||
|
} else {
|
||||||
|
settings.historySelectedRecords.split(",").toSet()
|
||||||
|
}
|
||||||
|
|
||||||
val selectedRecordsStr = settingsPrefs.getString("history_selected_records", "")
|
historyExpandedStates = if (settings.historyExpandedStates.isEmpty()) {
|
||||||
historySelectedRecords = if (selectedRecordsStr.isNullOrEmpty()) {
|
emptyMap()
|
||||||
emptySet()
|
} else {
|
||||||
} else {
|
settings.historyExpandedStates.split(";").mapNotNull { pair ->
|
||||||
selectedRecordsStr.split(",").toSet()
|
val parts = pair.split(":")
|
||||||
|
if (parts.size == 2) parts[0] to (parts[1] == "true") else null
|
||||||
|
}.toMap()
|
||||||
|
}
|
||||||
|
|
||||||
|
historyScrollPosition = settings.historyScrollPosition
|
||||||
|
historyScrollOffset = settings.historyScrollOffset
|
||||||
|
settingsScrollPosition = settings.settingsScrollPosition
|
||||||
|
android.util.Log.d(TAG, "Loaded settings scroll position: $settingsScrollPosition")
|
||||||
|
|
||||||
|
mapCenterPosition = if (settings.mapCenterLat != null && settings.mapCenterLon != null) {
|
||||||
|
settings.mapCenterLat.toDouble() to settings.mapCenterLon.toDouble()
|
||||||
|
} else null
|
||||||
|
|
||||||
|
mapZoomLevel = settings.mapZoomLevel.toDouble()
|
||||||
|
mapRailwayLayerVisible = settings.mapRailwayLayerVisible
|
||||||
|
|
||||||
|
mergeSettings = trainRecordManager.mergeSettings
|
||||||
|
|
||||||
|
specifiedDeviceAddress = settings.specifiedDeviceAddress
|
||||||
|
|
||||||
|
searchOrderList = if (settings.searchOrderList.isEmpty()) {
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
settings.searchOrderList.split(",").filter { it.isNotBlank() }
|
||||||
|
}
|
||||||
|
|
||||||
|
autoConnectEnabled = settings.autoConnectEnabled
|
||||||
|
|
||||||
|
bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress)
|
||||||
|
|
||||||
|
Log.d(TAG, "Loaded settings from Room: deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size} autoConnect=${autoConnectEnabled}")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error loading settings from Room", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val expandedStatesStr = settingsPrefs.getString("history_expanded_states", "")
|
|
||||||
historyExpandedStates = if (expandedStatesStr.isNullOrEmpty()) {
|
|
||||||
emptyMap()
|
|
||||||
} else {
|
|
||||||
expandedStatesStr.split(";").mapNotNull { pair ->
|
|
||||||
val parts = pair.split(":")
|
|
||||||
if (parts.size == 2) parts[0] to (parts[1] == "true") else null
|
|
||||||
}.toMap()
|
|
||||||
}
|
|
||||||
|
|
||||||
historyScrollPosition = settingsPrefs.getInt("history_scroll_position", 0)
|
|
||||||
historyScrollOffset = settingsPrefs.getInt("history_scroll_offset", 0)
|
|
||||||
settingsScrollPosition = settingsPrefs.getInt("settings_scroll_position", 0)
|
|
||||||
|
|
||||||
val centerLat = settingsPrefs.getFloat("map_center_lat", Float.NaN)
|
|
||||||
val centerLon = settingsPrefs.getFloat("map_center_lon", Float.NaN)
|
|
||||||
mapCenterPosition = if (!centerLat.isNaN() && !centerLon.isNaN()) {
|
|
||||||
centerLat.toDouble() to centerLon.toDouble()
|
|
||||||
} else null
|
|
||||||
|
|
||||||
mapZoomLevel = settingsPrefs.getFloat("map_zoom_level", 10.0f).toDouble()
|
|
||||||
mapRailwayLayerVisible = settingsPrefs.getBoolean("map_railway_visible", true)
|
|
||||||
|
|
||||||
mergeSettings = trainRecordManager.mergeSettings
|
|
||||||
|
|
||||||
specifiedDeviceAddress = settingsPrefs.getString("specified_device_address", null)
|
|
||||||
|
|
||||||
val searchOrderStr = settingsPrefs.getString("search_order_list", "")
|
|
||||||
searchOrderList = if (searchOrderStr.isNullOrEmpty()) {
|
|
||||||
emptyList()
|
|
||||||
} else {
|
|
||||||
searchOrderStr.split(",").filter { it.isNotBlank() }
|
|
||||||
}
|
|
||||||
|
|
||||||
autoConnectEnabled = settingsPrefs.getBoolean("auto_connect_enabled", true)
|
|
||||||
|
|
||||||
bleClient.setSpecifiedDeviceAddress(specifiedDeviceAddress)
|
|
||||||
|
|
||||||
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab} specifiedDevice=${specifiedDeviceAddress} searchOrder=${searchOrderList.size} autoConnect=${autoConnectEnabled}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun saveSettings() {
|
private fun saveSettings() {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
val editor = settingsPrefs.edit()
|
try {
|
||||||
.putString("device_name", settingsDeviceName)
|
val currentSettings = appSettingsRepository.getSettings()
|
||||||
.putInt("current_tab", currentTab)
|
val updatedSettings = currentSettings.copy(
|
||||||
.putBoolean("history_edit_mode", historyEditMode)
|
deviceName = settingsDeviceName,
|
||||||
.putString("history_selected_records", historySelectedRecords.joinToString(","))
|
currentTab = currentTab,
|
||||||
.putString("history_expanded_states", historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"))
|
historyEditMode = historyEditMode,
|
||||||
.putInt("history_scroll_position", historyScrollPosition)
|
historySelectedRecords = historySelectedRecords.joinToString(","),
|
||||||
.putInt("history_scroll_offset", historyScrollOffset)
|
historyExpandedStates = historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"),
|
||||||
.putInt("settings_scroll_position", settingsScrollPosition)
|
historyScrollPosition = historyScrollPosition,
|
||||||
.putFloat("map_zoom_level", mapZoomLevel.toFloat())
|
historyScrollOffset = historyScrollOffset,
|
||||||
.putBoolean("map_railway_visible", mapRailwayLayerVisible)
|
settingsScrollPosition = settingsScrollPosition,
|
||||||
.putString("specified_device_address", specifiedDeviceAddress)
|
mapCenterLat = mapCenterPosition?.first?.toFloat(),
|
||||||
.putString("search_order_list", searchOrderList.joinToString(","))
|
mapCenterLon = mapCenterPosition?.second?.toFloat(),
|
||||||
.putBoolean("auto_connect_enabled", autoConnectEnabled)
|
mapZoomLevel = mapZoomLevel.toFloat(),
|
||||||
|
mapRailwayLayerVisible = mapRailwayLayerVisible,
|
||||||
|
specifiedDeviceAddress = specifiedDeviceAddress,
|
||||||
|
searchOrderList = searchOrderList.joinToString(","),
|
||||||
|
autoConnectEnabled = autoConnectEnabled
|
||||||
|
)
|
||||||
|
|
||||||
mapCenterPosition?.let { (lat, lon) ->
|
appSettingsRepository.saveSettings(updatedSettings)
|
||||||
editor.putFloat("map_center_lat", lat.toFloat())
|
Log.d(TAG, "Saved settings to Room: deviceName=${settingsDeviceName} tab=${currentTab} settingsScrollPosition=${settingsScrollPosition} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
|
||||||
editor.putFloat("map_center_lon", lon.toFloat())
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error saving settings to Room", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
editor.apply()
|
|
||||||
Log.d(TAG, "Saved settings deviceName=${settingsDeviceName} tab=${currentTab} mapCenter=${mapCenterPosition} zoom=${mapZoomLevel}")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
Log.d(TAG, "onNewIntent called")
|
||||||
|
currentTab = 0
|
||||||
|
forceUiRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
Log.d(TAG, "App resumed")
|
Log.d(TAG, "App resumed")
|
||||||
@@ -909,14 +937,16 @@ fun MainContent(
|
|||||||
|
|
||||||
|
|
||||||
locoInfoUtil: LocoInfoUtil,
|
locoInfoUtil: LocoInfoUtil,
|
||||||
|
trainTypeUtil: TrainTypeUtil,
|
||||||
|
|
||||||
|
|
||||||
historyEditMode: Boolean,
|
historyEditMode: Boolean,
|
||||||
historySelectedRecords: Set<String>,
|
historySelectedRecords: Set<String>,
|
||||||
historyExpandedStates: Map<String, Boolean>,
|
historyExpandedStates: Map<String, Boolean>,
|
||||||
|
historyMapViewStates: Map<String, CardMapView>,
|
||||||
historyScrollPosition: Int,
|
historyScrollPosition: Int,
|
||||||
historyScrollOffset: Int,
|
historyScrollOffset: Int,
|
||||||
onHistoryStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit,
|
onHistoryStateChange: (Boolean, Set<String>, Map<String, Boolean>, Map<String, CardMapView>, Int, Int) -> Unit,
|
||||||
|
|
||||||
|
|
||||||
settingsScrollPosition: Int,
|
settingsScrollPosition: Int,
|
||||||
@@ -1022,7 +1052,7 @@ fun MainContent(
|
|||||||
},
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyMapViewStates, historyScrollPosition, historyScrollOffset)
|
||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
@@ -1068,7 +1098,7 @@ fun MainContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDeleteRecords(recordsToDelete.toList())
|
onDeleteRecords(recordsToDelete.toList())
|
||||||
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyMapViewStates, historyScrollPosition, historyScrollOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -1124,6 +1154,7 @@ fun MainContent(
|
|||||||
lastUpdateTime = lastUpdateTime,
|
lastUpdateTime = lastUpdateTime,
|
||||||
temporaryStatusMessage = temporaryStatusMessage,
|
temporaryStatusMessage = temporaryStatusMessage,
|
||||||
locoInfoUtil = locoInfoUtil,
|
locoInfoUtil = locoInfoUtil,
|
||||||
|
trainTypeUtil = trainTypeUtil,
|
||||||
mergeSettings = mergeSettings,
|
mergeSettings = mergeSettings,
|
||||||
onClearRecords = onClearRecords,
|
onClearRecords = onClearRecords,
|
||||||
onRecordClick = onRecordClick,
|
onRecordClick = onRecordClick,
|
||||||
@@ -1132,6 +1163,7 @@ fun MainContent(
|
|||||||
editMode = historyEditMode,
|
editMode = historyEditMode,
|
||||||
selectedRecords = historySelectedRecords,
|
selectedRecords = historySelectedRecords,
|
||||||
expandedStates = historyExpandedStates,
|
expandedStates = historyExpandedStates,
|
||||||
|
mapViewStates = historyMapViewStates,
|
||||||
scrollPosition = historyScrollPosition,
|
scrollPosition = historyScrollPosition,
|
||||||
scrollOffset = historyScrollOffset,
|
scrollOffset = historyScrollOffset,
|
||||||
onStateChange = onHistoryStateChange
|
onStateChange = onHistoryStateChange
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import android.app.NotificationManager
|
|||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.noxylva.lbjconsole.database.AppSettingsRepository
|
||||||
|
import org.noxylva.lbjconsole.database.TrainDatabase
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@@ -27,7 +29,7 @@ class NotificationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val notificationManager = NotificationManagerCompat.from(context)
|
private val notificationManager = NotificationManagerCompat.from(context)
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
private val appSettingsRepository = AppSettingsRepository(context)
|
||||||
private var notificationIdCounter = NOTIFICATION_ID_BASE
|
private var notificationIdCounter = NOTIFICATION_ID_BASE
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -53,11 +55,15 @@ class NotificationService(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun isNotificationEnabled(): Boolean {
|
fun isNotificationEnabled(): Boolean {
|
||||||
return prefs.getBoolean(KEY_ENABLED, false)
|
return runBlocking {
|
||||||
|
appSettingsRepository.getSettings().notificationEnabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setNotificationEnabled(enabled: Boolean) {
|
fun setNotificationEnabled(enabled: Boolean) {
|
||||||
prefs.edit().putBoolean(KEY_ENABLED, enabled).apply()
|
runBlocking {
|
||||||
|
appSettingsRepository.updateNotificationEnabled(enabled)
|
||||||
|
}
|
||||||
Log.d(TAG, "Notification enabled set to: $enabled")
|
Log.d(TAG, "Notification enabled set to: $enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
package org.noxylva.lbjconsole
|
package org.noxylva.lbjconsole
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.widget.Switch
|
import android.widget.Switch
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.noxylva.lbjconsole.database.AppSettingsRepository
|
||||||
|
|
||||||
class SettingsActivity : AppCompatActivity() {
|
class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val PREFS_NAME = "lbj_console_settings"
|
suspend fun isBackgroundServiceEnabled(context: Context): Boolean {
|
||||||
private const val KEY_BACKGROUND_SERVICE = "background_service_enabled"
|
val repository = AppSettingsRepository(context)
|
||||||
|
return repository.getSettings().backgroundServiceEnabled
|
||||||
fun isBackgroundServiceEnabled(context: Context): Boolean {
|
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
|
||||||
return prefs.getBoolean(KEY_BACKGROUND_SERVICE, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setBackgroundServiceEnabled(context: Context, enabled: Boolean) {
|
suspend fun setBackgroundServiceEnabled(context: Context, enabled: Boolean) {
|
||||||
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
val repository = AppSettingsRepository(context)
|
||||||
prefs.edit().putBoolean(KEY_BACKGROUND_SERVICE, enabled).apply()
|
repository.updateBackgroundServiceEnabled(enabled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var backgroundServiceSwitch: Switch
|
private lateinit var backgroundServiceSwitch: Switch
|
||||||
private lateinit var prefs: SharedPreferences
|
private lateinit var appSettingsRepository: AppSettingsRepository
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -33,7 +32,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.title = "Settings"
|
supportActionBar?.title = "Settings"
|
||||||
|
|
||||||
prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
appSettingsRepository = AppSettingsRepository(this)
|
||||||
|
|
||||||
initViews()
|
initViews()
|
||||||
setupListeners()
|
setupListeners()
|
||||||
@@ -41,12 +40,16 @@ class SettingsActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private fun initViews() {
|
private fun initViews() {
|
||||||
backgroundServiceSwitch = findViewById(R.id.switch_background_service)
|
backgroundServiceSwitch = findViewById(R.id.switch_background_service)
|
||||||
backgroundServiceSwitch.isChecked = isBackgroundServiceEnabled(this)
|
lifecycleScope.launch {
|
||||||
|
backgroundServiceSwitch.isChecked = isBackgroundServiceEnabled(this@SettingsActivity)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupListeners() {
|
private fun setupListeners() {
|
||||||
backgroundServiceSwitch.setOnCheckedChangeListener { _, isChecked ->
|
backgroundServiceSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||||
setBackgroundServiceEnabled(this, isChecked)
|
lifecycleScope.launch {
|
||||||
|
setBackgroundServiceEnabled(this@SettingsActivity, isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
if (isChecked) {
|
if (isChecked) {
|
||||||
BackgroundService.startService(this)
|
BackgroundService.startService(this)
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package org.noxylva.lbjconsole.database
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface AppSettingsDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app_settings WHERE id = 1")
|
||||||
|
suspend fun getSettings(): AppSettingsEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM app_settings WHERE id = 1")
|
||||||
|
fun getSettingsFlow(): Flow<AppSettingsEntity?>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertSettings(settings: AppSettingsEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateSettings(settings: AppSettingsEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM app_settings")
|
||||||
|
suspend fun deleteAllSettings()
|
||||||
|
|
||||||
|
@Query("UPDATE app_settings SET notificationEnabled = :enabled WHERE id = 1")
|
||||||
|
suspend fun updateNotificationEnabled(enabled: Boolean)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
suspend fun saveSettings(settings: AppSettingsEntity) {
|
||||||
|
insertSettings(settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package org.noxylva.lbjconsole.database
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "app_settings")
|
||||||
|
data class AppSettingsEntity(
|
||||||
|
@PrimaryKey val id: Int = 1,
|
||||||
|
val deviceName: String = "LBJReceiver",
|
||||||
|
val currentTab: Int = 0,
|
||||||
|
val historyEditMode: Boolean = false,
|
||||||
|
val historySelectedRecords: String = "",
|
||||||
|
val historyExpandedStates: String = "",
|
||||||
|
val historyScrollPosition: Int = 0,
|
||||||
|
val historyScrollOffset: Int = 0,
|
||||||
|
val settingsScrollPosition: Int = 0,
|
||||||
|
val mapCenterLat: Float? = null,
|
||||||
|
val mapCenterLon: Float? = null,
|
||||||
|
val mapZoomLevel: Float = 10.0f,
|
||||||
|
val mapRailwayLayerVisible: Boolean = true,
|
||||||
|
val specifiedDeviceAddress: String? = null,
|
||||||
|
val searchOrderList: String = "",
|
||||||
|
val autoConnectEnabled: Boolean = true,
|
||||||
|
val backgroundServiceEnabled: Boolean = false,
|
||||||
|
val notificationEnabled: Boolean = false
|
||||||
|
)
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package org.noxylva.lbjconsole.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
||||||
|
class AppSettingsRepository(private val context: Context) {
|
||||||
|
private val dao = TrainDatabase.getDatabase(context).appSettingsDao()
|
||||||
|
private val sharedPrefs: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
suspend fun getSettings(): AppSettingsEntity {
|
||||||
|
var settings = dao.getSettings()
|
||||||
|
|
||||||
|
if (settings == null) {
|
||||||
|
settings = migrateFromSharedPreferences()
|
||||||
|
dao.saveSettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSettingsFlow(): Flow<AppSettingsEntity?> {
|
||||||
|
return dao.getSettingsFlow()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun saveSettings(settings: AppSettingsEntity) {
|
||||||
|
dao.saveSettings(settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateDeviceName(deviceName: String) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(deviceName = deviceName))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateCurrentTab(tab: Int) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(currentTab = tab))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateHistoryEditMode(editMode: Boolean) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(historyEditMode = editMode))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateHistorySelectedRecords(selectedRecords: String) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(historySelectedRecords = selectedRecords))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateHistoryExpandedStates(expandedStates: String) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(historyExpandedStates = expandedStates))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateHistoryScrollPosition(position: Int, offset: Int = 0) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(historyScrollPosition = position, historyScrollOffset = offset))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateSettingsScrollPosition(position: Int) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(settingsScrollPosition = position))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateMapSettings(centerLat: Float?, centerLon: Float?, zoomLevel: Float, railwayLayerVisible: Boolean) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(
|
||||||
|
mapCenterLat = centerLat,
|
||||||
|
mapCenterLon = centerLon,
|
||||||
|
mapZoomLevel = zoomLevel,
|
||||||
|
mapRailwayLayerVisible = railwayLayerVisible
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateSpecifiedDeviceAddress(address: String?) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(specifiedDeviceAddress = address))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateSearchOrderList(orderList: String) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(searchOrderList = orderList))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateAutoConnectEnabled(enabled: Boolean) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(autoConnectEnabled = enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateBackgroundServiceEnabled(enabled: Boolean) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(backgroundServiceEnabled = enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateNotificationEnabled(enabled: Boolean) {
|
||||||
|
val current = getSettings()
|
||||||
|
saveSettings(current.copy(notificationEnabled = enabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateFromSharedPreferences(): AppSettingsEntity {
|
||||||
|
return AppSettingsEntity(
|
||||||
|
deviceName = sharedPrefs.getString("device_name", "LBJReceiver") ?: "LBJReceiver",
|
||||||
|
currentTab = sharedPrefs.getInt("current_tab", 0),
|
||||||
|
historyEditMode = sharedPrefs.getBoolean("history_edit_mode", false),
|
||||||
|
historySelectedRecords = sharedPrefs.getString("history_selected_records", "") ?: "",
|
||||||
|
historyExpandedStates = sharedPrefs.getString("history_expanded_states", "") ?: "",
|
||||||
|
historyScrollPosition = sharedPrefs.getInt("history_scroll_position", 0),
|
||||||
|
historyScrollOffset = sharedPrefs.getInt("history_scroll_offset", 0),
|
||||||
|
settingsScrollPosition = sharedPrefs.getInt("settings_scroll_position", 0),
|
||||||
|
mapCenterLat = if (sharedPrefs.contains("map_center_lat")) sharedPrefs.getFloat("map_center_lat", 0f) else null,
|
||||||
|
mapCenterLon = if (sharedPrefs.contains("map_center_lon")) sharedPrefs.getFloat("map_center_lon", 0f) else null,
|
||||||
|
mapZoomLevel = sharedPrefs.getFloat("map_zoom_level", 10.0f),
|
||||||
|
mapRailwayLayerVisible = sharedPrefs.getBoolean("map_railway_layer_visible", true),
|
||||||
|
specifiedDeviceAddress = sharedPrefs.getString("specified_device_address", null),
|
||||||
|
searchOrderList = sharedPrefs.getString("search_order_list", "") ?: "",
|
||||||
|
autoConnectEnabled = sharedPrefs.getBoolean("auto_connect_enabled", true),
|
||||||
|
backgroundServiceEnabled = sharedPrefs.getBoolean("background_service_enabled", false),
|
||||||
|
notificationEnabled = context.getSharedPreferences("notification_settings", Context.MODE_PRIVATE)
|
||||||
|
.getBoolean("notifications_enabled", false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun clearSharedPreferences() {
|
||||||
|
sharedPrefs.edit().clear().apply()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package org.noxylva.lbjconsole.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
||||||
|
version = 3,
|
||||||
|
exportSchema = false
|
||||||
|
)
|
||||||
|
abstract class TrainDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun trainRecordDao(): TrainRecordDao
|
||||||
|
abstract fun appSettingsDao(): AppSettingsDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: TrainDatabase? = null
|
||||||
|
|
||||||
|
private val MIGRATION_1_2 = object : Migration(1, 2) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("""
|
||||||
|
CREATE TABLE IF NOT EXISTS `app_settings` (
|
||||||
|
`id` INTEGER NOT NULL,
|
||||||
|
`deviceName` TEXT NOT NULL,
|
||||||
|
`currentTab` INTEGER NOT NULL,
|
||||||
|
`historyEditMode` INTEGER NOT NULL,
|
||||||
|
`historySelectedRecords` TEXT NOT NULL,
|
||||||
|
`historyExpandedStates` TEXT NOT NULL,
|
||||||
|
`historyScrollPosition` INTEGER NOT NULL,
|
||||||
|
`historyScrollOffset` INTEGER NOT NULL,
|
||||||
|
`settingsScrollPosition` INTEGER NOT NULL,
|
||||||
|
`mapCenterLat` REAL,
|
||||||
|
`mapCenterLon` REAL,
|
||||||
|
`mapZoomLevel` REAL NOT NULL,
|
||||||
|
`mapRailwayLayerVisible` INTEGER NOT NULL,
|
||||||
|
`specifiedDeviceAddress` TEXT,
|
||||||
|
`searchOrderList` TEXT NOT NULL,
|
||||||
|
`autoConnectEnabled` INTEGER NOT NULL,
|
||||||
|
`backgroundServiceEnabled` INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val MIGRATION_2_3 = object : Migration(2, 3) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
database.execSQL("ALTER TABLE app_settings ADD COLUMN notificationEnabled INTEGER NOT NULL DEFAULT 0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDatabase(context: Context): TrainDatabase {
|
||||||
|
return INSTANCE ?: synchronized(this) {
|
||||||
|
val instance = Room.databaseBuilder(
|
||||||
|
context.applicationContext,
|
||||||
|
TrainDatabase::class.java,
|
||||||
|
"train_database"
|
||||||
|
).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
|
||||||
|
INSTANCE = instance
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.noxylva.lbjconsole.database
|
||||||
|
|
||||||
|
import androidx.room.*
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface TrainRecordDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
|
||||||
|
suspend fun getAllRecords(): List<TrainRecordEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM train_records ORDER BY timestamp DESC")
|
||||||
|
fun getAllRecordsFlow(): Flow<List<TrainRecordEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM train_records WHERE uniqueId = :uniqueId")
|
||||||
|
suspend fun getRecordById(uniqueId: String): TrainRecordEntity?
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM train_records")
|
||||||
|
suspend fun getRecordCount(): Int
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertRecord(record: TrainRecordEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
suspend fun insertRecords(records: List<TrainRecordEntity>)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteRecord(record: TrainRecordEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteRecords(records: List<TrainRecordEntity>)
|
||||||
|
|
||||||
|
@Query("DELETE FROM train_records")
|
||||||
|
suspend fun deleteAllRecords()
|
||||||
|
|
||||||
|
@Query("DELETE FROM train_records WHERE uniqueId = :uniqueId")
|
||||||
|
suspend fun deleteRecordById(uniqueId: String)
|
||||||
|
|
||||||
|
@Query("DELETE FROM train_records WHERE uniqueId IN (:uniqueIds)")
|
||||||
|
suspend fun deleteRecordsByIds(uniqueIds: List<String>)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM train_records WHERE train LIKE '%' || :train || '%' AND route LIKE '%' || :route || '%' AND (:direction = '全部' OR (:direction = '上行' AND direction = 3) OR (:direction = '下行' AND direction = 1)) ORDER BY timestamp DESC")
|
||||||
|
suspend fun getFilteredRecords(train: String, route: String, direction: String): List<TrainRecordEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM train_records ORDER BY timestamp DESC LIMIT :limit")
|
||||||
|
suspend fun getLatestRecords(limit: Int): List<TrainRecordEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM train_records WHERE timestamp >= :fromTime ORDER BY timestamp DESC")
|
||||||
|
suspend fun getRecordsFromTime(fromTime: Long): List<TrainRecordEntity>
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package org.noxylva.lbjconsole.database
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import org.noxylva.lbjconsole.model.TrainRecord
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Entity(tableName = "train_records")
|
||||||
|
data class TrainRecordEntity(
|
||||||
|
@PrimaryKey val uniqueId: String,
|
||||||
|
val timestamp: Long,
|
||||||
|
val receivedTimestamp: Long,
|
||||||
|
val train: String,
|
||||||
|
val direction: Int,
|
||||||
|
val speed: String,
|
||||||
|
val position: String,
|
||||||
|
val time: String,
|
||||||
|
val loco: String,
|
||||||
|
val locoType: String,
|
||||||
|
val lbjClass: String,
|
||||||
|
val route: String,
|
||||||
|
val positionInfo: String,
|
||||||
|
val rssi: Double
|
||||||
|
) {
|
||||||
|
fun toTrainRecord(): TrainRecord {
|
||||||
|
val jsonData = JSONObject().apply {
|
||||||
|
put("uniqueId", uniqueId)
|
||||||
|
put("timestamp", timestamp)
|
||||||
|
put("receivedTimestamp", receivedTimestamp)
|
||||||
|
put("train", train)
|
||||||
|
put("dir", direction)
|
||||||
|
put("speed", speed)
|
||||||
|
put("pos", position)
|
||||||
|
put("time", time)
|
||||||
|
put("loco", loco)
|
||||||
|
put("loco_type", locoType)
|
||||||
|
put("lbj_class", lbjClass)
|
||||||
|
put("route", route)
|
||||||
|
put("position_info", positionInfo)
|
||||||
|
put("rssi", rssi)
|
||||||
|
}
|
||||||
|
return TrainRecord(jsonData)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromTrainRecord(record: TrainRecord): TrainRecordEntity {
|
||||||
|
return TrainRecordEntity(
|
||||||
|
uniqueId = record.uniqueId,
|
||||||
|
timestamp = record.timestamp.time,
|
||||||
|
receivedTimestamp = record.receivedTimestamp.time,
|
||||||
|
train = record.train,
|
||||||
|
direction = record.direction,
|
||||||
|
speed = record.speed,
|
||||||
|
position = record.position,
|
||||||
|
time = record.time,
|
||||||
|
loco = record.loco,
|
||||||
|
locoType = record.locoType,
|
||||||
|
lbjClass = record.lbjClass,
|
||||||
|
route = record.route,
|
||||||
|
positionInfo = record.positionInfo,
|
||||||
|
rssi = record.rssi
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ class TrainRecord(jsonData: JSONObject? = null) {
|
|||||||
|
|
||||||
_coordinates = null
|
_coordinates = null
|
||||||
|
|
||||||
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed")
|
Log.d(TAG, "Successfully parsed: train=$train, dir=$direction, speed=$speed, lbjClass='$lbjClass'")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "JSON parse error: ${e.message}", e)
|
Log.e(TAG, "JSON parse error: ${e.message}", e)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import android.util.Log
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import org.noxylva.lbjconsole.database.TrainDatabase
|
||||||
|
import org.noxylva.lbjconsole.database.TrainRecordEntity
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileWriter
|
import java.io.FileWriter
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
@@ -27,6 +29,8 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
|
private val trainRecords = CopyOnWriteArrayList<TrainRecord>()
|
||||||
private val recordCount = AtomicInteger(0)
|
private val recordCount = AtomicInteger(0)
|
||||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
private val database = TrainDatabase.getDatabase(context)
|
||||||
|
private val trainRecordDao = database.trainRecordDao()
|
||||||
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val ioScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
|
|
||||||
var mergeSettings = MergeSettings()
|
var mergeSettings = MergeSettings()
|
||||||
@@ -34,11 +38,36 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
ioScope.launch {
|
ioScope.launch {
|
||||||
|
migrateFromSharedPreferences()
|
||||||
loadRecords()
|
loadRecords()
|
||||||
loadMergeSettings()
|
loadMergeSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun migrateFromSharedPreferences() {
|
||||||
|
try {
|
||||||
|
val jsonStr = prefs.getString(KEY_RECORDS, null)
|
||||||
|
if (jsonStr != null && jsonStr != "[]") {
|
||||||
|
val jsonArray = JSONArray(jsonStr)
|
||||||
|
val records = mutableListOf<TrainRecordEntity>()
|
||||||
|
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val jsonObject = jsonArray.getJSONObject(i)
|
||||||
|
val trainRecord = TrainRecord(jsonObject)
|
||||||
|
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.isNotEmpty()) {
|
||||||
|
trainRecordDao.insertRecords(records)
|
||||||
|
prefs.edit().remove(KEY_RECORDS).apply()
|
||||||
|
Log.d(TAG, "Migrated ${records.size} records from SharedPreferences to Room database")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to migrate records: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private var filterTrain: String = ""
|
private var filterTrain: String = ""
|
||||||
private var filterRoute: String = ""
|
private var filterRoute: String = ""
|
||||||
@@ -52,11 +81,16 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
|
|
||||||
|
|
||||||
while (trainRecords.size > MAX_RECORDS) {
|
while (trainRecords.size > MAX_RECORDS) {
|
||||||
trainRecords.removeAt(trainRecords.size - 1)
|
val removedRecord = trainRecords.removeAt(trainRecords.size - 1)
|
||||||
|
ioScope.launch {
|
||||||
|
trainRecordDao.deleteRecordById(removedRecord.uniqueId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
recordCount.incrementAndGet()
|
recordCount.incrementAndGet()
|
||||||
saveRecords()
|
ioScope.launch {
|
||||||
|
trainRecordDao.insertRecord(TrainRecordEntity.fromTrainRecord(record))
|
||||||
|
}
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +110,16 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getFilteredRecordsFromDatabase(): List<TrainRecord> {
|
||||||
|
return try {
|
||||||
|
val entities = trainRecordDao.getFilteredRecords(filterTrain, filterRoute, filterDirection)
|
||||||
|
entities.map { it.toTrainRecord() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to get filtered records from database: ${e.message}")
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun matchFilter(record: TrainRecord): Boolean {
|
private fun matchFilter(record: TrainRecord): Boolean {
|
||||||
|
|
||||||
@@ -118,32 +162,56 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
suspend fun refreshRecordsFromDatabase() {
|
||||||
|
try {
|
||||||
|
val entities = trainRecordDao.getAllRecords()
|
||||||
|
trainRecords.clear()
|
||||||
|
entities.forEach { entity ->
|
||||||
|
trainRecords.add(entity.toTrainRecord())
|
||||||
|
}
|
||||||
|
recordCount.set(trainRecords.size)
|
||||||
|
Log.d(TAG, "Refreshed ${trainRecords.size} records from database")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to refresh records from database: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun clearRecords() {
|
fun clearRecords() {
|
||||||
trainRecords.clear()
|
trainRecords.clear()
|
||||||
recordCount.set(0)
|
recordCount.set(0)
|
||||||
saveRecords()
|
ioScope.launch {
|
||||||
|
trainRecordDao.deleteAllRecords()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRecord(record: TrainRecord): Boolean {
|
fun deleteRecord(record: TrainRecord): Boolean {
|
||||||
val result = trainRecords.remove(record)
|
val result = trainRecords.remove(record)
|
||||||
if (result) {
|
if (result) {
|
||||||
recordCount.decrementAndGet()
|
recordCount.decrementAndGet()
|
||||||
saveRecords()
|
ioScope.launch {
|
||||||
|
trainRecordDao.deleteRecordById(record.uniqueId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteRecords(records: List<TrainRecord>): Int {
|
fun deleteRecords(records: List<TrainRecord>): Int {
|
||||||
var deletedCount = 0
|
var deletedCount = 0
|
||||||
|
val idsToDelete = mutableListOf<String>()
|
||||||
|
|
||||||
records.forEach { record ->
|
records.forEach { record ->
|
||||||
if (trainRecords.remove(record)) {
|
if (trainRecords.remove(record)) {
|
||||||
deletedCount++
|
deletedCount++
|
||||||
|
idsToDelete.add(record.uniqueId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deletedCount > 0) {
|
if (deletedCount > 0) {
|
||||||
recordCount.addAndGet(-deletedCount)
|
recordCount.addAndGet(-deletedCount)
|
||||||
saveRecords()
|
ioScope.launch {
|
||||||
|
trainRecordDao.deleteRecordsByIds(idsToDelete)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return deletedCount
|
return deletedCount
|
||||||
}
|
}
|
||||||
@@ -151,12 +219,9 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
private fun saveRecords() {
|
private fun saveRecords() {
|
||||||
ioScope.launch {
|
ioScope.launch {
|
||||||
try {
|
try {
|
||||||
val jsonArray = JSONArray()
|
val entities = trainRecords.map { TrainRecordEntity.fromTrainRecord(it) }
|
||||||
for (record in trainRecords) {
|
trainRecordDao.insertRecords(entities)
|
||||||
jsonArray.put(record.toJSON())
|
Log.d(TAG, "Saved ${trainRecords.size} records to database")
|
||||||
}
|
|
||||||
prefs.edit().putString(KEY_RECORDS, jsonArray.toString()).apply()
|
|
||||||
Log.d(TAG, "Saved ${trainRecords.size} records")
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to save records: ${e.message}")
|
Log.e(TAG, "Failed to save records: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -164,19 +229,17 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun loadRecords() {
|
private suspend fun loadRecords() {
|
||||||
try {
|
try {
|
||||||
val jsonStr = prefs.getString(KEY_RECORDS, "[]")
|
val entities = trainRecordDao.getAllRecords()
|
||||||
val jsonArray = JSONArray(jsonStr)
|
|
||||||
trainRecords.clear()
|
trainRecords.clear()
|
||||||
|
|
||||||
for (i in 0 until jsonArray.length()) {
|
entities.forEach { entity ->
|
||||||
val jsonObject = jsonArray.getJSONObject(i)
|
trainRecords.add(entity.toTrainRecord())
|
||||||
trainRecords.add(TrainRecord(jsonObject))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
recordCount.set(trainRecords.size)
|
recordCount.set(trainRecords.size)
|
||||||
Log.d(TAG, "Loaded ${trainRecords.size} records")
|
Log.d(TAG, "Loaded ${trainRecords.size} records from database")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load records: ${e.message}")
|
Log.e(TAG, "Failed to load records: ${e.message}")
|
||||||
}
|
}
|
||||||
@@ -349,4 +412,41 @@ class TrainRecordManager(private val context: Context) {
|
|||||||
mergeSettings = MergeSettings()
|
mergeSettings = MergeSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun exportRecordsToJson(): JSONArray {
|
||||||
|
val jsonArray = JSONArray()
|
||||||
|
try {
|
||||||
|
val entities = trainRecordDao.getAllRecords()
|
||||||
|
entities.forEach { entity ->
|
||||||
|
val record = entity.toTrainRecord()
|
||||||
|
jsonArray.put(record.toJSON())
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Exported ${entities.size} records to JSON")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to export records to JSON: ${e.message}")
|
||||||
|
}
|
||||||
|
return jsonArray
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun importRecordsFromJson(jsonArray: JSONArray): Int {
|
||||||
|
var importedCount = 0
|
||||||
|
try {
|
||||||
|
val records = mutableListOf<TrainRecordEntity>()
|
||||||
|
for (i in 0 until jsonArray.length()) {
|
||||||
|
val jsonObject = jsonArray.getJSONObject(i)
|
||||||
|
val trainRecord = TrainRecord(jsonObject)
|
||||||
|
records.add(TrainRecordEntity.fromTrainRecord(trainRecord))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (records.isNotEmpty()) {
|
||||||
|
trainRecordDao.insertRecords(records)
|
||||||
|
importedCount = records.size
|
||||||
|
refreshRecordsFromDatabase()
|
||||||
|
Log.d(TAG, "Imported $importedCount records from JSON")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to import records from JSON: ${e.message}")
|
||||||
|
}
|
||||||
|
return importedCount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -42,8 +42,15 @@ import org.noxylva.lbjconsole.model.MergedTrainRecord
|
|||||||
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.util.LocoInfoUtil
|
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
||||||
|
import org.noxylva.lbjconsole.util.TrainTypeUtil
|
||||||
|
import org.osmdroid.util.BoundingBox
|
||||||
|
import org.osmdroid.util.GeoPoint
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
|
||||||
|
data class CardMapView(val center: GeoPoint, val zoom: Double)
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -54,6 +61,7 @@ fun TrainRecordItem(
|
|||||||
expandedStatesMap: MutableMap<String, Boolean>,
|
expandedStatesMap: MutableMap<String, Boolean>,
|
||||||
latestRecord: TrainRecord?,
|
latestRecord: TrainRecord?,
|
||||||
locoInfoUtil: LocoInfoUtil?,
|
locoInfoUtil: LocoInfoUtil?,
|
||||||
|
trainTypeUtil: TrainTypeUtil?,
|
||||||
onRecordClick: (TrainRecord) -> Unit,
|
onRecordClick: (TrainRecord) -> Unit,
|
||||||
onToggleSelection: (TrainRecord) -> Unit,
|
onToggleSelection: (TrainRecord) -> Unit,
|
||||||
onLongClick: (TrainRecord) -> Unit,
|
onLongClick: (TrainRecord) -> Unit,
|
||||||
@@ -62,6 +70,8 @@ fun TrainRecordItem(
|
|||||||
val recordId = record.uniqueId
|
val recordId = record.uniqueId
|
||||||
val isExpanded = expandedStatesMap[recordId] == true
|
val isExpanded = expandedStatesMap[recordId] == true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
val cardColor = when {
|
val cardColor = when {
|
||||||
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
isSelected -> MaterialTheme.colorScheme.primaryContainer
|
||||||
else -> MaterialTheme.colorScheme.surface
|
else -> MaterialTheme.colorScheme.surface
|
||||||
@@ -130,11 +140,19 @@ fun TrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
val trainType = if (record.train?.trim().isNullOrEmpty()) {
|
||||||
text = "${record.rssi} dBm",
|
null
|
||||||
fontSize = 10.sp,
|
} else {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
val lbjClassValue = record.lbjClass?.trim() ?: "NA"
|
||||||
)
|
trainTypeUtil?.getTrainType(lbjClassValue, record.train!!.trim())
|
||||||
|
}
|
||||||
|
if (!trainType.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = trainType,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
@@ -316,6 +334,7 @@ fun TrainRecordItem(
|
|||||||
controller.setZoom(10.0)
|
controller.setZoom(10.0)
|
||||||
controller.setCenter(coordinates)
|
controller.setCenter(coordinates)
|
||||||
this.isTilesScaledToDpi = true
|
this.isTilesScaledToDpi = true
|
||||||
|
tilesScaleFactor = context.resources.displayMetrics.density * 0.2f
|
||||||
this.setUseDataConnection(true)
|
this.setUseDataConnection(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -393,9 +412,12 @@ fun MergedTrainRecordItem(
|
|||||||
mergedRecord: MergedTrainRecord,
|
mergedRecord: MergedTrainRecord,
|
||||||
expandedStatesMap: MutableMap<String, Boolean>,
|
expandedStatesMap: MutableMap<String, Boolean>,
|
||||||
locoInfoUtil: LocoInfoUtil?,
|
locoInfoUtil: LocoInfoUtil?,
|
||||||
|
trainTypeUtil: TrainTypeUtil?,
|
||||||
mergeSettings: MergeSettings? = null,
|
mergeSettings: MergeSettings? = null,
|
||||||
isInEditMode: Boolean = false,
|
isInEditMode: Boolean = false,
|
||||||
selectedRecords: List<TrainRecord> = emptyList(),
|
selectedRecords: List<TrainRecord> = emptyList(),
|
||||||
|
mapViewState: CardMapView?,
|
||||||
|
onMapViewStateChange: (CardMapView) -> Unit,
|
||||||
onToggleSelection: (TrainRecord) -> Unit = {},
|
onToggleSelection: (TrainRecord) -> Unit = {},
|
||||||
onLongClick: (TrainRecord) -> Unit = {},
|
onLongClick: (TrainRecord) -> Unit = {},
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -484,11 +506,19 @@ fun MergedTrainRecordItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
val trainType = if (latestRecord.train?.trim().isNullOrEmpty()) {
|
||||||
text = "${latestRecord.rssi} dBm",
|
null
|
||||||
fontSize = 10.sp,
|
} else {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
val lbjClassValue = latestRecord.lbjClass?.trim() ?: "NA"
|
||||||
)
|
trainTypeUtil?.getTrainType(lbjClassValue, latestRecord.train!!.trim())
|
||||||
|
}
|
||||||
|
if (!trainType.isNullOrEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = trainType,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(2.dp))
|
Spacer(modifier = Modifier.height(2.dp))
|
||||||
@@ -638,93 +668,152 @@ fun MergedTrainRecordItem(
|
|||||||
exit = shrinkVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow))
|
exit = shrinkVertically(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow)) + fadeOut(animationSpec = spring(dampingRatio = Spring.DampingRatioNoBouncy, stiffness = Spring.StiffnessMediumLow))
|
||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
val coordinates = remember { latestRecord.getCoordinates() }
|
val allValidCoordinates = remember {
|
||||||
|
mergedRecord.records
|
||||||
|
.mapNotNull { it.getCoordinates() }
|
||||||
|
.filter { it.latitude != 0.0 || it.longitude != 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allValidCoordinates.isNotEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(220.dp)
|
||||||
|
.padding(vertical = 4.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() }
|
||||||
|
) {},
|
||||||
|
factory = { context ->
|
||||||
|
MapView(context).apply {
|
||||||
|
setTileSource(TileSourceFactory.MAPNIK)
|
||||||
|
setMultiTouchControls(true)
|
||||||
|
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||||
|
isHorizontalMapRepetitionEnabled = false
|
||||||
|
isVerticalMapRepetitionEnabled = false
|
||||||
|
setHasTransientState(true)
|
||||||
|
setOnTouchListener { v, event ->
|
||||||
|
v.parent?.requestDisallowInterceptTouchEvent(true)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
this.isTilesScaledToDpi = true
|
||||||
|
tilesScaleFactor = context.resources.displayMetrics.density * 0.2f
|
||||||
|
this.setUseDataConnection(true)
|
||||||
|
|
||||||
|
addMapListener(object : org.osmdroid.events.MapListener {
|
||||||
|
override fun onScroll(event: org.osmdroid.events.ScrollEvent?): Boolean {
|
||||||
|
val center = mapCenter
|
||||||
|
val zoom = zoomLevelDouble
|
||||||
|
onMapViewStateChange(CardMapView(center as GeoPoint, zoom))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
if (coordinates != null) {
|
override fun onZoom(event: org.osmdroid.events.ZoomEvent?): Boolean {
|
||||||
Box(
|
val center = mapCenter
|
||||||
modifier = Modifier
|
val zoom = zoomLevelDouble
|
||||||
.fillMaxWidth()
|
onMapViewStateChange(CardMapView(center as GeoPoint, zoom))
|
||||||
.height(220.dp)
|
return true
|
||||||
.padding(vertical = 4.dp)
|
}
|
||||||
.clip(RoundedCornerShape(8.dp)),
|
})
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
AndroidView(
|
|
||||||
modifier = Modifier.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {},
|
|
||||||
factory = { context ->
|
|
||||||
MapView(context).apply {
|
|
||||||
setTileSource(TileSourceFactory.MAPNIK)
|
|
||||||
setMultiTouchControls(true)
|
|
||||||
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
|
||||||
isHorizontalMapRepetitionEnabled = false
|
|
||||||
isVerticalMapRepetitionEnabled = false
|
|
||||||
setHasTransientState(true)
|
|
||||||
setOnTouchListener { v, event ->
|
|
||||||
v.parent?.requestDisallowInterceptTouchEvent(true)
|
|
||||||
false
|
|
||||||
}
|
|
||||||
controller.setZoom(10.0)
|
|
||||||
controller.setCenter(coordinates)
|
|
||||||
this.isTilesScaledToDpi = true
|
|
||||||
this.setUseDataConnection(true)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val railwayTileSource = XYTileSource(
|
val railwayTileSource = XYTileSource(
|
||||||
"OpenRailwayMap", 8, 16, 256, ".png",
|
"OpenRailwayMap", 8, 16, 256, ".png",
|
||||||
arrayOf(
|
arrayOf(
|
||||||
"https://a.tiles.openrailwayMap.org/standard/",
|
"https://a.tiles.openrailwaymap.org/standard/",
|
||||||
"https://b.tiles.openrailwaymap.org/standard/",
|
"https://b.tiles.openrailwaymap.org/standard/",
|
||||||
"https://c.tiles.openrailwaymap.org/standard/"
|
"https://c.tiles.openrailwaymap.org/standard/"
|
||||||
),
|
),
|
||||||
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
||||||
)
|
)
|
||||||
|
|
||||||
val railwayProvider = MapTileProviderBasic(context)
|
val railwayProvider = MapTileProviderBasic(context)
|
||||||
railwayProvider.tileSource = railwayTileSource
|
railwayProvider.tileSource = railwayTileSource
|
||||||
|
|
||||||
val railwayOverlay = TilesOverlay(railwayProvider, context)
|
val railwayOverlay = TilesOverlay(railwayProvider, context)
|
||||||
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
|
railwayOverlay.loadingBackgroundColor = android.graphics.Color.TRANSPARENT
|
||||||
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
|
railwayOverlay.loadingLineColor = android.graphics.Color.TRANSPARENT
|
||||||
|
|
||||||
overlays.add(railwayOverlay)
|
overlays.add(railwayOverlay)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
val locationProvider = GpsMyLocationProvider(context).apply {
|
|
||||||
locationUpdateMinDistance = 10f
|
|
||||||
locationUpdateMinTime = 1000
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MyLocationNewOverlay(locationProvider, this).apply {
|
try {
|
||||||
enableMyLocation()
|
val locationProvider = GpsMyLocationProvider(context).apply {
|
||||||
}.also { overlays.add(it) }
|
locationUpdateMinDistance = 10f
|
||||||
} catch (e: Exception) {
|
locationUpdateMinTime = 1000
|
||||||
e.printStackTrace()
|
}
|
||||||
|
|
||||||
|
MyLocationNewOverlay(locationProvider, this).apply {
|
||||||
|
enableMyLocation()
|
||||||
|
}.also { overlays.add(it) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
|
||||||
|
mergedRecord.records.forEach { record ->
|
||||||
|
record.getCoordinates()?.let { coordinates ->
|
||||||
|
if (coordinates.latitude != 0.0 || coordinates.longitude != 0.0) {
|
||||||
|
val recordMap = record.toMap()
|
||||||
|
val marker = Marker(this)
|
||||||
|
marker.position = coordinates
|
||||||
|
|
||||||
|
val latStr = String.format("%.4f", coordinates.latitude)
|
||||||
|
val lonStr = String.format("%.4f", coordinates.longitude)
|
||||||
|
val coordStr = "${latStr}°N, ${lonStr}°E"
|
||||||
|
marker.title = recordMap["train"]?.toString() ?: "列车"
|
||||||
|
marker.snippet = coordStr
|
||||||
|
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
||||||
|
|
||||||
|
overlays.add(marker)
|
||||||
|
if (record == latestRecord) {
|
||||||
|
marker.showInfoWindow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mapViewState != null) {
|
||||||
|
controller.setZoom(mapViewState.zoom)
|
||||||
|
controller.setCenter(mapViewState.center)
|
||||||
|
} else if (allValidCoordinates.size > 1) {
|
||||||
|
val boundingBox = BoundingBox.fromGeoPoints(allValidCoordinates.filter { it.latitude != 0.0 || it.longitude != 0.0 })
|
||||||
|
val layoutListener = object : android.view.View.OnLayoutChangeListener {
|
||||||
|
override fun onLayoutChange(v: android.view.View?, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int) {
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
val zoomLevel = org.osmdroid.views.MapView.getTileSystem().getBoundingBoxZoom(boundingBox, width, height)
|
||||||
|
val latSpan = boundingBox.latitudeSpan
|
||||||
|
val adjustedCenter = org.osmdroid.util.GeoPoint(
|
||||||
|
boundingBox.center.latitude + latSpan * 0.25, // Shift center UP (north) to create top padding
|
||||||
|
boundingBox.center.longitude
|
||||||
|
)
|
||||||
|
val newZoom = zoomLevel - 1.0
|
||||||
|
|
||||||
|
controller.setZoom(newZoom)
|
||||||
|
controller.setCenter(adjustedCenter)
|
||||||
|
onMapViewStateChange(CardMapView(adjustedCenter, newZoom))
|
||||||
|
|
||||||
|
removeOnLayoutChangeListener(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addOnLayoutChangeListener(layoutListener)
|
||||||
|
} else if (allValidCoordinates.isNotEmpty()) {
|
||||||
|
val center = allValidCoordinates.first()
|
||||||
|
val zoom = 10.0
|
||||||
|
controller.setZoom(zoom)
|
||||||
|
controller.setCenter(center)
|
||||||
|
onMapViewStateChange(CardMapView(center, zoom))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
val marker = Marker(this)
|
update = { mapView -> mapView.invalidate() }
|
||||||
marker.position = coordinates
|
)
|
||||||
|
|
||||||
val latStr = String.format("%.4f", coordinates.latitude)
|
|
||||||
val lonStr = String.format("%.4f", coordinates.longitude)
|
|
||||||
val coordStr = "${latStr}°N, ${lonStr}°E"
|
|
||||||
marker.title = recordMap["train"]?.toString() ?: "列车"
|
|
||||||
marker.snippet = coordStr
|
|
||||||
marker.setInfoWindowAnchor(Marker.ANCHOR_CENTER, 0f)
|
|
||||||
|
|
||||||
overlays.add(marker)
|
|
||||||
marker.showInfoWindow()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update = { mapView -> mapView.invalidate() }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (recordMap.containsKey("position_info")) {
|
if (recordMap.containsKey("position_info")) {
|
||||||
@@ -771,6 +860,30 @@ fun MergedTrainRecordItem(
|
|||||||
"${recordItem.locoType}-${recordItem.loco}"
|
"${recordItem.locoType}-${recordItem.loco}"
|
||||||
} else null
|
} else null
|
||||||
}
|
}
|
||||||
|
GroupBy.TRAIN_OR_LOCO -> {
|
||||||
|
val latestTrain = mergedRecord.latestRecord.train.trim()
|
||||||
|
val latestLoco = mergedRecord.latestRecord.loco.trim()
|
||||||
|
val recordTrain = recordItem.train.trim()
|
||||||
|
val recordLoco = recordItem.loco.trim()
|
||||||
|
|
||||||
|
when {
|
||||||
|
latestTrain.isNotEmpty() && latestTrain != "<NUL>" &&
|
||||||
|
recordTrain.isNotEmpty() && recordTrain != "<NUL>" &&
|
||||||
|
latestTrain == recordTrain && latestLoco != recordLoco -> {
|
||||||
|
if (recordLoco.isNotEmpty() && recordLoco != "<NUL>") {
|
||||||
|
"${recordItem.locoType}-${recordLoco}"
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
latestLoco.isNotEmpty() && latestLoco != "<NUL>" &&
|
||||||
|
recordLoco.isNotEmpty() && recordLoco != "<NUL>" &&
|
||||||
|
latestLoco == recordLoco && latestTrain != recordTrain -> {
|
||||||
|
if (recordTrain.isNotEmpty() && recordTrain != "<NUL>") {
|
||||||
|
recordTrain
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -849,6 +962,7 @@ fun HistoryScreen(
|
|||||||
lastUpdateTime: Date?,
|
lastUpdateTime: Date?,
|
||||||
temporaryStatusMessage: String? = null,
|
temporaryStatusMessage: String? = null,
|
||||||
locoInfoUtil: LocoInfoUtil? = null,
|
locoInfoUtil: LocoInfoUtil? = null,
|
||||||
|
trainTypeUtil: TrainTypeUtil? = null,
|
||||||
mergeSettings: MergeSettings? = null,
|
mergeSettings: MergeSettings? = null,
|
||||||
onClearRecords: () -> Unit = {},
|
onClearRecords: () -> Unit = {},
|
||||||
onRecordClick: (TrainRecord) -> Unit = {},
|
onRecordClick: (TrainRecord) -> Unit = {},
|
||||||
@@ -857,9 +971,10 @@ fun HistoryScreen(
|
|||||||
editMode: Boolean = false,
|
editMode: Boolean = false,
|
||||||
selectedRecords: Set<String> = emptySet(),
|
selectedRecords: Set<String> = emptySet(),
|
||||||
expandedStates: Map<String, Boolean> = emptyMap(),
|
expandedStates: Map<String, Boolean> = emptyMap(),
|
||||||
|
mapViewStates: Map<String, CardMapView> = emptyMap(),
|
||||||
scrollPosition: Int = 0,
|
scrollPosition: Int = 0,
|
||||||
scrollOffset: Int = 0,
|
scrollOffset: Int = 0,
|
||||||
onStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit = { _, _, _, _, _ -> }
|
onStateChange: (Boolean, Set<String>, Map<String, Boolean>, Map<String, CardMapView>, Int, Int) -> Unit = { _, _, _, _, _, _ -> }
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val refreshKey = latestRecord?.timestamp?.time ?: 0
|
val refreshKey = latestRecord?.timestamp?.time ?: 0
|
||||||
@@ -889,6 +1004,9 @@ fun HistoryScreen(
|
|||||||
val expandedStatesMap = remember(expandedStates) {
|
val expandedStatesMap = remember(expandedStates) {
|
||||||
mutableStateMapOf<String, Boolean>().apply { putAll(expandedStates) }
|
mutableStateMapOf<String, Boolean>().apply { putAll(expandedStates) }
|
||||||
}
|
}
|
||||||
|
val mapViewStatesMap = remember(mapViewStates) {
|
||||||
|
mutableStateMapOf<String, CardMapView>().apply { putAll(mapViewStates) }
|
||||||
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState(
|
val listState = rememberLazyListState(
|
||||||
initialFirstVisibleItemIndex = scrollPosition,
|
initialFirstVisibleItemIndex = scrollPosition,
|
||||||
@@ -919,28 +1037,28 @@ fun HistoryScreen(
|
|||||||
|
|
||||||
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
|
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
|
||||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(expandedStatesMap.toMap()) {
|
LaunchedEffect(expandedStatesMap.toMap()) {
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||||
delay(50)
|
delay(50)
|
||||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) {
|
||||||
if (!isInEditMode) {
|
if (!isInEditMode) {
|
||||||
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
val selectedIds = selectedRecordsList.map { it.uniqueId }.toSet()
|
||||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(selectedRecordsList.size) {
|
LaunchedEffect(selectedRecordsList.size) {
|
||||||
if (selectedRecordsList.isEmpty() && isInEditMode) {
|
if (selectedRecordsList.isEmpty() && isInEditMode) {
|
||||||
isInEditMode = false
|
isInEditMode = false
|
||||||
onStateChange(false, emptySet(), expandedStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
onStateChange(false, emptySet(), expandedStatesMap.toMap(), mapViewStatesMap.toMap(), listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,6 +1136,7 @@ fun HistoryScreen(
|
|||||||
expandedStatesMap = expandedStatesMap,
|
expandedStatesMap = expandedStatesMap,
|
||||||
latestRecord = latestRecord,
|
latestRecord = latestRecord,
|
||||||
locoInfoUtil = locoInfoUtil,
|
locoInfoUtil = locoInfoUtil,
|
||||||
|
trainTypeUtil = trainTypeUtil,
|
||||||
onRecordClick = onRecordClick,
|
onRecordClick = onRecordClick,
|
||||||
onToggleSelection = { record ->
|
onToggleSelection = { record ->
|
||||||
if (selectedRecordsList.contains(record)) {
|
if (selectedRecordsList.contains(record)) {
|
||||||
@@ -1041,9 +1160,14 @@ fun HistoryScreen(
|
|||||||
mergedRecord = item,
|
mergedRecord = item,
|
||||||
expandedStatesMap = expandedStatesMap,
|
expandedStatesMap = expandedStatesMap,
|
||||||
locoInfoUtil = locoInfoUtil,
|
locoInfoUtil = locoInfoUtil,
|
||||||
|
trainTypeUtil = trainTypeUtil,
|
||||||
mergeSettings = mergeSettings,
|
mergeSettings = mergeSettings,
|
||||||
isInEditMode = isInEditMode,
|
isInEditMode = isInEditMode,
|
||||||
selectedRecords = selectedRecordsList,
|
selectedRecords = selectedRecordsList,
|
||||||
|
mapViewState = mapViewStatesMap[item.groupKey],
|
||||||
|
onMapViewStateChange = { newState ->
|
||||||
|
mapViewStatesMap[item.groupKey] = newState
|
||||||
|
},
|
||||||
onToggleSelection = { record ->
|
onToggleSelection = { record ->
|
||||||
if (selectedRecordsList.contains(record)) {
|
if (selectedRecordsList.contains(record)) {
|
||||||
selectedRecordsList.remove(record)
|
selectedRecordsList.remove(record)
|
||||||
|
|||||||
@@ -15,7 +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 kotlinx.coroutines.launch
|
||||||
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
|
||||||
@@ -24,7 +24,7 @@ import org.noxylva.lbjconsole.BackgroundService
|
|||||||
import org.noxylva.lbjconsole.NotificationService
|
import org.noxylva.lbjconsole.NotificationService
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,19 +44,14 @@ fun SettingsScreen(
|
|||||||
onAutoConnectEnabledChange: (Boolean) -> Unit = {}
|
onAutoConnectEnabledChange: (Boolean) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState(initial = scrollPosition)
|
||||||
|
|
||||||
LaunchedEffect(scrollPosition) {
|
DisposableEffect(Unit) {
|
||||||
if (scrollState.value != scrollPosition) {
|
onDispose {
|
||||||
scrollState.scrollTo(scrollPosition)
|
onScrollPositionChange(scrollState.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(scrollState.value) {
|
|
||||||
delay(50)
|
|
||||||
onScrollPositionChange(scrollState.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
@@ -200,8 +195,11 @@ fun SettingsScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val notificationService = remember(context) { NotificationService(context) }
|
val notificationService = remember(context) { NotificationService(context) }
|
||||||
|
|
||||||
var backgroundServiceEnabled by remember(context) {
|
var backgroundServiceEnabled by remember { mutableStateOf<Boolean?>(null) }
|
||||||
mutableStateOf(SettingsActivity.isBackgroundServiceEnabled(context))
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
LaunchedEffect(context) {
|
||||||
|
backgroundServiceEnabled = SettingsActivity.isBackgroundServiceEnabled(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationEnabled by remember(context, notificationService) {
|
var notificationEnabled by remember(context, notificationService) {
|
||||||
@@ -225,19 +223,24 @@ fun SettingsScreen(
|
|||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(
|
if (backgroundServiceEnabled == null) {
|
||||||
checked = backgroundServiceEnabled,
|
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||||
onCheckedChange = { enabled ->
|
} else {
|
||||||
backgroundServiceEnabled = enabled
|
Switch(
|
||||||
SettingsActivity.setBackgroundServiceEnabled(context, enabled)
|
checked = backgroundServiceEnabled!!,
|
||||||
|
onCheckedChange = { enabled ->
|
||||||
if (enabled) {
|
backgroundServiceEnabled = enabled
|
||||||
BackgroundService.startService(context)
|
coroutineScope.launch {
|
||||||
} else {
|
SettingsActivity.setBackgroundServiceEnabled(context, enabled)
|
||||||
BackgroundService.stopService(context)
|
if (enabled) {
|
||||||
|
BackgroundService.startService(context)
|
||||||
|
} else {
|
||||||
|
BackgroundService.stopService(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ object LocationUtils {
|
|||||||
|
|
||||||
val minuteEndIndex = dmsString.indexOf('′')
|
val minuteEndIndex = dmsString.indexOf('′')
|
||||||
if (minuteEndIndex == -1) {
|
if (minuteEndIndex == -1) {
|
||||||
return degrees
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
val minutes = dmsString.substring(degreeIndex + 1, minuteEndIndex).toDouble()
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package org.noxylva.lbjconsole.util
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
class TrainTypeUtil(private val context: Context) {
|
||||||
|
private val trainTypePatterns = mutableListOf<Pair<Pattern, String>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadTrainTypePatterns()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadTrainTypePatterns() {
|
||||||
|
try {
|
||||||
|
val inputStream = context.assets.open("train_number_info.csv")
|
||||||
|
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||||
|
|
||||||
|
reader.useLines { lines ->
|
||||||
|
lines.forEach { line ->
|
||||||
|
if (line.isNotBlank()) {
|
||||||
|
val firstQuoteEnd = line.indexOf('"', 1)
|
||||||
|
if (firstQuoteEnd > 0 && firstQuoteEnd < line.length - 1) {
|
||||||
|
val regex = line.substring(1, firstQuoteEnd)
|
||||||
|
val remainingPart = line.substring(firstQuoteEnd + 1).trim()
|
||||||
|
if (remainingPart.startsWith(",\"") && remainingPart.endsWith("\"")) {
|
||||||
|
val type = remainingPart.substring(2, remainingPart.length - 1)
|
||||||
|
try {
|
||||||
|
val pattern = Pattern.compile(regex)
|
||||||
|
trainTypePatterns.add(Pair(pattern, type))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTrainType(locoType: String, train: String): String? {
|
||||||
|
if (train.isEmpty()) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val actualTrain = if (locoType == "NA") {
|
||||||
|
train
|
||||||
|
} else {
|
||||||
|
locoType + train
|
||||||
|
}
|
||||||
|
|
||||||
|
for ((pattern, type) in trainTypePatterns) {
|
||||||
|
if (pattern.matcher(actualTrain).matches()) {
|
||||||
|
return type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
0
app/src/main/res/drawable/ic_launcher_background.xml
Normal file → Executable file
0
app/src/main/res/drawable/ic_launcher_foreground.xml
Normal file → Executable file
0
app/src/main/res/drawable/ic_notification.xml
Normal file → Executable file
0
app/src/main/res/drawable/ic_person.xml
Normal file → Executable file
0
app/src/main/res/layout/activity_settings.xml
Normal file → Executable file
0
app/src/main/res/layout/notification_train_record.xml
Normal file → Executable file
0
app/src/main/res/mipmap-anydpi/ic_launcher.xml
Normal file → Executable file
0
app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
Normal file → Executable file
0
app/src/main/res/mipmap-hdpi/ic_launcher.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
0
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
0
app/src/main/res/mipmap-mdpi/ic_launcher.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 982 B |
0
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
0
app/src/main/res/mipmap-xhdpi/ic_launcher.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
0
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
0
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
0
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
0
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
0
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
Normal file → Executable file
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 7.6 KiB |
0
app/src/main/res/raw/loco_info.csv
Normal file → Executable file
0
app/src/main/res/values/colors.xml
Normal file → Executable file
0
app/src/main/res/values/strings.xml
Normal file → Executable file
0
app/src/main/res/values/themes.xml
Normal file → Executable file
0
app/src/main/res/xml/backup_rules.xml
Normal file → Executable file
0
app/src/main/res/xml/data_extraction_rules.xml
Normal file → Executable file
0
app/src/main/res/xml/file_paths.xml
Normal file → Executable file
@@ -8,6 +8,8 @@ espressoCore = "3.6.1"
|
|||||||
lifecycleRuntimeKtx = "2.9.0"
|
lifecycleRuntimeKtx = "2.9.0"
|
||||||
activityCompose = "1.10.1"
|
activityCompose = "1.10.1"
|
||||||
composeBom = "2024.04.01"
|
composeBom = "2024.04.01"
|
||||||
|
room = "2.6.1"
|
||||||
|
startup = "1.1.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
@@ -24,9 +26,14 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
|
|||||||
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||||
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||||
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||||
|
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||||
|
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
|
||||||
|
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||||
|
androidx-startup-runtime = { group = "androidx.startup", name = "startup-runtime", version.ref = "startup" }
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
ksp = { id = "com.google.devtools.ksp", version = "2.0.0-1.0.21" }
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "LBJ Receiver"
|
rootProject.name = "LBJ_Console"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
||||||