feat: mutual chip uses theme primary yellow, plain text distance; top bar text indent; low-zoom meridians via direct Mercator math (bypass osmdroid wrap bug); remove Wavelog sync entry — LoTW is the sole grid source (replace instead of merge)

This commit is contained in:
atsunatsu committed 2026-09-07 19:10:44 +08:00
1 parent a9fe0b74e9
commit c09f718aae
6 files changed
+37 -95

No files matched your search

@@ -53,7 +53,8 @@ fun GridTargetChip(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val configured = grid != null && distanceKm != null && bearingDeg != null val configured = grid != null && distanceKm != null && bearingDeg != null
val accent = MaterialTheme.colorScheme.tertiaryContainer // The app's signature accent yellow (MaterialTheme primary in both themes).
val accent = MaterialTheme.colorScheme.primary
val placeholder = MaterialTheme.colorScheme.onSurfaceVariant val placeholder = MaterialTheme.colorScheme.onSurfaceVariant
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -83,13 +84,8 @@ fun GridTargetChip(
Text( Text(
text = distanceKm?.let { "${"%,.0f".format(it)} km" } ?: "-- km", text = distanceKm?.let { "${"%,.0f".format(it)} km" } ?: "-- km",
fontSize = 14.sp, fontSize = 14.sp,
color = if (configured) Color.White else placeholder, color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
.background(
color = if (configured) MaterialTheme.colorScheme.onSurfaceVariant else Color.Transparent,
shape = RoundedCornerShape(8.dp)
)
.padding(horizontal = 8.dp, vertical = 2.dp)
) )
} }
} }
@@ -94,6 +94,10 @@ class MaidenheadGridOverlay : Overlay() {
val firstCol = floor(leftLon / cellLon).toInt() val firstCol = floor(leftLon / cellLon).toInt()
val lastCol = ceil(rightLon / cellLon).toInt() val lastCol = ceil(rightLon / cellLon).toInt()
val centerLon = (leftLon + rightLon) / 2.0 val centerLon = (leftLon + rightLon) / 2.0
// World width in screen pixels for the current zoom (256 px per tile,
// 2^zoom tiles across the whole 360° world). Used by the custom
// Mercator-X computation in projectionToX.
val worldWidthPx = 256.0 * Math.pow(2.0, zoom)
// The station's own grid square (4-char, only meaningful at sub-square zoom) // The station's own grid square (4-char, only meaningful at sub-square zoom)
val ownCell = ownGrid?.takeIf { zoom >= GRID_ZOOM_SUB } val ownCell = ownGrid?.takeIf { zoom >= GRID_ZOOM_SUB }
@@ -110,8 +114,8 @@ class MaidenheadGridOverlay : Overlay() {
if (yTop == null || yBottom == null) continue if (yTop == null || yBottom == null) continue
for (col in firstCol..lastCol) { for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon) ?: continue val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, lon + cellLon, centerLon) ?: continue val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
if (xRight < 0f || xLeft > canvas.width) continue if (xRight < 0f || xLeft > canvas.width) continue
if (cellLabel(lat, lon, zoom) in workedGrids) { if (cellLabel(lat, lon, zoom) in workedGrids) {
canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint) canvas.drawRect(xLeft, yTop, xRight, yBottom, workedPaint)
@@ -123,7 +127,7 @@ class MaidenheadGridOverlay : Overlay() {
// Vertical lines (meridians) // Vertical lines (meridians)
for (col in firstCol..lastCol) { for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val x = projectionToX(projection, lon, centerLon) val x = projectionToX(projection, lon, centerLon, worldWidthPx)
if (x == null) continue if (x == null) continue
canvas.drawLine(x, 0f, x, canvas.height.toFloat(), linePaint) canvas.drawLine(x, 0f, x, canvas.height.toFloat(), linePaint)
} }
@@ -147,8 +151,8 @@ class MaidenheadGridOverlay : Overlay() {
if (cellLabel(lat, lon, zoom) != ownCell) continue if (cellLabel(lat, lon, zoom) != ownCell) continue
val yTop = projectionToY(projection, lat + cellLat) ?: continue val yTop = projectionToY(projection, lat + cellLat) ?: continue
val yBottom = projectionToY(projection, lat) ?: continue val yBottom = projectionToY(projection, lat) ?: continue
val xLeft = projectionToX(projection, lon, centerLon) ?: continue val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, lon + cellLon, centerLon) ?: continue val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
canvas.drawLine(xLeft, yTop, xRight, yTop, ownLinePaint) canvas.drawLine(xLeft, yTop, xRight, yTop, ownLinePaint)
canvas.drawLine(xLeft, yBottom, xRight, yBottom, ownLinePaint) canvas.drawLine(xLeft, yBottom, xRight, yBottom, ownLinePaint)
canvas.drawLine(xLeft, yTop, xLeft, yBottom, ownLinePaint) canvas.drawLine(xLeft, yTop, xLeft, yBottom, ownLinePaint)
@@ -187,8 +191,8 @@ class MaidenheadGridOverlay : Overlay() {
if (yBottom < 0f || yTop > canvas.height) continue if (yBottom < 0f || yTop > canvas.height) continue
for (col in firstCol..lastCol) { for (col in firstCol..lastCol) {
val lon = col * cellLon val lon = col * cellLon
val xLeft = projectionToX(projection, lon, centerLon) ?: continue val xLeft = projectionToX(projection, lon, centerLon, worldWidthPx) ?: continue
val xRight = projectionToX(projection, lon + cellLon, centerLon) ?: continue val xRight = projectionToX(projection, lon + cellLon, centerLon, worldWidthPx) ?: continue
if (xRight < 0f || xLeft > canvas.width) continue if (xRight < 0f || xLeft > canvas.width) continue
val label = cellLabel(lat, lon, zoom) val label = cellLabel(lat, lon, zoom)
canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint) canvas.drawText(label, (xLeft + xRight) / 2f, yCenter, labelPaint)
@@ -197,22 +201,18 @@ class MaidenheadGridOverlay : Overlay() {
} }
/** X pixel for a longitude (meridians are vertical in Web Mercator). */ /** X pixel for a longitude (meridians are vertical in Web Mercator). */
private fun projectionToX(projection: Projection, lon: Double, centerLon: Double): Float? { private fun projectionToX(projection: Projection, lon: Double, centerLon: Double, worldWidthPx: Double): Float? {
// osmdroid clips Mercator X to [0, mapSize], which destroys the projection of // Do NOT use osmdroid's toPixels() here: at low zoom its wrap-around
// longitudes outside the [0, 360) window of the current view (visible at low // logic (getCloserPixel) mis-projects longitudes far from the view
// zoom where the whole world is narrower than the viewport). Normalizing the // center, which makes meridians vanish while panning. Mercator X is
// longitude to the equivalent value closest to the view center keeps every // linear in longitude, so compute it directly:
// meridian's X near the center, safely inside the clip window. // x = screenCenterX + (lon - centerLon) / 360 * worldWidthPx
var normalized = lon var delta = lon - centerLon
while (normalized < centerLon - 180.0) normalized += 360.0 while (delta > 180.0) delta -= 360.0
while (normalized > centerLon + 180.0) normalized -= 360.0 while (delta < -180.0) delta += 360.0
val geo = org.osmdroid.util.GeoPoint(0.0, normalized) val geo = org.osmdroid.util.GeoPoint(0.0, centerLon)
val p = projection.toPixels(geo, null) val p = projection.toPixels(geo, null)
// Accept generous horizontal overshoot: at low zoom the world repeats and return (p.x + delta / 360.0 * worldWidthPx).toFloat()
// a meridian just off-screen may still be projected; rejecting too early
// makes lines vanish while panning. 2x viewport width is plenty.
val limit = canvasWidthPx * 2f + MAX_OVERSHOOT_PX
return if (p.x >= -limit && p.x <= limit) p.x.toFloat() else null
} }
/** Y pixel for a latitude, or null when outside the viewport. */ /** Y pixel for a latitude, or null when outside the viewport. */
@@ -89,7 +89,8 @@ fun MutualScreen(
Text( Text(
text = stringResource(R.string.mutual_title), text = stringResource(R.string.mutual_title),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 12.dp)
) )
}, },
bottomInfo = { bottomInfo = {
@@ -99,7 +100,9 @@ fun MutualScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f) modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
) )
}, },
endAction = { endAction = {
@@ -203,18 +203,6 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
onSave = { onAction(SettingsAction.UpdateRadioControl(it)) } onSave = { onAction(SettingsAction.UpdateRadioControl(it)) }
) )
} }
if (dialogs.wavelog) {
WavelogDialog(
initialSettings = uiState.wavelogSettings,
workedGridsCount = uiState.workedGridsCount,
isSyncing = uiState.wavelogSyncing,
message = uiState.wavelogMessage,
dismiss = { dialogs.wavelog = false },
onSave = { onAction(SettingsAction.UpdateWavelog(it)) },
onSync = { onAction(SettingsAction.SyncWorkedGrids(it)) }
)
}
if (dialogs.lotw) { if (dialogs.lotw) {
LoTWDialog( LoTWDialog(
initialSettings = uiState.lotwSettings, initialSettings = uiState.lotwSettings,
@@ -330,13 +318,6 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
onRadioControlClick = { dialogs.radioControl = true } onRadioControlClick = { dialogs.radioControl = true }
) )
} }
item {
WavelogCard(
settings = uiState.wavelogSettings,
workedGridsCount = uiState.workedGridsCount,
showWavelogDialog = { dialogs.wavelog = true }
)
}
item { item {
LoTWCard( LoTWCard(
settings = uiState.lotwSettings, settings = uiState.lotwSettings,
@@ -581,44 +562,6 @@ private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boole
} }
} }
@Preview(showBackground = true)
@Composable
private fun WavelogCardPreview() = MainTheme {
WavelogCard(settings = WavelogSettings("http://192.168.1.10", "wl2_demo"), workedGridsCount = 42) {}
}
@Composable
private fun WavelogCard(
settings: WavelogSettings,
workedGridsCount: Int,
showWavelogDialog: () -> Unit
) {
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Text(
text = stringResource(id = R.string.prefs_wavelog_title),
color = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(2.dp))
Text(
text = if (settings.isConfigured) {
stringResource(R.string.prefs_wavelog_configured, workedGridsCount)
} else {
stringResource(R.string.prefs_wavelog_not_configured)
},
style = MaterialTheme.typography.bodySmall,
maxLines = 2
)
Spacer(modifier = Modifier.height(4.dp))
CardButton(
onClick = showWavelogDialog,
text = stringResource(id = R.string.prefs_wavelog_configure),
modifier = Modifier.fillMaxWidth()
)
}
}
}
@Composable @Composable
private fun LoTWCard( private fun LoTWCard(
settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings, settings: com.rtbishop.look4sat.core.domain.model.LoTWSettings,
@@ -206,10 +206,10 @@ class SettingsViewModel(
if (lotwGrids == null) { if (lotwGrids == null) {
state.copy(lotwSyncing = false, lotwMessage = "LoTW sync failed — check callsign/password/network") state.copy(lotwSyncing = false, lotwMessage = "LoTW sync failed — check callsign/password/network")
} else { } else {
// Merge with Wavelog grids: union of both sources, all confirmed/worked // Wavelog entry removed: LoTW is now the only source, so the
val merged = settingsRepo.getWorkedGrids() + lotwGrids // synced set fully replaces the stored worked grids.
settingsRepo.setWorkedGrids(merged) settingsRepo.setWorkedGrids(lotwGrids)
state.copy(lotwSyncing = false, workedGridsCount = merged.size, lotwMessage = null) state.copy(lotwSyncing = false, workedGridsCount = lotwGrids.size, lotwMessage = null)
} }
} }
} }
+2 -2
View File
@@ -1,8 +1,8 @@
[versions] [versions]
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionCode = "457" appVersionCode = "458"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.6-ba7opf.9.5" appVersionName = "4.4.6-ba7opf.9.6"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry
compileSdk = "37" compileSdk = "37"
#noinspection UnusedVersionCatalogEntry #noinspection UnusedVersionCatalogEntry