diff --git a/.gitignore b/.gitignore
index 4bdae4d..fcfadf8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,4 +21,146 @@ local.properties
docs
linux
windows
-android_original
\ No newline at end of file
+flutter/ephemeral/
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+x64/
+x86/
+*.[Cc]ache
+!*.[Cc]ache/
+.gradle/
+build/
+local.properties
+*.log
+captures/
+.externalNativeBuild/
+.cxx/
+*.apk
+output.json
+*.iml
+.idea/
+misc.xml
+deploymentTargetDropDown.xml
+render.experimental.xml
+*.jks
+*.keystore
+google-services.json
+*.hprof
+gen-external-apklibs
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.fvm/flutter_sdk
+.packages
+.pub-cache/
+.pub/
+coverage/
+lib/generated_plugin_registrant.dart
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/key.properties
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/.last_build_id
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+*.ap_
+*.aab
+*.dex
+*.class
+bin/
+gen/
+out/
+.gradle
+.signing/
+proguard/
+/*/build/
+/*/local.properties
+/*/out
+/*/*/build
+/*/*/production
+.navigation/
+*.ipr
+*~
+*.swp
+.externalNativeBuild
+obj/
+*.iws
+/out/
+.idea/caches/
+.idea/libraries/
+.idea/shelf/
+.idea/workspace.xml
+.idea/tasks.xml
+.idea/.name
+.idea/compiler.xml
+.idea/copyright/profiles_settings.xml
+.idea/encodings.xml
+.idea/misc.xml
+.idea/modules.xml
+.idea/scopes/scope_settings.xml
+.idea/dictionaries
+.idea/vcs.xml
+.idea/jsLibraryMappings.xml
+.idea/datasources.xml
+.idea/dataSources.ids
+.idea/sqlDataSources.xml
+.idea/dynamic.xml
+.idea/uiDesigner.xml
+.idea/assetWizardSettings.xml
+.idea/gradle.xml
+.idea/jarRepositories.xml
+.idea/navEditor.xml
+.classpath
+.project
+.cproject
+.settings/
+.mtj.tmp/
+*.war
+*.ear
+hs_err_pid*
+.idea_modules/
+atlassian-ide-plugin.xml
+.idea/mongoSettings.xml
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+!/gradle/wrapper/gradle-wrapper.jar
+macos/Flutter/ephemeral/flutter_export_environment.sh
+macos/Flutter/ephemeral/Flutter-Generated.xcconfig
diff --git a/README.md b/README.md
index 0f8f23c..a131f1d 100644
--- a/README.md
+++ b/README.md
@@ -1,12 +1,12 @@
# LBJ Console
-LBJ Console 是一款 Android 应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
+LBJ Console 是一款应用程序,用于通过 BLE 从 [SX1276_Receive_LBJ](https://github.com/undef-i/SX1276_Receive_LBJ) 设备接收并显示列车预警消息,功能包括:
- 接收列车预警消息,支持可选的手机推送通知。
- 在地图上显示预警消息的 GPS 信息。
- 基于内置数据文件显示机车配属,机车类型和车次类型。
-
+主分支目前只适配了 Android 。如需在其它平台上面使用,请参考 [flutter](https://github.com/undef-i/LBJ_Console/tree/flutter) 分支自行编译。
## 数据文件
LBJ Console 依赖以下数据文件,位于 `app/src/main/assets/` 目录,用于支持机车配属和车次信息的展示:
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 75e3f70..f2292d1 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -13,8 +13,8 @@ android {
applicationId = "org.noxylva.lbjconsole"
minSdk = 29
targetSdk = 35
- versionCode = 13
- versionName = "0.1.3"
+ versionCode = 14
+ versionName = "0.1.4"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -92,4 +92,5 @@ dependencies {
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
implementation(libs.androidx.startup.runtime)
+ implementation("com.google.code.gson:gson:2.10.1")
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index cc2ae1f..5ab5764 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,6 +15,9 @@
+
+
@@ -41,6 +44,12 @@
+
+
+ uri?.let { exportDatabase(it) }
+ }
+
+ private val importFilePicker = registerForActivityResult(
+ ActivityResultContracts.OpenDocument()
+ ) { uri ->
+ uri?.let { importDatabase(it) }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val action = intent.getStringExtra("action") ?: "export"
+
+ setContent {
+ val coroutineScope = rememberCoroutineScope()
+
+ when (action) {
+ "export" -> {
+ val fileName = "lbj_console_backup_${System.currentTimeMillis()}.json"
+ exportFilePicker.launch(fileName)
+ }
+ "import" -> {
+ importFilePicker.launch(arrayOf("application/json", "text/plain"))
+ }
+ }
+ }
+ }
+
+ private fun exportDatabase(uri: Uri) {
+ val coroutineScope = kotlinx.coroutines.MainScope()
+ coroutineScope.launch {
+ try {
+ val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity)
+ val json = databaseUtil.exportDatabase()
+
+ contentResolver.openOutputStream(uri)?.use { outputStream ->
+ outputStream.write(json.toByteArray())
+ }
+
+ Toast.makeText(
+ this@FilePickerActivity,
+ "数据导出成功",
+ Toast.LENGTH_SHORT
+ ).show()
+
+ } catch (e: Exception) {
+ Toast.makeText(
+ this@FilePickerActivity,
+ "导出失败: ${e.message}",
+ Toast.LENGTH_SHORT
+ ).show()
+ } finally {
+ finish()
+ }
+ }
+ }
+
+ private fun importDatabase(uri: Uri) {
+ val coroutineScope = kotlinx.coroutines.MainScope()
+ coroutineScope.launch {
+ try {
+ val databaseUtil = DatabaseExportImportUtil(this@FilePickerActivity)
+ val success = databaseUtil.importDatabase(uri)
+
+ if (success) {
+ Toast.makeText(
+ this@FilePickerActivity,
+ "数据导入成功",
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ Toast.makeText(
+ this@FilePickerActivity,
+ "数据导入失败",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+
+ } catch (e: Exception) {
+ Toast.makeText(
+ this@FilePickerActivity,
+ "导入失败: ${e.message}",
+ Toast.LENGTH_SHORT
+ ).show()
+ } finally {
+ finish()
+ }
+ }
+ }
+
+ companion object {
+ fun createExportIntent(context: android.content.Context): Intent {
+ return Intent(context, FilePickerActivity::class.java).apply {
+ putExtra("action", "export")
+ }
+ }
+
+ fun createImportIntent(context: android.content.Context): Intent {
+ return Intent(context, FilePickerActivity::class.java).apply {
+ putExtra("action", "import")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt
index e21296b..58cd4fc 100644
--- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt
+++ b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/SettingsScreen.kt
@@ -1,5 +1,6 @@
package org.noxylva.lbjconsole.ui.screens
+import android.content.Intent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -22,6 +23,7 @@ import org.noxylva.lbjconsole.model.TimeWindow
import org.noxylva.lbjconsole.database.AppSettingsRepository
import org.noxylva.lbjconsole.BackgroundService
import org.noxylva.lbjconsole.NotificationService
+import org.noxylva.lbjconsole.FilePickerActivity
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.DisposableEffect
@@ -426,6 +428,66 @@ fun SettingsScreen(
}
+ Card(
+ modifier = Modifier.fillMaxWidth(),
+ colors = CardDefaults.cardColors(
+ containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
+ ),
+ shape = RoundedCornerShape(16.dp)
+ ) {
+ Column(
+ modifier = Modifier.padding(20.dp),
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Storage,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ "数据管理",
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold
+ )
+ }
+
+ val context = LocalContext.current
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.SpaceEvenly
+ ) {
+ Button(
+ onClick = {
+ val intent = FilePickerActivity.createExportIntent(context)
+ context.startActivity(intent)
+ },
+ modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
+ ) {
+ Icon(Icons.Default.Upload, contentDescription = null)
+ Spacer(Modifier.width(8.dp))
+ Text("导出")
+ }
+
+ Button(
+ onClick = {
+ val intent = FilePickerActivity.createImportIntent(context)
+ context.startActivity(intent)
+ },
+ modifier = Modifier.weight(1f).padding(horizontal = 4.dp)
+ ) {
+ Icon(Icons.Default.Download, contentDescription = null)
+ Spacer(Modifier.width(8.dp))
+ Text("导入")
+ }
+ }
+ }
+ }
+
Text(
text = "LBJ Console v$appVersion by undef-i",
style = MaterialTheme.typography.bodySmall,
diff --git a/app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt b/app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt
new file mode 100644
index 0000000..a05ac3d
--- /dev/null
+++ b/app/src/main/java/org/noxylva/lbjconsole/util/DatabaseExportImportUtil.kt
@@ -0,0 +1,67 @@
+package org.noxylva.lbjconsole.util
+
+import android.content.Context
+import android.net.Uri
+import android.util.Log
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.noxylva.lbjconsole.database.AppSettingsEntity
+import org.noxylva.lbjconsole.database.TrainDatabase
+import org.noxylva.lbjconsole.database.TrainRecordEntity
+import java.io.*
+import java.util.*
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+
+class DatabaseExportImportUtil(private val context: Context) {
+ private val gson = Gson()
+ private val database = TrainDatabase.getDatabase(context)
+
+ data class SimpleRecordBackup(
+ val records: List
+ )
+
+ suspend fun exportDatabase(): String = withContext(Dispatchers.IO) {
+ try {
+ val trainRecords = database.trainRecordDao().getAllRecords()
+
+ val backup = SimpleRecordBackup(
+ records = trainRecords
+ )
+
+ val json = gson.toJson(backup)
+ json
+ } catch (e: Exception) {
+ Log.e("DatabaseExport", "导出失败", e)
+ throw e
+ }
+ }
+
+ suspend fun importDatabase(uri: Uri): Boolean = withContext(Dispatchers.IO) {
+ try {
+ context.contentResolver.openInputStream(uri)?.use { inputStream ->
+ val json = inputStream.bufferedReader().use { it.readText() }
+ val backup: SimpleRecordBackup = gson.fromJson(json, object : TypeToken() {}.type)
+
+ database.trainRecordDao().deleteAllRecords()
+
+ if (backup.records.isNotEmpty()) {
+ database.trainRecordDao().insertRecords(backup.records)
+ }
+
+ true
+ } ?: false
+ } catch (e: Exception) {
+ Log.e("DatabaseImport", "导入失败", e)
+ false
+ }
+ }
+
+ suspend fun getExportFileUri(): Uri {
+ val filePath = exportDatabase()
+ return Uri.parse("file://$filePath")
+ }
+}
\ No newline at end of file