From 4da59bdd17b12b6982121fdd9db30ff2b3273c33 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Wed, 23 Sep 2026 13:14:38 +0800 Subject: [PATCH] feat(map): mark a station you want to contact in unworked grids - New MarkedStation model + ISettingsRepo/SettingsRepo persistence (single JSON key markedGridStations, org.json only) - MapState/MapViewModel: markedGrids state, Set/RemoveMarkedStation actions; marks auto-clear once the grid becomes worked - MaidenheadGridOverlay: red fill (alpha 90, same as green worked fill) for marked grids at both zoom levels; first-call label mode shows the marked callsign in pale yellow on red cells - MapScreen: unworked-grid dialog gains a 'mark a station you want to contact' button + callsign input dialog; marked row shows red callsign, 'Marked' in the count column, marking date, delete icon - i18n: values/ + values-zh/ (grid_mark_*) --- .../core/data/repository/SettingsRepo.kt | 37 +++++ .../core/data/repository/DatabaseRepoTest.kt | 4 + .../repository/SelectionRepoSearchTest.kt | 2 + .../core/domain/model/MarkedStation.kt | 31 ++++ .../core/domain/repository/ISettingsRepo.kt | 5 + .../domain/repository/LoTWGridSyncTest.kt | 4 + .../src/main/res/values-zh/strings.xml | 6 + .../src/main/res/values/strings.xml | 6 + .../feature/map/MaidenheadGridOverlay.kt | 40 ++++- .../look4sat/feature/map/MapScreen.kt | 144 ++++++++++++++++-- .../rtbishop/look4sat/feature/map/MapState.kt | 4 + .../look4sat/feature/map/MapViewModel.kt | 26 +++- 12 files changed, 288 insertions(+), 21 deletions(-) create mode 100644 core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/MarkedStation.kt diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt index 7efca483..66de088d 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt @@ -269,6 +269,43 @@ class SettingsRepo( preferences.edit { putString(keyRoamedGrids, array.toString()) } } + // Marked stations in unworked gridsquares, persisted as a single JSON + // object: {"OL62":{"c":call,"t":epochMs}}. One mark per grid. + private val keyMarkedGridStations = "markedGridStations" + + override fun getMarkedGridStations(): Map { + val json = preferences.getString(keyMarkedGridStations, null).orEmpty() + if (json.isBlank()) return emptyMap() + return try { + val root = org.json.JSONObject(json) + val result = mutableMapOf() + for (grid in root.keys()) { + val o = root.optJSONObject(grid) ?: continue + val call = o.optString("c").ifBlank { continue } + result[grid] = com.rtbishop.look4sat.core.domain.model.MarkedStation( + call = call, + epochMs = o.optLong("t") + ) + } + result + } catch (_: Exception) { + emptyMap() + } + } + + override fun setMarkedGridStations(stations: Map) { + val root = org.json.JSONObject() + for ((grid, station) in stations) { + root.put( + grid, + org.json.JSONObject() + .put("c", station.call) + .put("t", station.epochMs) + ) + } + preferences.edit { putString(keyMarkedGridStations, root.toString()) } + } + // LoTW credentials (stored locally on the device only) private val keyLoTWCall = "lotwCallsign" private val keyLoTWPass = "lotwPassword" diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt index 7c54b585..40bb2c7a 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/DatabaseRepoTest.kt @@ -312,6 +312,10 @@ private class FakeSettingsRepo(dataSources: DataSourcesSettings = defaultDataSou override fun setRoamedGrids(grids: Set) { roamedGrids.value = grids } + + override fun getMarkedGridStations(): Map = emptyMap() + + override fun setMarkedGridStations(stations: Map) = Unit } private fun defaultDataSourcesSettings(): DataSourcesSettings { diff --git a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt index 2b559562..b17933cf 100644 --- a/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt +++ b/core/data/src/test/java/com/rtbishop/look4sat/core/data/repository/SelectionRepoSearchTest.kt @@ -189,6 +189,8 @@ private class FakeSettingsRepoForSearch : ISettingsRepo { override fun setWorkedGridQsos(qsos: Map>) = Unit override fun getRoamedGrids(): Set = emptySet() override fun setRoamedGrids(grids: Set) = Unit + override fun getMarkedGridStations(): Map = emptyMap() + override fun setMarkedGridStations(stations: Map) = Unit override fun updateLoTWSettings(settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings) = Unit override fun getLastLotwSyncDate(): String = "" override fun setLastLotwSyncDate(date: String) = Unit diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/MarkedStation.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/MarkedStation.kt new file mode 100644 index 00000000..db9039d8 --- /dev/null +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/MarkedStation.kt @@ -0,0 +1,31 @@ +/* + * Look4Sat. Amateur radio satellite tracker and pass predictor. + * Copyright (C) 2019-2026 Arty Bishop and contributors. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.rtbishop.look4sat.core.domain.model + +/** + * A station the user marked in a gridsquare they have NOT yet worked — a + * reminder to try to contact that callsign. Stored per 4-char grid (one mark + * per grid). + * + * @param call the callsign the user wants to work (uppercase) + * @param epochMs the moment the mark was created, UTC milliseconds + */ +data class MarkedStation( + val call: String, + val epochMs: Long +) diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt index b1961590..af88f8be 100644 --- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt +++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/repository/ISettingsRepo.kt @@ -105,6 +105,11 @@ interface ISettingsRepo { /** Distinct 4-char gridsquares the account operated from (LoTW ). */ fun getRoamedGrids(): Set fun setRoamedGrids(grids: Set) + /** Stations the user marked as "want to contact" in unworked gridsquares + * (grid -> mark). One mark per grid; cleared automatically when the grid + * becomes worked. */ + fun getMarkedGridStations(): Map + fun setMarkedGridStations(stations: Map) //endregion //region # LoTW confirmed-grids settings diff --git a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt index b64fda82..580a55ca 100644 --- a/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt +++ b/core/domain/src/test/java/com/rtbishop/look4sat/core/domain/repository/LoTWGridSyncTest.kt @@ -21,6 +21,7 @@ import com.rtbishop.look4sat.core.domain.model.DatabaseState import com.rtbishop.look4sat.core.domain.model.DataSourcesSettings import com.rtbishop.look4sat.core.domain.model.GridQso import com.rtbishop.look4sat.core.domain.model.LoTWSettings +import com.rtbishop.look4sat.core.domain.model.MarkedStation import com.rtbishop.look4sat.core.domain.model.OtherSettings import com.rtbishop.look4sat.core.domain.model.PassesSettings import com.rtbishop.look4sat.core.domain.model.RCSettings @@ -261,6 +262,9 @@ class LoTWGridSyncTest { override fun setWorkedGridQsos(qsos: Map>) { workedGridQsos = qsos } override fun getRoamedGrids(): Set = roamedGrids override fun setRoamedGrids(grids: Set) { roamedGrids = grids } + private var markedGridStations: Map = emptyMap() + override fun getMarkedGridStations(): Map = markedGridStations + override fun setMarkedGridStations(stations: Map) { markedGridStations = stations } override fun getLastLotwSyncDate(): String = lastSyncDate override fun setLastLotwSyncDate(date: String) { lastSyncDate = date } override fun getLastLotwSyncCallsign(): String = lastSyncCallsign diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 061fc662..ee69950a 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -131,6 +131,12 @@ 首通 卫星未知 匹配 + + 标记你想通联的电台 + 呼号 + 标记 + 标记 + 删除标记 日照中 diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index ec9505e2..0972b70e 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -151,6 +151,12 @@ First satellite unknown Match + + Mark a station you want to contact + Callsign + Mark + Marked + Remove mark Visible diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt index d188d08b..b694b8d8 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MaidenheadGridOverlay.kt @@ -64,6 +64,11 @@ class MaidenheadGridOverlay : Overlay() { style = android.graphics.Paint.Style.FILL color = Color.argb(90, 76, 217, 100) } + // 标记(想通联)网格填充: 红色, 透明度与绿色 workedPaint 一致(alpha 90). + private val markedPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = android.graphics.Paint.Style.FILL + color = Color.argb(90, 255, 59, 48) + } private val roamStripePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.STROKE strokeWidth = 14f @@ -87,6 +92,11 @@ class MaidenheadGridOverlay : Overlay() { /** Worked gridsquares (4-char, uppercase) to highlight, e.g. {"OL62", "PM95"}. */ var workedGrids: Set = emptySet() + /** Marked gridsquares (4-char, uppercase) — unworked grids the user has + * marked a station they want to contact in; drawn red, same alpha as the + * green worked fill. Value carries the marked callsign for label mode. */ + var markedGrids: Map = emptyMap() + /** Grid-mode first-call labels: label worked (green) cells with the first * callsign worked in that grid instead of the Maidenhead code; non-worked * cells get no label at all. Only meaningful at sub-square zoom. */ @@ -189,6 +199,9 @@ class MaidenheadGridOverlay : Overlay() { if (cellLabel(lat, lon, zoom) in workedGrids) { canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint) } + if (cellLabel(lat, lon, zoom) in markedGrids) { + canvas.drawRect(xLeft, yTop, xRight, yBottom, markedPaint) + } } } } else { @@ -215,6 +228,22 @@ class MaidenheadGridOverlay : Overlay() { canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint) } } + for (grid in markedGrids.keys) { + val cell = gridCellBounds(grid) ?: continue + for (turn in -colRepeats..colRepeats) { + val dLon = turn * 360.0 + if (cell.lonRight + dLon <= leftLon || cell.lonLeft + dLon >= rightLon) continue + val yTop = projectionToY(projection, cell.latTop) ?: continue + val yBottom = projectionToY(projection, cell.latBottom) ?: continue + val xLeftBase = projectionToX(projection, cell.lonLeft, centerLon, worldWidthPx) ?: continue + val xRightBase = projectionToX(projection, cell.lonRight, centerLon, worldWidthPx) ?: continue + val xLeft = xLeftBase + turn * worldWidthPx.toFloat() + val xRight = xRightBase + turn * worldWidthPx.toFloat() + if (xRight < 0f || xLeft > canvas.width) continue + if (yBottom < 0f || yTop > canvas.height) continue + canvas.drawRect(xLeft, yTop, xRight, yBottom, markedPaint) + } + } } } @@ -414,12 +443,15 @@ class MaidenheadGridOverlay : Overlay() { if (xRight < 0f || xLeft > canvas.width) continue val label = cellLabel(lat, lon, zoom) if (showFirstCallLabels) { - // 首通呼号模式: 只有绿格(worked)标注该格第一个通联的呼号, - // 非绿格空着不写网格字符. - if (label in workedGrids) { - firstCallsByGrid[label]?.let { call -> + // 首通呼号模式: 绿格(worked)标注该格第一个通联的呼号, 红格(marked) + // 标注用户标记想通联的呼号, 均淡黄文字; 其它格空着不写网格字符. + when { + label in workedGrids -> firstCallsByGrid[label]?.let { call -> canvas.drawText(call, (xLeft + xRight) / 2f, yCenter, firstCallPaint) } + label in markedGrids -> markedGrids[label]?.let { station -> + canvas.drawText(station.call, (xLeft + xRight) / 2f, yCenter, firstCallPaint) + } } } else { canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint) diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt index e45fa522..c72b0b82 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapScreen.kt @@ -362,7 +362,8 @@ private fun MapScreen( centerOnStation = shouldCenter, // 首通呼号标签只作用于 VUCC 视图(用户要求); "All"/其他奖状不过滤. showFirstCallLabels = uiState.showFirstCallLabels && selectedAward == AwardType.VUCC, - firstCallsByGrid = firstCallsByGrid + firstCallsByGrid = firstCallsByGrid, + markedGrids = uiState.markedGrids ) if (!shouldCenter || uiState.stationPosition != null) { prevGridMode = uiState.isGridMode @@ -422,15 +423,19 @@ private fun MapScreen( } } // Centered dialog listing the tapped worked grid's confirmed satellite QSOs. - // Dismissed by tapping outside (no explicit close button). + // Dismissed by tapping outside (no explicit close button). Unworked grids + // open the same dialog with an empty list plus a mark-a-station reminder. selectedGrid?.let { grid -> WorkedGridQsoDialog( grid = grid, qsos = uiState.workedGridQsos[grid].orEmpty().sortedBy { it.epochMs }, + marked = uiState.markedGrids[grid], isUtc = uiState.isUtc, matchCalculating = matchCalculating, onDismiss = { selectedGrid = null }, - onMatch = { onMatchGrid(grid) } + onMatch = { onMatchGrid(grid) }, + onMark = { call -> onAction(MapAction.SetMarkedStation(grid, call)) }, + onRemoveMark = { onAction(MapAction.RemoveMarkedStation(grid)) } ) } } @@ -441,11 +446,16 @@ private fun MapScreen( private fun WorkedGridQsoDialog( grid: String, qsos: List, + marked: com.rtbishop.look4sat.core.domain.model.MarkedStation?, isUtc: Boolean, matchCalculating: Boolean, onDismiss: () -> Unit, - onMatch: () -> Unit + onMatch: () -> Unit, + onMark: (String) -> Unit, + onRemoveMark: () -> Unit ) { + var showMarkInput by remember { mutableStateOf(false) } + var callInput by remember { mutableStateOf("") } androidx.compose.ui.window.Dialog(onDismissRequest = onDismiss) { // Centered card, ~85% width, internal scroll for long lists. Card( @@ -497,26 +507,127 @@ private fun WorkedGridQsoDialog( } } androidx.compose.material3.HorizontalDivider() - if (qsos.isNotEmpty()) { - // Group by callsign preserving first-contact order (list is - // already sorted oldest-first); expandable rows. - val grouped = remember(qsos) { - qsos.groupBy { it.call }.entries.sortedBy { it.value.first().epochMs } + Column( + Modifier + .heightIn(max = 380.dp) + .verticalScroll(rememberScrollState()) + ) { + // A marked station shows first as a red reminder row: red + // callsign, "标记" in the count column, the marking date in + // the date column, and a small delete icon to remove it. + if (marked != null) { + MarkedStationRow(marked = marked, isUtc = isUtc, onRemoveMark = onRemoveMark) + androidx.compose.material3.HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)) } - Column( - Modifier - .heightIn(max = 380.dp) - .verticalScroll(rememberScrollState()) - ) { + if (qsos.isNotEmpty()) { + // Group by callsign preserving first-contact order (list is + // already sorted oldest-first); expandable rows. + val grouped = remember(qsos) { + qsos.groupBy { it.call }.entries.sortedBy { it.value.first().epochMs } + } grouped.forEach { (call, callQsos) -> WorkedGridCallRow(call, callQsos = callQsos, isUtc = isUtc) androidx.compose.material3.HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)) } } + // Unworked grids: offer to mark a station the user wants to + // contact in this grid (a reminder, cleared when worked). + if (qsos.isEmpty()) { + TextButton( + onClick = { + callInput = marked?.call.orEmpty() + showMarkInput = true + }, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + painter = painterResource(R.drawable.ic_add), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(Modifier.width(4.dp)) + Text(stringResource(R.string.grid_mark_station)) + } + } } } } } + // Mark-a-station input dialog: enter a callsign as a reminder. + if (showMarkInput) { + androidx.compose.material3.AlertDialog( + onDismissRequest = { showMarkInput = false }, + title = { Text(stringResource(R.string.grid_mark_station)) }, + text = { + androidx.compose.material3.OutlinedTextField( + value = callInput, + onValueChange = { callInput = it }, + label = { Text(stringResource(R.string.grid_mark_callsign)) }, + singleLine = true + ) + }, + confirmButton = { + TextButton( + enabled = callInput.isNotBlank(), + onClick = { + onMark(callInput) + showMarkInput = false + } + ) { Text(stringResource(R.string.grid_mark_confirm)) } + }, + dismissButton = { + TextButton(onClick = { showMarkInput = false }) { Text(stringResource(R.string.btn_cancel)) } + } + ) + } +} + +@Composable +private fun MarkedStationRow( + marked: com.rtbishop.look4sat.core.domain.model.MarkedStation, + isUtc: Boolean, + onRemoveMark: () -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + text = marked.call, + style = MaterialTheme.typography.titleMedium, + fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, + color = ComposeColor(0xFFFF3B30), + maxLines = 1, + overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + Text( + text = stringResource(R.string.grid_mark_label), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = androidx.compose.ui.text.style.TextAlign.Center, + modifier = Modifier.weight(1f) + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier.weight(1f) + ) { + Text( + text = formatDate(marked.epochMs, isUtc), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = stringResource(R.string.grid_mark_delete), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier + .size(18.dp) + .clickable(onClick = onRemoveMark) + ) + } + } } @Composable @@ -944,7 +1055,8 @@ private fun setGridMode( stationPosition: GeoPos?, centerOnStation: Boolean = false, showFirstCallLabels: Boolean = false, - firstCallsByGrid: Map = emptyMap() + firstCallsByGrid: Map = emptyMap(), + markedGrids: Map = emptyMap() ) { try { val gridOverlay = mapView.overlays[OVERLAY_GRID] @@ -955,6 +1067,7 @@ private fun setGridMode( // 首通呼号模式: 绿格标注第一个通联的呼号, 非绿格不显示标签. gridOverlay.showFirstCallLabels = showFirstCallLabels gridOverlay.firstCallsByGrid = firstCallsByGrid + gridOverlay.markedGrids = markedGrids // ownGrid must be set on EVERY update — the position is available // regardless of whether this frame centers (centering happens only // on entry, but passing null here would wipe the bold outline). @@ -966,6 +1079,7 @@ private fun setGridMode( this.roamedGrids = roamedGrids this.showFirstCallLabels = showFirstCallLabels this.firstCallsByGrid = firstCallsByGrid + this.markedGrids = markedGrids this.ownGrid = ownGridOf(stationPosition) } } diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt index 558791b1..7f5ac687 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapState.kt @@ -34,6 +34,8 @@ data class MapState( val workedGridQsos: Map> = emptyMap(), /** 4-char gridsquares the station operated from (LoTW MY_GRIDSQUARE) — blue stripes. */ val roamedGrids: Set = emptySet(), + /** Stations the user marked in unworked gridsquares (grid -> mark) — red fill. */ + val markedGrids: Map = emptyMap(), val stationPosition: GeoPos? = null, val orbitalPass: OrbitalPass, val track: List>? = null, @@ -52,6 +54,8 @@ sealed interface MapAction { data class SelectDefaultItem(val catnum: Int) : MapAction data class ToggleGridMode(val value: Boolean) : MapAction data class ToggleFirstCallLabels(val value: Boolean) : MapAction + data class SetMarkedStation(val grid: String, val call: String) : MapAction + data class RemoveMarkedStation(val grid: String) : MapAction data class SetVisible(val isVisible: Boolean) : MapAction } diff --git a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt index aa587f58..496815bf 100644 --- a/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt +++ b/feature/map/src/main/java/com/rtbishop/look4sat/feature/map/MapViewModel.kt @@ -90,11 +90,19 @@ class MapViewModel( } viewModelScope.launch { settingsRepo.wavelogSettings.collectLatest { _ -> + val workedGrids = settingsRepo.getWorkedGrids() + val markedGrids = settingsRepo.getMarkedGridStations() + // Marked stations auto-clear once their grid becomes worked: + // the reminder has served its purpose (user req: 网格变绿后标记自动清除). + val stale = markedGrids.keys.filter { it in workedGrids } + val activeMarks = if (stale.isEmpty()) markedGrids else markedGrids - stale.toSet() + if (stale.isNotEmpty()) settingsRepo.setMarkedGridStations(activeMarks) _uiState.update { it.copy( - workedGrids = settingsRepo.getWorkedGrids(), + workedGrids = workedGrids, workedGridQsos = settingsRepo.getWorkedGridQsos(), - roamedGrids = settingsRepo.getRoamedGrids() + roamedGrids = settingsRepo.getRoamedGrids(), + markedGrids = activeMarks ) } } @@ -112,6 +120,20 @@ class MapViewModel( is MapAction.SelectDefaultItem -> selectDefaultSatellite(action.catnum) is MapAction.ToggleGridMode -> settingsRepo.updateOtherSettings { it.copy(stateOfMapGrid = action.value) } is MapAction.ToggleFirstCallLabels -> settingsRepo.updateOtherSettings { it.copy(stateOfMapFirstCall = action.value) } + is MapAction.SetMarkedStation -> { + val updated = settingsRepo.getMarkedGridStations() + + (action.grid to com.rtbishop.look4sat.core.domain.model.MarkedStation( + call = action.call.trim().uppercase(), + epochMs = System.currentTimeMillis() + )) + settingsRepo.setMarkedGridStations(updated) + _uiState.update { it.copy(markedGrids = updated) } + } + is MapAction.RemoveMarkedStation -> { + val updated = settingsRepo.getMarkedGridStations() - action.grid + settingsRepo.setMarkedGridStations(updated) + _uiState.update { it.copy(markedGrids = updated) } + } is MapAction.SetVisible -> isScreenVisible.value = action.isVisible } }