fix: correct incorrect rendering and status of map marker points
4
.gitignore
vendored
@@ -13,10 +13,10 @@ captures
|
|||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
.cxx
|
.cxx
|
||||||
local.properties
|
local.properties
|
||||||
local.properties
|
|
||||||
*.ps1
|
*.ps1
|
||||||
.*.bat
|
.*.bat
|
||||||
*.jks
|
*.jks
|
||||||
*.keystore
|
*.keystore
|
||||||
*.base64
|
*.base64
|
||||||
docs
|
docs
|
||||||
|
gradle.properties
|
||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "org.noxylva.lbjconsole"
|
applicationId = "org.noxylva.lbjconsole"
|
||||||
minSdk = 29
|
minSdk = 29
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 9
|
versionCode = 10
|
||||||
versionName = "0.1.0"
|
versionName = "0.1.1"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,7 @@ android {
|
|||||||
}
|
}
|
||||||
lint {
|
lint {
|
||||||
disable += "NullSafeMutableLiveData"
|
disable += "NullSafeMutableLiveData"
|
||||||
|
warning += "MissingPermission"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ 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
|
||||||
@@ -108,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)
|
||||||
@@ -365,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()
|
||||||
@@ -932,9 +936,10 @@ fun MainContent(
|
|||||||
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,
|
||||||
@@ -1040,7 +1045,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,
|
||||||
@@ -1086,7 +1091,7 @@ fun MainContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
onDeleteRecords(recordsToDelete.toList())
|
onDeleteRecords(recordsToDelete.toList())
|
||||||
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyScrollPosition, historyScrollOffset)
|
onHistoryStateChange(false, emptySet(), historyExpandedStates, historyMapViewStates, historyScrollPosition, historyScrollOffset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
@@ -1151,6 +1156,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
|
||||||
|
|||||||
@@ -43,8 +43,14 @@ 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.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
|
||||||
@@ -328,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 {
|
||||||
@@ -409,6 +416,8 @@ fun MergedTrainRecordItem(
|
|||||||
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
|
||||||
@@ -659,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)
|
||||||
|
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 = 14.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")) {
|
||||||
@@ -903,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
|
||||||
@@ -935,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,
|
||||||
@@ -965,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1092,6 +1164,10 @@ fun HistoryScreen(
|
|||||||
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,6 @@ 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
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -195,23 +194,14 @@ 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 { mutableStateOf(false) }
|
var backgroundServiceEnabled by remember { mutableStateOf<Boolean?>(null) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
LaunchedEffect(context) {
|
LaunchedEffect(context) {
|
||||||
backgroundServiceEnabled = SettingsActivity.isBackgroundServiceEnabled(context)
|
backgroundServiceEnabled = SettingsActivity.isBackgroundServiceEnabled(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(backgroundServiceEnabled) {
|
|
||||||
SettingsActivity.setBackgroundServiceEnabled(context, backgroundServiceEnabled)
|
|
||||||
|
|
||||||
if (backgroundServiceEnabled) {
|
|
||||||
BackgroundService.startService(context)
|
|
||||||
} else {
|
|
||||||
BackgroundService.stopService(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var notificationEnabled by remember(context, notificationService) {
|
var notificationEnabled by remember(context, notificationService) {
|
||||||
mutableStateOf(notificationService.isNotificationEnabled())
|
mutableStateOf(notificationService.isNotificationEnabled())
|
||||||
}
|
}
|
||||||
@@ -233,12 +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(
|
||||||
}
|
checked = backgroundServiceEnabled!!,
|
||||||
)
|
onCheckedChange = { enabled ->
|
||||||
|
backgroundServiceEnabled = enabled
|
||||||
|
coroutineScope.launch {
|
||||||
|
SettingsActivity.setBackgroundServiceEnabled(context, enabled)
|
||||||
|
if (enabled) {
|
||||||
|
BackgroundService.startService(context)
|
||||||
|
} else {
|
||||||
|
BackgroundService.stopService(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -436,4 +438,4 @@ fun SettingsScreen(
|
|||||||
.padding(12.dp)
|
.padding(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
@@ -20,4 +20,4 @@ kotlin.code.style=official
|
|||||||
# Enables namespacing of each library's R class so that its R class includes only the
|
# Enables namespacing of each library's R class so that its R class includes only the
|
||||||
# resources declared in the library itself and none from the library's dependencies,
|
# resources declared in the library itself and none from the library's dependencies,
|
||||||
# thereby reducing the size of the R class for that library
|
# thereby reducing the size of the R class for that library
|
||||||
android.nonTransitiveRClass=true
|
android.nonTransitiveRClass=true
|
||||||
|
|||||||
@@ -19,6 +19,6 @@ dependencyResolutionManagement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = "LBJ Receiver"
|
rootProject.name = "LBJ_Console"
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
||||||