diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml
index becca72c..a9293462 100644
--- a/core/presentation/src/main/res/values-zh/strings.xml
+++ b/core/presentation/src/main/res/values-zh/strings.xml
@@ -122,6 +122,10 @@
能见度:%s
%d 个呼号 · %d 次通联
+
+ - %d QSO
+ - %d QSOs
+
首通
卫星未知
日照中
diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml
index 192cc876..52288200 100644
--- a/core/presentation/src/main/res/values/strings.xml
+++ b/core/presentation/src/main/res/values/strings.xml
@@ -147,6 +147,10 @@
Visibility: %s
%d callsigns · %d QSOs
+
+ - %d QSO
+ - %d QSOs
+
First
satellite unknown
Visible
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 96af2a1b..af07a5fe 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
@@ -62,6 +62,7 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color as ComposeColor
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -181,8 +182,30 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
var selectedAward by remember { mutableStateOf(AwardType.VUCC) }
// Six-award progress derived from the confirmed QSO store; recomputed when
// the store changes (LoTW/Wavelog sync).
- val awardProgress: List = remember(uiState.workedGridQsos) {
- AwardCalculator.calculate(uiState.workedGridQsos)
+ // Per operated-grid VUCC breakdown: myGrid -> set of worked grids worked from it.
+ val vuccByMyGrid: Map> = remember(uiState.workedGridQsos) {
+ val m = mutableMapOf>()
+ for ((grid, qsos) in uiState.workedGridQsos) {
+ for (q in qsos) {
+ val mg = q.myGrid ?: continue
+ m.getOrPut(mg) { mutableSetOf() }.add(grid)
+ }
+ }
+ m
+ }
+ // Selected operated grid for VUCC counting; defaults to the grid with the
+ // most worked grids. Null when the store carries no per-QSO myGrid data
+ // (requires a LoTW resync) — then VUCC falls back to the global count.
+ var selectedMyGrid by remember(vuccByMyGrid) {
+ mutableStateOf(vuccByMyGrid.maxByOrNull { it.value.size }?.key)
+ }
+ val awardProgress: List = remember(uiState.workedGridQsos, vuccByMyGrid, selectedMyGrid) {
+ val vuccGrids = selectedMyGrid?.let { vuccByMyGrid[it].orEmpty() }
+ val base = AwardCalculator.calculate(uiState.workedGridQsos)
+ if (vuccGrids == null) base
+ else base.map { p ->
+ if (p.type == AwardType.VUCC) p.copy(workedKeys = vuccGrids, count = vuccGrids.size) else p
+ }
}
// Attach the tap listener whenever grid mode / worked grids change.
val workedGrids = uiState.workedGrids
@@ -298,6 +321,18 @@ private fun MapScreen(uiState: MapState, onAction: (MapAction) -> Unit, mapView:
if (isVertical) MapDataCard(mapData) else MapDataCards(mapData)
}
}
+ // Top-left: operated-grid selector for VUCC counting (grid mode only).
+ val myGrid = selectedMyGrid
+ if (uiState.isGridMode && selectedAward == AwardType.VUCC && myGrid != null) {
+ VuccGridSelector(
+ options = vuccByMyGrid,
+ selected = myGrid,
+ onSelect = { selectedMyGrid = it },
+ modifier = Modifier
+ .align(Alignment.TopStart)
+ .padding(8.dp)
+ )
+ }
GridModeToggle(
isGridMode = uiState.isGridMode,
onToggle = { onAction(MapAction.ToggleGridMode(it)) },
@@ -400,18 +435,33 @@ private fun WorkedGridCallRow(
style = MaterialTheme.typography.titleMedium,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
color = MaterialTheme.colorScheme.tertiary,
+ maxLines = 1,
+ overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
Text(
- text = formatDate(first.epochMs, isUtc),
+ text = pluralStringResource(R.plurals.grid_qso_count, callQsos.size, callQsos.size),
style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant
- )
- Text(
- text = if (expanded) " ▴" else " ▾",
- fontSize = 18.sp,
- color = MaterialTheme.colorScheme.primary
+ 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(first.epochMs, isUtc),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant
+ )
+ Text(
+ text = if (expanded) " ▴" else " ▾",
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.primary
+ )
+ }
}
// First-QSO summary line, always visible.
Text(
@@ -579,6 +629,73 @@ private fun AwardChipsRow(
}
}
+/**
+ * Compact pill toggle for switching between satellite view and grid mode,
+ * floated over the map's top-right corner. The label reflects the active
+ * mode ("Grid mode" when ON, "Satellite mode" when OFF). Semi-transparent
+ * background keeps the map readable underneath.
+ */
+@Composable
+private fun VuccGridSelector(
+ options: Map>,
+ selected: String,
+ onSelect: (String) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ var expanded by remember { mutableStateOf(false) }
+ // Most-worked grid first; selection is made on tap.
+ val sorted = remember(options) { options.entries.sortedByDescending { it.value.size } }
+ Surface(
+ color = ComposeColor.Black.copy(alpha = 0.45f),
+ shape = RoundedCornerShape(8.dp),
+ modifier = modifier
+ ) {
+ Column {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .clickable { expanded = !expanded }
+ .padding(start = 10.dp, end = 6.dp, top = 4.dp, bottom = 4.dp)
+ ) {
+ Text(
+ text = "$selected (${options[selected]?.size ?: 0})",
+ color = ComposeColor.White,
+ fontSize = 12.sp,
+ fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
+ )
+ Text(
+ text = if (expanded) " ▴" else " ▾",
+ fontSize = 18.sp,
+ color = MaterialTheme.colorScheme.primary,
+ modifier = Modifier.padding(start = 4.dp)
+ )
+ }
+ if (expanded) {
+ androidx.compose.material3.HorizontalDivider(color = ComposeColor.White.copy(alpha = 0.2f))
+ sorted.forEach { (grid, grids) ->
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ onSelect(grid)
+ expanded = false
+ }
+ .padding(horizontal = 10.dp, vertical = 3.dp)
+ ) {
+ Text(
+ text = "$grid (${grids.size})",
+ color = if (grid == selected) MaterialTheme.colorScheme.primary else ComposeColor.White,
+ fontSize = 12.sp,
+ fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
/**
* Compact pill toggle for switching between satellite view and grid mode,
* floated over the map's top-right corner. The label reflects the active