chore: update project name and .gitignore file
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -18,4 +18,7 @@ local.properties
|
|||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
*.base64
|
*.base64
|
||||||
docs
|
docs
|
||||||
|
linux
|
||||||
|
windows
|
||||||
|
android_original
|
||||||
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
LBJ Receiver
|
LBJ_Console
|
||||||
@@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
|
|||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
entities = [TrainRecordEntity::class, AppSettingsEntity::class],
|
||||||
version = 3,
|
version = 4,
|
||||||
exportSchema = false
|
exportSchema = false
|
||||||
)
|
)
|
||||||
abstract class TrainDatabase : RoomDatabase() {
|
abstract class TrainDatabase : RoomDatabase() {
|
||||||
@@ -54,13 +54,89 @@ abstract class TrainDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val MIGRATION_3_4 = object : Migration(3, 4) {
|
||||||
|
override fun migrate(database: SupportSQLiteDatabase) {
|
||||||
|
// Since we can't determine the exact schema change, we'll use fallback migration
|
||||||
|
// This will preserve data where possible while updating the schema
|
||||||
|
|
||||||
|
// Create new table with correct schema
|
||||||
|
database.execSQL("""
|
||||||
|
CREATE TABLE IF NOT EXISTS `app_settings_new` (
|
||||||
|
`id` INTEGER NOT NULL,
|
||||||
|
`deviceName` TEXT NOT NULL DEFAULT 'LBJReceiver',
|
||||||
|
`currentTab` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historyEditMode` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historySelectedRecords` TEXT NOT NULL DEFAULT '',
|
||||||
|
`historyExpandedStates` TEXT NOT NULL DEFAULT '',
|
||||||
|
`historyScrollPosition` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`historyScrollOffset` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`settingsScrollPosition` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`mapCenterLat` REAL,
|
||||||
|
`mapCenterLon` REAL,
|
||||||
|
`mapZoomLevel` REAL NOT NULL DEFAULT 10.0,
|
||||||
|
`mapRailwayLayerVisible` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`specifiedDeviceAddress` TEXT,
|
||||||
|
`searchOrderList` TEXT NOT NULL DEFAULT '',
|
||||||
|
`autoConnectEnabled` INTEGER NOT NULL DEFAULT 1,
|
||||||
|
`backgroundServiceEnabled` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`notificationEnabled` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
PRIMARY KEY(`id`)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
// Copy data from old table to new table, handling missing columns
|
||||||
|
try {
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO `app_settings_new` (
|
||||||
|
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
|
||||||
|
historyExpandedStates, historyScrollPosition, historyScrollOffset,
|
||||||
|
settingsScrollPosition, mapCenterLat, mapCenterLon, mapZoomLevel,
|
||||||
|
mapRailwayLayerVisible
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
COALESCE(id, 1),
|
||||||
|
COALESCE(deviceName, 'LBJReceiver'),
|
||||||
|
COALESCE(currentTab, 0),
|
||||||
|
COALESCE(historyEditMode, 0),
|
||||||
|
COALESCE(historySelectedRecords, ''),
|
||||||
|
COALESCE(historyExpandedStates, ''),
|
||||||
|
COALESCE(historyScrollPosition, 0),
|
||||||
|
COALESCE(historyScrollOffset, 0),
|
||||||
|
COALESCE(settingsScrollPosition, 0),
|
||||||
|
mapCenterLat,
|
||||||
|
mapCenterLon,
|
||||||
|
COALESCE(mapZoomLevel, 10.0),
|
||||||
|
COALESCE(mapRailwayLayerVisible, 1)
|
||||||
|
FROM `app_settings`
|
||||||
|
""")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// If the old table doesn't exist or has different structure, insert default
|
||||||
|
database.execSQL("""
|
||||||
|
INSERT INTO `app_settings_new` (
|
||||||
|
id, deviceName, currentTab, historyEditMode, historySelectedRecords,
|
||||||
|
historyExpandedStates, historyScrollPosition, historyScrollOffset,
|
||||||
|
settingsScrollPosition, mapZoomLevel, mapRailwayLayerVisible,
|
||||||
|
searchOrderList, autoConnectEnabled, backgroundServiceEnabled,
|
||||||
|
notificationEnabled
|
||||||
|
) VALUES (
|
||||||
|
1, 'LBJReceiver', 0, 0, '', '', 0, 0, 0, 10.0, 1, '', 1, 0, 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old table and rename new table
|
||||||
|
database.execSQL("DROP TABLE IF EXISTS `app_settings`")
|
||||||
|
database.execSQL("ALTER TABLE `app_settings_new` RENAME TO `app_settings`")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getDatabase(context: Context): TrainDatabase {
|
fun getDatabase(context: Context): TrainDatabase {
|
||||||
return INSTANCE ?: synchronized(this) {
|
return INSTANCE ?: synchronized(this) {
|
||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
TrainDatabase::class.java,
|
TrainDatabase::class.java,
|
||||||
"train_database"
|
"train_database"
|
||||||
).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build()
|
).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build()
|
||||||
INSTANCE = instance
|
INSTANCE = instance
|
||||||
instance
|
instance
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,308 +0,0 @@
|
|||||||
package org.noxylva.lbjconsole.ui.screens
|
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
|
||||||
import androidx.compose.animation.core.*
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.TextUnit
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import org.noxylva.lbjconsole.model.TrainRecord
|
|
||||||
import org.noxylva.lbjconsole.ui.components.TrainDetailDialog
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MonitorScreen(
|
|
||||||
latestRecord: TrainRecord?,
|
|
||||||
recentRecords: List<TrainRecord>,
|
|
||||||
lastUpdateTime: Date?,
|
|
||||||
temporaryStatusMessage: String? = null,
|
|
||||||
onRecordClick: (TrainRecord) -> Unit,
|
|
||||||
onClearLog: () -> Unit
|
|
||||||
) {
|
|
||||||
var showDetailDialog by remember { mutableStateOf(false) }
|
|
||||||
var selectedRecord by remember { mutableStateOf<TrainRecord?>(null) }
|
|
||||||
var isPressed by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
targetValue = if (isPressed) 0.98f else 1f,
|
|
||||||
animationSpec = tween(durationMillis = 120),
|
|
||||||
label = "content_scale"
|
|
||||||
)
|
|
||||||
|
|
||||||
LaunchedEffect(isPressed) {
|
|
||||||
if (isPressed) {
|
|
||||||
delay(100)
|
|
||||||
isPressed = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val timeSinceLastUpdate = remember { mutableStateOf<String?>(null) }
|
|
||||||
LaunchedEffect(key1 = lastUpdateTime) {
|
|
||||||
if (lastUpdateTime != null) {
|
|
||||||
while (true) {
|
|
||||||
val now = Date()
|
|
||||||
val diffInSec = (now.time - lastUpdateTime.time) / 1000
|
|
||||||
timeSinceLastUpdate.value = when {
|
|
||||||
diffInSec < 60 -> "${diffInSec}秒前"
|
|
||||||
diffInSec < 3600 -> "${diffInSec / 60}分钟前"
|
|
||||||
else -> "${diffInSec / 3600}小时前"
|
|
||||||
}
|
|
||||||
val updateInterval = if (diffInSec < 60) 500L else if (diffInSec < 3600) 30000L else 300000L
|
|
||||||
delay(updateInterval)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp)) {
|
|
||||||
Card(modifier = Modifier.fillMaxSize()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(20.dp)
|
|
||||||
) {
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = timeSinceLastUpdate.value ?: "暂无数据",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
AnimatedContent(
|
|
||||||
targetState = latestRecord,
|
|
||||||
transitionSpec = {
|
|
||||||
fadeIn(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 300,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
)
|
|
||||||
) + slideInVertically(
|
|
||||||
initialOffsetY = { it / 4 },
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 300,
|
|
||||||
easing = FastOutSlowInEasing
|
|
||||||
)
|
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 150,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
) + slideOutVertically(
|
|
||||||
targetOffsetY = { -it / 4 },
|
|
||||||
animationSpec = tween(
|
|
||||||
durationMillis = 150,
|
|
||||||
easing = FastOutLinearInEasing
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
label = "content_animation"
|
|
||||||
) { record ->
|
|
||||||
if (record != null) {
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = rememberRipple(bounded = true)
|
|
||||||
) {
|
|
||||||
isPressed = true
|
|
||||||
selectedRecord = record
|
|
||||||
showDetailDialog = true
|
|
||||||
onRecordClick(record)
|
|
||||||
}
|
|
||||||
.padding(8.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
scaleX = scale
|
|
||||||
scaleY = scale
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
val recordMap = record.toMap()
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = recordMap["train"]?.toString() ?: "",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 20.sp,
|
|
||||||
color = MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = recordMap["direction"]?.toString() ?: "",
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
fontSize = 16.sp,
|
|
||||||
color = when(recordMap["direction"]?.toString()) {
|
|
||||||
"上行" -> MaterialTheme.colorScheme.primary
|
|
||||||
"下行" -> MaterialTheme.colorScheme.secondary
|
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(6.dp))
|
|
||||||
|
|
||||||
|
|
||||||
if (recordMap.containsKey("time")) {
|
|
||||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
|
||||||
Text(
|
|
||||||
text = timeLine,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(thickness = 0.5.dp)
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween
|
|
||||||
) {
|
|
||||||
recordMap["speed"]?.let { speed ->
|
|
||||||
Text(
|
|
||||||
text = speed,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
recordMap["position"]?.let { position ->
|
|
||||||
Text(
|
|
||||||
text = position,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
recordMap.forEach { (key, value) ->
|
|
||||||
when (key) {
|
|
||||||
"timestamp", "train", "direction", "time", "speed", "position", "position_info" -> {}
|
|
||||||
else -> {
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (recordMap.containsKey("position_info")) {
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
Text(
|
|
||||||
text = recordMap["position_info"] ?: "",
|
|
||||||
fontSize = 14.sp,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
|
||||||
Text(
|
|
||||||
"暂无列车信息",
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
color = MaterialTheme.colorScheme.outline
|
|
||||||
)
|
|
||||||
|
|
||||||
if (lastUpdateTime != null) {
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
Text(
|
|
||||||
"上次接收数据: ${SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(lastUpdateTime)}",
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (showDetailDialog && selectedRecord != null) {
|
|
||||||
TrainDetailDialog(
|
|
||||||
trainRecord = selectedRecord!!,
|
|
||||||
onDismiss = { showDetailDialog = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun InfoItem(
|
|
||||||
label: String,
|
|
||||||
value: String,
|
|
||||||
fontSize: TextUnit = 14.sp
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 2.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "$label: ",
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
|
||||||
text = value,
|
|
||||||
fontSize = fontSize,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user