diff --git a/.gitignore b/.gitignore index c03c112..4bdae4d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,7 @@ local.properties *.jks *.keystore *.base64 -docs \ No newline at end of file +docs +linux +windows +android_original \ No newline at end of file diff --git a/.idea/.name b/.idea/.name index d07af4f..cf9db12 100644 --- a/.idea/.name +++ b/.idea/.name @@ -1 +1 @@ -LBJ Receiver \ No newline at end of file +LBJ_Console \ No newline at end of file diff --git a/app/src/main/java/org/noxylva/lbjconsole/database/TrainDatabase.kt b/app/src/main/java/org/noxylva/lbjconsole/database/TrainDatabase.kt index 31bee06..f879fb3 100644 --- a/app/src/main/java/org/noxylva/lbjconsole/database/TrainDatabase.kt +++ b/app/src/main/java/org/noxylva/lbjconsole/database/TrainDatabase.kt @@ -9,7 +9,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase @Database( entities = [TrainRecordEntity::class, AppSettingsEntity::class], - version = 3, + version = 4, exportSchema = false ) 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 { return INSTANCE ?: synchronized(this) { val instance = Room.databaseBuilder( context.applicationContext, TrainDatabase::class.java, "train_database" - ).addMigrations(MIGRATION_1_2, MIGRATION_2_3).build() + ).addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4).build() INSTANCE = instance instance } diff --git a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MonitorScreen.kt b/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MonitorScreen.kt deleted file mode 100644 index c777eeb..0000000 --- a/app/src/main/java/org/noxylva/lbjconsole/ui/screens/MonitorScreen.kt +++ /dev/null @@ -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, - lastUpdateTime: Date?, - temporaryStatusMessage: String? = null, - onRecordClick: (TrainRecord) -> Unit, - onClearLog: () -> Unit -) { - var showDetailDialog by remember { mutableStateOf(false) } - var selectedRecord by remember { mutableStateOf(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(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 - ) - } -} \ No newline at end of file