feat: add record merging functionality and optimize settings page
This commit is contained in:
@@ -42,9 +42,12 @@ import org.json.JSONObject
|
||||
import org.osmdroid.config.Configuration
|
||||
import org.noxylva.lbjconsole.model.TrainRecord
|
||||
import org.noxylva.lbjconsole.model.TrainRecordManager
|
||||
import org.noxylva.lbjconsole.model.MergeSettings
|
||||
import org.noxylva.lbjconsole.ui.screens.HistoryScreen
|
||||
import org.noxylva.lbjconsole.ui.screens.MergedHistoryScreen
|
||||
import org.noxylva.lbjconsole.ui.screens.MapScreen
|
||||
import org.noxylva.lbjconsole.ui.screens.SettingsScreen
|
||||
|
||||
import org.noxylva.lbjconsole.ui.theme.LBJReceiverTheme
|
||||
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
||||
import java.util.*
|
||||
@@ -87,8 +90,13 @@ class MainActivity : ComponentActivity() {
|
||||
private var historyScrollOffset by mutableStateOf(0)
|
||||
private var mapCenterPosition by mutableStateOf<Pair<Double, Double>?>(null)
|
||||
private var mapZoomLevel by mutableStateOf(10.0)
|
||||
private var mapRailwayLayerVisible by mutableStateOf(true)
|
||||
private var mapRailwayLayerVisible by mutableStateOf(true)
|
||||
|
||||
private var settingsScrollPosition by mutableStateOf(0)
|
||||
|
||||
private var mergeSettings by mutableStateOf(MergeSettings())
|
||||
|
||||
|
||||
|
||||
private var targetDeviceName = "LBJReceiver"
|
||||
|
||||
@@ -236,7 +244,7 @@ class MainActivity : ComponentActivity() {
|
||||
isConnected = bleClient.isConnected(),
|
||||
isScanning = isScanning,
|
||||
currentTab = currentTab,
|
||||
onTabChange = { tab ->
|
||||
onTabChange = { tab ->
|
||||
currentTab = tab
|
||||
saveSettings()
|
||||
},
|
||||
@@ -256,12 +264,18 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
|
||||
|
||||
allRecords = if (trainRecordManager.getFilteredRecords().isNotEmpty())
|
||||
trainRecordManager.getFilteredRecords() else trainRecordManager.getAllRecords(),
|
||||
allRecords = trainRecordManager.getMixedRecords(),
|
||||
mergedRecords = trainRecordManager.getMergedRecords(),
|
||||
recordCount = trainRecordManager.getRecordCount(),
|
||||
filterTrain = filterTrain,
|
||||
filterRoute = filterRoute,
|
||||
filterDirection = filterDirection,
|
||||
mergeSettings = mergeSettings,
|
||||
onMergeSettingsChange = { newSettings ->
|
||||
mergeSettings = newSettings
|
||||
trainRecordManager.updateMergeSettings(newSettings)
|
||||
saveSettings()
|
||||
},
|
||||
|
||||
|
||||
historyEditMode = historyEditMode,
|
||||
@@ -279,6 +293,13 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
|
||||
|
||||
settingsScrollPosition = settingsScrollPosition,
|
||||
onSettingsScrollPositionChange = { position ->
|
||||
settingsScrollPosition = position
|
||||
saveSettings()
|
||||
},
|
||||
|
||||
|
||||
mapCenterPosition = mapCenterPosition,
|
||||
mapZoomLevel = mapZoomLevel,
|
||||
mapRailwayLayerVisible = mapRailwayLayerVisible,
|
||||
@@ -332,7 +353,6 @@ class MainActivity : ComponentActivity() {
|
||||
onApplySettings = {
|
||||
saveSettings()
|
||||
targetDeviceName = settingsDeviceName
|
||||
Toast.makeText(this, "设备名称 '${settingsDeviceName}' 已保存,下次连接时生效", Toast.LENGTH_LONG).show()
|
||||
Log.d(TAG, "Applied settings deviceName=${settingsDeviceName}")
|
||||
},
|
||||
appVersion = getAppVersion(),
|
||||
@@ -343,7 +363,7 @@ class MainActivity : ComponentActivity() {
|
||||
ConnectionDialog(
|
||||
isScanning = isScanning,
|
||||
devices = foundDevices,
|
||||
onDismiss = {
|
||||
onDismiss = {
|
||||
showConnectionDialog = false
|
||||
stopScan()
|
||||
},
|
||||
@@ -360,6 +380,8 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -585,6 +607,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
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)
|
||||
@@ -595,6 +618,8 @@ class MainActivity : ComponentActivity() {
|
||||
mapZoomLevel = settingsPrefs.getFloat("map_zoom_level", 10.0f).toDouble()
|
||||
mapRailwayLayerVisible = settingsPrefs.getBoolean("map_railway_visible", true)
|
||||
|
||||
mergeSettings = trainRecordManager.mergeSettings
|
||||
|
||||
Log.d(TAG, "Loaded settings deviceName=${settingsDeviceName} tab=${currentTab}")
|
||||
}
|
||||
|
||||
@@ -608,6 +633,7 @@ class MainActivity : ComponentActivity() {
|
||||
.putString("history_expanded_states", historyExpandedStates.map { "${it.key}:${it.value}" }.joinToString(";"))
|
||||
.putInt("history_scroll_position", historyScrollPosition)
|
||||
.putInt("history_scroll_offset", historyScrollOffset)
|
||||
.putInt("settings_scroll_position", settingsScrollPosition)
|
||||
.putFloat("map_zoom_level", mapZoomLevel.toFloat())
|
||||
.putBoolean("map_railway_visible", mapRailwayLayerVisible)
|
||||
|
||||
@@ -656,11 +682,14 @@ fun MainContent(
|
||||
onClearMonitorLog: () -> Unit,
|
||||
|
||||
|
||||
allRecords: List<TrainRecord>,
|
||||
allRecords: List<Any>,
|
||||
mergedRecords: List<org.noxylva.lbjconsole.model.MergedTrainRecord>,
|
||||
recordCount: Int,
|
||||
filterTrain: String,
|
||||
filterRoute: String,
|
||||
filterDirection: String,
|
||||
mergeSettings: MergeSettings,
|
||||
onMergeSettingsChange: (MergeSettings) -> Unit,
|
||||
onFilterChange: (String, String, String) -> Unit,
|
||||
onClearFilter: () -> Unit,
|
||||
onClearRecords: () -> Unit,
|
||||
@@ -685,6 +714,10 @@ fun MainContent(
|
||||
onHistoryStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit,
|
||||
|
||||
|
||||
settingsScrollPosition: Int,
|
||||
onSettingsScrollPositionChange: (Int) -> Unit,
|
||||
|
||||
|
||||
mapCenterPosition: Pair<Double, Double>?,
|
||||
mapZoomLevel: Double,
|
||||
mapRailwayLayerVisible: Boolean,
|
||||
@@ -770,8 +803,22 @@ fun MainContent(
|
||||
IconButton(
|
||||
onClick = {
|
||||
if (historySelectedRecords.isNotEmpty()) {
|
||||
val recordsToDelete = allRecords.filter {
|
||||
historySelectedRecords.contains(it.timestamp.time.toString())
|
||||
val recordsToDelete = mutableListOf<TrainRecord>()
|
||||
allRecords.forEach { item ->
|
||||
when (item) {
|
||||
is TrainRecord -> {
|
||||
if (historySelectedRecords.contains(item.timestamp.time.toString())) {
|
||||
recordsToDelete.add(item)
|
||||
}
|
||||
}
|
||||
is org.noxylva.lbjconsole.model.MergedTrainRecord -> {
|
||||
item.records.forEach { record ->
|
||||
if (historySelectedRecords.contains(record.timestamp.time.toString())) {
|
||||
recordsToDelete.add(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onDeleteRecords(recordsToDelete)
|
||||
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
||||
@@ -823,31 +870,42 @@ fun MainContent(
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
when (currentTab) {
|
||||
0 -> HistoryScreen(
|
||||
records = allRecords,
|
||||
latestRecord = latestRecord,
|
||||
lastUpdateTime = lastUpdateTime,
|
||||
temporaryStatusMessage = temporaryStatusMessage,
|
||||
locoInfoUtil = locoInfoUtil,
|
||||
onClearRecords = onClearRecords,
|
||||
onRecordClick = onRecordClick,
|
||||
onClearLog = onClearMonitorLog,
|
||||
onDeleteRecords = onDeleteRecords,
|
||||
editMode = historyEditMode,
|
||||
selectedRecords = historySelectedRecords,
|
||||
expandedStates = historyExpandedStates,
|
||||
scrollPosition = historyScrollPosition,
|
||||
scrollOffset = historyScrollOffset,
|
||||
onStateChange = onHistoryStateChange
|
||||
)
|
||||
0 -> {
|
||||
HistoryScreen(
|
||||
records = allRecords,
|
||||
latestRecord = latestRecord,
|
||||
lastUpdateTime = lastUpdateTime,
|
||||
temporaryStatusMessage = temporaryStatusMessage,
|
||||
locoInfoUtil = locoInfoUtil,
|
||||
mergeSettings = mergeSettings,
|
||||
onClearRecords = onClearRecords,
|
||||
onRecordClick = onRecordClick,
|
||||
onClearLog = onClearMonitorLog,
|
||||
onDeleteRecords = onDeleteRecords,
|
||||
editMode = historyEditMode,
|
||||
selectedRecords = historySelectedRecords,
|
||||
expandedStates = historyExpandedStates,
|
||||
scrollPosition = historyScrollPosition,
|
||||
scrollOffset = historyScrollOffset,
|
||||
onStateChange = onHistoryStateChange
|
||||
)
|
||||
}
|
||||
2 -> SettingsScreen(
|
||||
deviceName = deviceName,
|
||||
onDeviceNameChange = onDeviceNameChange,
|
||||
onApplySettings = onApplySettings,
|
||||
appVersion = appVersion
|
||||
appVersion = appVersion,
|
||||
mergeSettings = mergeSettings,
|
||||
onMergeSettingsChange = onMergeSettingsChange,
|
||||
scrollPosition = settingsScrollPosition,
|
||||
onScrollPositionChange = onSettingsScrollPositionChange
|
||||
)
|
||||
3 -> MapScreen(
|
||||
records = if (allRecords.isNotEmpty()) allRecords else recentRecords,
|
||||
records = if (allRecords.isNotEmpty()) {
|
||||
allRecords.filterIsInstance<TrainRecord>()
|
||||
} else {
|
||||
recentRecords
|
||||
},
|
||||
centerPosition = mapCenterPosition,
|
||||
zoomLevel = mapZoomLevel,
|
||||
railwayLayerVisible = mapRailwayLayerVisible,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package org.noxylva.lbjconsole.model
|
||||
|
||||
data class MergeSettings(
|
||||
val enabled: Boolean = true,
|
||||
val groupBy: GroupBy = GroupBy.TRAIN_AND_LOCO,
|
||||
val timeWindow: TimeWindow = TimeWindow.UNLIMITED
|
||||
)
|
||||
|
||||
enum class GroupBy(val displayName: String) {
|
||||
TRAIN_AND_LOCO("车次号+机车号"),
|
||||
TRAIN_ONLY("仅车次号"),
|
||||
LOCO_ONLY("仅机车号")
|
||||
}
|
||||
|
||||
enum class TimeWindow(val displayName: String, val seconds: Long?) {
|
||||
ONE_HOUR("1小时", 3600),
|
||||
TWO_HOURS("2小时", 7200),
|
||||
SIX_HOURS("6小时", 21600),
|
||||
TWELVE_HOURS("12小时", 43200),
|
||||
ONE_DAY("24小时", 86400),
|
||||
UNLIMITED("不限时间", null)
|
||||
}
|
||||
|
||||
fun generateGroupKey(record: TrainRecord, groupBy: GroupBy): String? {
|
||||
return when (groupBy) {
|
||||
GroupBy.TRAIN_AND_LOCO -> {
|
||||
val train = record.train.trim()
|
||||
val loco = record.loco.trim()
|
||||
if (train.isNotEmpty() && train != "<NUL>" &&
|
||||
loco.isNotEmpty() && loco != "<NUL>") {
|
||||
"${train}_${loco}"
|
||||
} else null
|
||||
}
|
||||
GroupBy.TRAIN_ONLY -> {
|
||||
val train = record.train.trim()
|
||||
if (train.isNotEmpty() && train != "<NUL>") train else null
|
||||
}
|
||||
GroupBy.LOCO_ONLY -> {
|
||||
val loco = record.loco.trim()
|
||||
if (loco.isNotEmpty() && loco != "<NUL>") loco else null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.noxylva.lbjconsole.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
data class MergedTrainRecord(
|
||||
val groupKey: String,
|
||||
val records: List<TrainRecord>,
|
||||
val latestRecord: TrainRecord
|
||||
) {
|
||||
val recordCount: Int get() = records.size
|
||||
val timeSpan: Pair<Date, Date> get() =
|
||||
records.minByOrNull { it.timestamp }!!.timestamp to
|
||||
records.maxByOrNull { it.timestamp }!!.timestamp
|
||||
|
||||
fun getAllCoordinates() = records.mapNotNull { it.getCoordinates() }
|
||||
|
||||
fun getUniqueRoutes() = records.map { it.route }.filter { it.isNotEmpty() && it != "<NUL>" }.toSet()
|
||||
|
||||
fun getUniquePositions() = records.map { it.position }.filter { it.isNotEmpty() && it != "<NUL>" }.toSet()
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class TrainRecordManager(private val context: Context) {
|
||||
const val MAX_RECORDS = 1000
|
||||
private const val PREFS_NAME = "train_records"
|
||||
private const val KEY_RECORDS = "records"
|
||||
private const val KEY_MERGE_SETTINGS = "merge_settings"
|
||||
}
|
||||
|
||||
|
||||
@@ -26,8 +27,12 @@ class TrainRecordManager(private val context: Context) {
|
||||
private val recordCount = AtomicInteger(0)
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
var mergeSettings = MergeSettings()
|
||||
private set
|
||||
|
||||
init {
|
||||
loadRecords()
|
||||
loadMergeSettings()
|
||||
}
|
||||
|
||||
|
||||
@@ -177,4 +182,110 @@ class TrainRecordManager(private val context: Context) {
|
||||
fun getRecordCount(): Int {
|
||||
return recordCount.get()
|
||||
}
|
||||
|
||||
fun updateMergeSettings(newSettings: MergeSettings) {
|
||||
mergeSettings = newSettings
|
||||
saveMergeSettings()
|
||||
}
|
||||
|
||||
|
||||
fun getMergedRecords(): List<MergedTrainRecord> {
|
||||
if (!mergeSettings.enabled) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val records = getFilteredRecords()
|
||||
return processRecordsForMerging(records, mergeSettings)
|
||||
}
|
||||
|
||||
fun getMixedRecords(): List<Any> {
|
||||
if (!mergeSettings.enabled) {
|
||||
return getFilteredRecords()
|
||||
}
|
||||
|
||||
val allRecords = getFilteredRecords()
|
||||
val mergedRecords = processRecordsForMerging(allRecords, mergeSettings)
|
||||
|
||||
val mergedRecordIds = mergedRecords.flatMap { merged ->
|
||||
merged.records.map { it.timestamp.time.toString() }
|
||||
}.toSet()
|
||||
|
||||
val singleRecords = allRecords.filter { record ->
|
||||
!mergedRecordIds.contains(record.timestamp.time.toString())
|
||||
}
|
||||
|
||||
val mixedList = mutableListOf<Any>()
|
||||
mixedList.addAll(mergedRecords)
|
||||
mixedList.addAll(singleRecords)
|
||||
|
||||
return mixedList.sortedByDescending { item ->
|
||||
when (item) {
|
||||
is MergedTrainRecord -> item.latestRecord.timestamp
|
||||
is TrainRecord -> item.timestamp
|
||||
else -> Date(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processRecordsForMerging(records: List<TrainRecord>, settings: MergeSettings): List<MergedTrainRecord> {
|
||||
val groupedRecords = mutableMapOf<String, MutableList<TrainRecord>>()
|
||||
val currentTime = Date()
|
||||
|
||||
records.forEach { record ->
|
||||
val groupKey = generateGroupKey(record, settings.groupBy)
|
||||
if (groupKey != null) {
|
||||
val withinTimeWindow = settings.timeWindow.seconds?.let { windowSeconds ->
|
||||
(currentTime.time - record.timestamp.time) / 1000 <= windowSeconds
|
||||
} ?: true
|
||||
|
||||
if (withinTimeWindow) {
|
||||
groupedRecords.getOrPut(groupKey) { mutableListOf() }.add(record)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupedRecords.mapNotNull { (groupKey, groupRecords) ->
|
||||
if (groupRecords.size >= 2) {
|
||||
val sortedRecords = groupRecords.sortedBy { it.timestamp }
|
||||
val latestRecord = sortedRecords.maxByOrNull { it.timestamp }!!
|
||||
MergedTrainRecord(
|
||||
groupKey = groupKey,
|
||||
records = sortedRecords,
|
||||
latestRecord = latestRecord
|
||||
)
|
||||
} else null
|
||||
}.sortedByDescending { it.latestRecord.timestamp }
|
||||
}
|
||||
|
||||
private fun saveMergeSettings() {
|
||||
try {
|
||||
val json = JSONObject().apply {
|
||||
put("enabled", mergeSettings.enabled)
|
||||
put("groupBy", mergeSettings.groupBy.name)
|
||||
put("timeWindow", mergeSettings.timeWindow.name)
|
||||
}
|
||||
prefs.edit().putString(KEY_MERGE_SETTINGS, json.toString()).apply()
|
||||
Log.d(TAG, "Saved merge settings")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to save merge settings: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadMergeSettings() {
|
||||
try {
|
||||
val jsonStr = prefs.getString(KEY_MERGE_SETTINGS, null)
|
||||
if (jsonStr != null) {
|
||||
val json = JSONObject(jsonStr)
|
||||
mergeSettings = MergeSettings(
|
||||
enabled = json.getBoolean("enabled"),
|
||||
groupBy = GroupBy.valueOf(json.getString("groupBy")),
|
||||
timeWindow = TimeWindow.valueOf(json.getString("timeWindow"))
|
||||
)
|
||||
}
|
||||
Log.d(TAG, "Loaded merge settings: $mergeSettings")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load merge settings: ${e.message}")
|
||||
mergeSettings = MergeSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,490 @@
|
||||
package org.noxylva.lbjconsole.ui.screens
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import org.osmdroid.tileprovider.MapTileProviderBasic
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory
|
||||
import org.osmdroid.tileprovider.tilesource.XYTileSource
|
||||
import org.osmdroid.views.MapView
|
||||
import org.osmdroid.views.overlay.Marker
|
||||
import org.osmdroid.views.overlay.Polyline
|
||||
import org.osmdroid.views.overlay.TilesOverlay
|
||||
import org.noxylva.lbjconsole.model.MergedTrainRecord
|
||||
import org.noxylva.lbjconsole.model.TrainRecord
|
||||
import org.noxylva.lbjconsole.util.LocoInfoUtil
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MergedHistoryScreen(
|
||||
mergedRecords: List<MergedTrainRecord>,
|
||||
latestRecord: TrainRecord?,
|
||||
lastUpdateTime: Date?,
|
||||
temporaryStatusMessage: String? = null,
|
||||
locoInfoUtil: LocoInfoUtil? = null,
|
||||
onClearRecords: () -> Unit = {},
|
||||
onRecordClick: (TrainRecord) -> Unit = {},
|
||||
onClearLog: () -> Unit = {},
|
||||
onDeleteRecords: (List<TrainRecord>) -> Unit = {},
|
||||
onDeleteMergedRecord: (MergedTrainRecord) -> Unit = {},
|
||||
editMode: Boolean = false,
|
||||
selectedRecords: Set<String> = emptySet(),
|
||||
expandedStates: Map<String, Boolean> = emptyMap(),
|
||||
scrollPosition: Int = 0,
|
||||
scrollOffset: Int = 0,
|
||||
onStateChange: (Boolean, Set<String>, Map<String, Boolean>, Int, Int) -> Unit = { _, _, _, _, _ -> }
|
||||
) {
|
||||
var isInEditMode by remember(editMode) { mutableStateOf(editMode) }
|
||||
val selectedRecordsList = remember(selectedRecords) {
|
||||
mutableStateListOf<TrainRecord>().apply {
|
||||
addAll(mergedRecords.flatMap { it.records }.filter {
|
||||
selectedRecords.contains(it.timestamp.time.toString())
|
||||
})
|
||||
}
|
||||
}
|
||||
val expandedStatesMap = remember(expandedStates) {
|
||||
mutableStateMapOf<String, Boolean>().apply { putAll(expandedStates) }
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState(
|
||||
initialFirstVisibleItemIndex = scrollPosition,
|
||||
initialFirstVisibleItemScrollOffset = scrollOffset
|
||||
)
|
||||
|
||||
LaunchedEffect(isInEditMode, selectedRecordsList.size) {
|
||||
val selectedIds = selectedRecordsList.map { it.timestamp.time.toString() }.toSet()
|
||||
onStateChange(isInEditMode, selectedIds, expandedStatesMap.toMap(),
|
||||
listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset)
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.weight(1.0f)
|
||||
) {
|
||||
if (mergedRecords.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
"暂无合并记录",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.outline
|
||||
)
|
||||
Text(
|
||||
"请检查合并设置或等待更多数据",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.7f)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(mergedRecords) { mergedRecord ->
|
||||
MergedRecordCard(
|
||||
mergedRecord = mergedRecord,
|
||||
isExpanded = expandedStatesMap[mergedRecord.groupKey] == true,
|
||||
onExpandToggle = {
|
||||
expandedStatesMap[mergedRecord.groupKey] =
|
||||
!(expandedStatesMap[mergedRecord.groupKey] ?: false)
|
||||
},
|
||||
locoInfoUtil = locoInfoUtil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun MergedRecordCard(
|
||||
mergedRecord: MergedTrainRecord,
|
||||
isExpanded: Boolean,
|
||||
onExpandToggle: () -> Unit,
|
||||
locoInfoUtil: LocoInfoUtil?
|
||||
) {
|
||||
val record = mergedRecord.latestRecord
|
||||
val recordMap = record.toMap(showDetailedTime = true)
|
||||
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface
|
||||
),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = onExpandToggle,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true)
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (recordMap.containsKey("time")) {
|
||||
Column {
|
||||
recordMap["time"]?.split("\n")?.forEach { timeLine ->
|
||||
Text(
|
||||
text = timeLine,
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "${record.rssi} dBm",
|
||||
fontSize = 10.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val trainDisplay = recordMap["train"]?.toString() ?: "未知列车"
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
Text(
|
||||
text = trainDisplay,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
val directionText = when (record.direction) {
|
||||
1 -> "下"
|
||||
3 -> "上"
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (directionText.isNotEmpty()) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(2.dp),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.size(20.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = directionText,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.offset(y = (-2).dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val formattedInfo = when {
|
||||
record.locoType.isNotEmpty() && record.loco.isNotEmpty() -> {
|
||||
val shortLoco = if (record.loco.length > 5) {
|
||||
record.loco.takeLast(5)
|
||||
} else {
|
||||
record.loco
|
||||
}
|
||||
"${record.locoType}-${shortLoco}"
|
||||
}
|
||||
record.locoType.isNotEmpty() -> record.locoType
|
||||
record.loco.isNotEmpty() -> record.loco
|
||||
else -> ""
|
||||
}
|
||||
|
||||
if (formattedInfo.isNotEmpty() && formattedInfo != "<NUL>") {
|
||||
Text(
|
||||
text = formattedInfo,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val routeStr = record.route.trim()
|
||||
val isValidRoute = routeStr.isNotEmpty() && !routeStr.all { it == '*' }
|
||||
|
||||
val position = record.position.trim()
|
||||
val isValidPosition = position.isNotEmpty() &&
|
||||
!position.all { it == '-' || it == '.' } &&
|
||||
position != "<NUL>"
|
||||
|
||||
if (isValidRoute || isValidPosition) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.height(24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
if (isValidRoute) {
|
||||
Text(
|
||||
text = "$routeStr",
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.alignByBaseline()
|
||||
)
|
||||
}
|
||||
|
||||
if (isValidPosition) {
|
||||
Text(
|
||||
text = "${position}K",
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.alignByBaseline()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val speed = record.speed.trim()
|
||||
val isValidSpeed = speed.isNotEmpty() &&
|
||||
!speed.all { it == '*' || it == '-' } &&
|
||||
speed != "NUL" &&
|
||||
speed != "<NUL>"
|
||||
if (isValidSpeed) {
|
||||
Text(
|
||||
text = "${speed} km/h",
|
||||
fontSize = 16.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (locoInfoUtil != null && record.locoType.isNotEmpty() && record.loco.isNotEmpty()) {
|
||||
val locoInfoText = locoInfoUtil.getLocoInfoDisplay(record.locoType, record.loco)
|
||||
if (locoInfoText != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = locoInfoText,
|
||||
fontSize = 14.sp,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isExpanded) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
"记录详情",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
mergedRecord.records.forEach { recordItem ->
|
||||
val timeFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp)
|
||||
) {
|
||||
Text(
|
||||
text = timeFormat.format(recordItem.timestamp),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
val locationText = buildString {
|
||||
if (recordItem.route.isNotEmpty() && recordItem.route != "<NUL>") {
|
||||
append(recordItem.route)
|
||||
}
|
||||
if (recordItem.position.isNotEmpty() && recordItem.position != "<NUL>") {
|
||||
if (isNotEmpty()) append(" ")
|
||||
append("${recordItem.position}K")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = locationText.ifEmpty { "位置未知" },
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
val directionText = when (recordItem.direction) {
|
||||
1 -> "下行"
|
||||
3 -> "上行"
|
||||
else -> "未知"
|
||||
}
|
||||
Text(
|
||||
text = directionText,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
val speedText = if (recordItem.speed.isNotEmpty() &&
|
||||
recordItem.speed != "<NUL>" &&
|
||||
!recordItem.speed.all { it == '*' || it == '-' }) {
|
||||
"${recordItem.speed}km/h"
|
||||
} else {
|
||||
"速度未知"
|
||||
}
|
||||
Text(
|
||||
text = speedText,
|
||||
fontSize = 11.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val coordinates = mergedRecord.getAllCoordinates()
|
||||
val recordsWithCoordinates = mergedRecord.records.filter { it.getCoordinates() != null }
|
||||
|
||||
if (coordinates.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
"行进路径 (${coordinates.size}/${mergedRecord.records.size} 个记录有位置信息)",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(220.dp)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
MapView(context).apply {
|
||||
setTileSource(TileSourceFactory.MAPNIK)
|
||||
setMultiTouchControls(true)
|
||||
zoomController.setVisibility(org.osmdroid.views.CustomZoomButtonsController.Visibility.NEVER)
|
||||
|
||||
try {
|
||||
val railwayTileSource = XYTileSource(
|
||||
"OpenRailwayMap", 8, 16, 256, ".png",
|
||||
arrayOf(
|
||||
"https://a.tiles.openrailwaymap.org/standard/",
|
||||
"https://b.tiles.openrailwaymap.org/standard/",
|
||||
"https://c.tiles.openrailwaymap.org/standard/"
|
||||
),
|
||||
"© OpenRailwayMap contributors, © OpenStreetMap contributors"
|
||||
)
|
||||
val railwayProvider = MapTileProviderBasic(context)
|
||||
railwayProvider.tileSource = railwayTileSource
|
||||
val railwayOverlay = TilesOverlay(railwayProvider, context)
|
||||
overlays.add(railwayOverlay)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
if (coordinates.size > 1) {
|
||||
val polyline = Polyline().apply {
|
||||
setPoints(coordinates)
|
||||
outlinePaint.color = android.graphics.Color.BLUE
|
||||
outlinePaint.strokeWidth = 5f
|
||||
}
|
||||
overlays.add(polyline)
|
||||
}
|
||||
|
||||
coordinates.forEachIndexed { index, coord ->
|
||||
val marker = Marker(this).apply {
|
||||
position = coord
|
||||
title = when (index) {
|
||||
0 -> "起点"
|
||||
coordinates.lastIndex -> "终点"
|
||||
else -> "经过点 ${index + 1}"
|
||||
}
|
||||
}
|
||||
overlays.add(marker)
|
||||
}
|
||||
|
||||
val centerLat = coordinates.map { it.latitude }.average()
|
||||
val centerLon = coordinates.map { it.longitude }.average()
|
||||
controller.setCenter(org.osmdroid.util.GeoPoint(centerLat, centerLon))
|
||||
controller.setZoom(12.0)
|
||||
}
|
||||
},
|
||||
update = { mapView ->
|
||||
mapView.invalidate()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,24 @@ package org.noxylva.lbjconsole.ui.screens
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.noxylva.lbjconsole.model.MergeSettings
|
||||
import org.noxylva.lbjconsole.model.GroupBy
|
||||
import org.noxylva.lbjconsole.model.TimeWindow
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -15,44 +27,220 @@ fun SettingsScreen(
|
||||
deviceName: String,
|
||||
onDeviceNameChange: (String) -> Unit,
|
||||
onApplySettings: () -> Unit,
|
||||
appVersion: String = "Unknown"
|
||||
appVersion: String = "Unknown",
|
||||
mergeSettings: MergeSettings,
|
||||
onMergeSettingsChange: (MergeSettings) -> Unit,
|
||||
scrollPosition: Int = 0,
|
||||
onScrollPositionChange: (Int) -> Unit = {}
|
||||
) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
LaunchedEffect(scrollPosition) {
|
||||
scrollState.scrollTo(scrollPosition)
|
||||
}
|
||||
|
||||
LaunchedEffect(scrollState.value) {
|
||||
onScrollPositionChange(scrollState.value)
|
||||
}
|
||||
|
||||
LaunchedEffect(deviceName) {
|
||||
onApplySettings()
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.verticalScroll(scrollState)
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp)
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = deviceName,
|
||||
onValueChange = onDeviceNameChange,
|
||||
label = { Text("蓝牙设备名称") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onApplySettings,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text("应用设置")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bluetooth,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"蓝牙设备",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = deviceName,
|
||||
onValueChange = onDeviceNameChange,
|
||||
label = { Text("设备名称") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeviceHub,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
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.MergeType,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
"记录合并",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
"启用记录合并",
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
Switch(
|
||||
checked = mergeSettings.enabled,
|
||||
onCheckedChange = { enabled ->
|
||||
onMergeSettingsChange(mergeSettings.copy(enabled = enabled))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (mergeSettings.enabled) {
|
||||
var groupByExpanded by remember { mutableStateOf(false) }
|
||||
var timeWindowExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = groupByExpanded,
|
||||
onExpandedChange = { groupByExpanded = !groupByExpanded }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = mergeSettings.groupBy.displayName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("分组方式") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Group,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = groupByExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = groupByExpanded,
|
||||
onDismissRequest = { groupByExpanded = false }
|
||||
) {
|
||||
GroupBy.values().forEach { groupBy ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(groupBy.displayName) },
|
||||
onClick = {
|
||||
onMergeSettingsChange(mergeSettings.copy(groupBy = groupBy))
|
||||
groupByExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = timeWindowExpanded,
|
||||
onExpandedChange = { timeWindowExpanded = !timeWindowExpanded }
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = mergeSettings.timeWindow.displayName,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("时间窗口") },
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Schedule,
|
||||
contentDescription = null
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = timeWindowExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.menuAnchor(),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = timeWindowExpanded,
|
||||
onDismissRequest = { timeWindowExpanded = false }
|
||||
) {
|
||||
TimeWindow.values().forEach { timeWindow ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(timeWindow.displayName) },
|
||||
onClick = {
|
||||
onMergeSettingsChange(mergeSettings.copy(timeWindow = timeWindow))
|
||||
timeWindowExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Text(
|
||||
text = "LBJ Console v$appVersion by undef-i",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.clickable {
|
||||
uriHandler.openUri("https://github.com/undef-i")
|
||||
}
|
||||
)
|
||||
text = "LBJ Console v$appVersion by undef-i",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.clickable {
|
||||
uriHandler.openUri("https://github.com/undef-i")
|
||||
}
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user