mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(settings): 致谢与声明卡、版本卡打开检查更新、浅色主题、品牌与 GPL 头
This commit is contained in:
1 parent
5acf71b2a6
commit
45cd72faf7
23 files changed
+410
-100
No files matched your search
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2026 BA7OPF.
|
||||
* Based on Look4Sat by 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
/*
|
||||
* CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。
|
||||
*
|
||||
|
||||
@@ -139,6 +139,18 @@ class AwardBoundaryOverlay : Overlay() {
|
||||
color = Color.argb(90, 76, 217, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recolor the amber boundary/label paints for the active map theme (dark amber + white halo on
|
||||
* the light map, light amber + black halo on the dark map). The worked fill reads on both.
|
||||
*/
|
||||
fun applyTheme(isLightUi: Boolean) {
|
||||
val line = if (isLightUi) Color.argb(200, 113, 92, 12) else Color.argb(160, 255, 224, 130)
|
||||
val label = if (isLightUi) Color.argb(235, 30, 25, 5) else Color.argb(220, 255, 224, 130)
|
||||
linePaint.color = line
|
||||
labelPaint.color = label
|
||||
labelPaint.setShadowLayer(3f, 2f, 2f, if (isLightUi) Color.WHITE else Color.BLACK)
|
||||
}
|
||||
|
||||
/** Boundary regions to render; assigning recomputes culling bounds. */
|
||||
var regions: List<AwardRegion> = emptyList()
|
||||
set(value) {
|
||||
|
||||
@@ -89,6 +89,24 @@ class MaidenheadGridOverlay : Overlay() {
|
||||
color = Color.argb(255, 76, 217, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recolor the amber line/label paints for the active map theme: dark amber with a white halo
|
||||
* on the light map, light amber with a black halo on the dark map. The worked / marked / roam /
|
||||
* selected colors stay as they are — they read on both backgrounds.
|
||||
*/
|
||||
fun applyTheme(isLightUi: Boolean) {
|
||||
val line = if (isLightUi) Color.argb(200, 113, 92, 12) else Color.argb(160, 255, 224, 130)
|
||||
val label = if (isLightUi) Color.argb(235, 30, 25, 5) else Color.argb(220, 255, 224, 130)
|
||||
val halo = if (isLightUi) Color.WHITE else Color.BLACK
|
||||
linePaint.color = line
|
||||
// ownLine stays the same color as the normal grid lines (only thicker).
|
||||
ownLinePaint.color = line
|
||||
labelPaint.color = label
|
||||
labelPaint.setShadowLayer(3f, 2f, 2f, halo)
|
||||
firstCallPaint.color = label
|
||||
firstCallPaint.setShadowLayer(3f, 2f, 2f, halo)
|
||||
}
|
||||
|
||||
/** Worked gridsquares (4-char, uppercase) to highlight, e.g. {"OL62", "PM95"}. */
|
||||
var workedGrids: Set<String> = emptySet()
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
/*
|
||||
* Look4Sat-BA7OPF. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2026 BA7OPF.
|
||||
* Based on Look4Sat by 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.map
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
|
||||
@@ -159,6 +159,71 @@ private val moonIconPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
/**
|
||||
* Accent used for the station marker, footprint outline and map labels. It is recolored per
|
||||
* theme (dark amber on light tiles, light amber on dark tiles) so every map element stays
|
||||
* readable; [applyMapColors] keeps it in sync with the shared paints.
|
||||
*/
|
||||
private var mapAccentColor = "#FFE082".toColorInt()
|
||||
|
||||
/** Grayscale + invert filter that turns the offline OSM map dark; unused (and off) in light theme. */
|
||||
private val darkTileFilter by lazy { createColorFilter() }
|
||||
|
||||
/** Theme the shared map paints were last colored for; null = not applied yet this process. */
|
||||
private var mapIsLightUi: Boolean? = null
|
||||
|
||||
/** Theme the tile filter / icon caches were last applied for (drives the per-frame theme check). */
|
||||
private var appliedLightUi: Boolean? = null
|
||||
|
||||
/** True while the offline OSM source is active — satellite imagery (Tianditu) is never filtered. */
|
||||
private var offlineTilesActive = true
|
||||
|
||||
/**
|
||||
* Recolor the shared overlay paints and custom overlays for the current theme. Must run before the
|
||||
* markers are (re)built each frame so freshly created icons use the new colors; [applyThemeToOverlays]
|
||||
* and the icon-cache eviction in the update block handle the already created ones.
|
||||
*/
|
||||
private fun applyMapColors(isLightUi: Boolean) {
|
||||
mapIsLightUi = isLightUi
|
||||
if (isLightUi) {
|
||||
// Dark amber (lightScheme primary) reads on light tiles; labels get a white halo.
|
||||
mapAccentColor = "#715C0C".toColorInt()
|
||||
footprintPaint.color = mapAccentColor
|
||||
textPaint.color = "#1E1B13".toColorInt()
|
||||
textPaint.setShadowLayer(3f, 3f, 3f, Color.WHITE)
|
||||
sunIconPaint.colorFilter =
|
||||
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
||||
moonIconPaint.colorFilter = android.graphics.PorterDuffColorFilter(
|
||||
"#4C4639".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
} else {
|
||||
mapAccentColor = "#FFE082".toColorInt()
|
||||
footprintPaint.color = mapAccentColor
|
||||
textPaint.color = mapAccentColor
|
||||
textPaint.setShadowLayer(3f, 3f, 3f, Color.BLACK)
|
||||
sunIconPaint.colorFilter =
|
||||
android.graphics.PorterDuffColorFilter(mapAccentColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
||||
moonIconPaint.colorFilter =
|
||||
android.graphics.PorterDuffColorFilter("#E0E0E0".toColorInt(), android.graphics.PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
applyThemeToOverlays()
|
||||
}
|
||||
|
||||
/** Push the current theme into the grid / award overlays, which own their own paints. */
|
||||
private fun applyThemeToOverlays() {
|
||||
val isLight = mapIsLightUi == true
|
||||
currentMapView?.overlays?.forEach { overlay ->
|
||||
when (overlay) {
|
||||
is MaidenheadGridOverlay -> overlay.applyTheme(isLight)
|
||||
is AwardBoundaryOverlay -> overlay.applyTheme(isLight)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The MapView currently attached to the map page; used to recolor overlays on a theme change. */
|
||||
private var currentMapView: MapView? = null
|
||||
|
||||
@Composable
|
||||
fun MapDestination(
|
||||
mapFilterViewModel: MapFilterViewModel,
|
||||
@@ -320,8 +385,8 @@ private fun MapScreen(
|
||||
val firstPos = uiState.track?.firstOrNull()?.firstOrNull() ?: return@LaunchedEffect
|
||||
mapView.controller.animateTo(GeoPoint(firstPos.latitude, firstPos.longitude))
|
||||
}
|
||||
LaunchedEffect(uiState.mapSource, uiState.tiandituKey) {
|
||||
configureTileSources(mapView, uiState.mapSource, uiState.tiandituKey)
|
||||
LaunchedEffect(uiState.mapSource, uiState.tiandituKey, uiState.isLightUi) {
|
||||
configureTileSources(mapView, uiState.mapSource, uiState.tiandituKey, uiState.isLightUi)
|
||||
}
|
||||
Column(modifier = Modifier.layoutPadding(), verticalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
val isVertical = isVerticalLayout()
|
||||
@@ -359,6 +424,18 @@ private fun MapScreen(
|
||||
mutableStateOf(restoredViewport && uiState.isGridMode)
|
||||
}
|
||||
AndroidView({ mapView }) { view ->
|
||||
// Theme change: recolor the paints/tile filter/overlays and drop the icon
|
||||
// caches, which baked the previous theme's colors into their bitmaps.
|
||||
if (appliedLightUi != uiState.isLightUi) {
|
||||
appliedLightUi = uiState.isLightUi
|
||||
applyMapColors(uiState.isLightUi)
|
||||
iconCache.evictAll()
|
||||
dotIcon = null
|
||||
view.overlayManager.tilesOverlay.setColorFilter(
|
||||
if (!uiState.isLightUi && offlineTilesActive) darkTileFilter else null
|
||||
)
|
||||
requestTileRefresh(view)
|
||||
}
|
||||
// Award filter mode: show the selected award's regions with
|
||||
// worked ones filled green; hide satellite layers like grid mode.
|
||||
val awardMode = selectedAward?.takeIf { it != AwardType.VUCC }
|
||||
@@ -393,13 +470,13 @@ private fun MapScreen(
|
||||
}
|
||||
}
|
||||
if (!uiState.isGridMode) {
|
||||
uiState.stationPosition?.let { setStationPosition(it, view) }
|
||||
uiState.stationPosition?.let { setStationPosition(it, view, uiState.isLightUi) }
|
||||
uiState.track?.let { setSatelliteTrack(it, view) }
|
||||
uiState.footprint?.let { setFootprint(it, view) }
|
||||
uiState.positions?.let { setPositions(it, view) { item -> onAction(MapAction.SelectItem(item)) } }
|
||||
setTerminator(uiState.sunLatDeg, uiState.sunLonDeg, view)
|
||||
setSubSolarPoint(uiState.sunLatDeg, uiState.sunLonDeg, view)
|
||||
setMoonPosition(uiState.moonLatDeg, uiState.moonLonDeg, view)
|
||||
setSubSolarPoint(uiState.sunLatDeg, uiState.sunLonDeg, view, uiState.isLightUi)
|
||||
setMoonPosition(uiState.moonLatDeg, uiState.moonLonDeg, view, uiState.isLightUi)
|
||||
}
|
||||
view.invalidate()
|
||||
}
|
||||
@@ -634,7 +711,7 @@ private fun MarkedStationRow(
|
||||
text = marked.call,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
color = ComposeColor(0xFFFFE082),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
@@ -651,7 +728,7 @@ private fun MarkedStationRow(
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_pin),
|
||||
contentDescription = stringResource(R.string.grid_mark_pin),
|
||||
tint = if (isPinned) ComposeColor(0xFF9E9E9E) else ComposeColor(0xFFFFE082),
|
||||
tint = if (isPinned) ComposeColor(0xFF9E9E9E) else MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clickable(onClick = onPinMark)
|
||||
@@ -706,7 +783,7 @@ private fun WorkedGridCallRow(
|
||||
text = call,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
color = ComposeColor(0xFFFFE082),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
@@ -1092,22 +1169,29 @@ private fun FirstCallToggle(
|
||||
// region Tile sources
|
||||
private var configuredTileMapView: MapView? = null
|
||||
private var configuredTileSource: String? = null
|
||||
private var configuredTileIsLightUi: Boolean? = null
|
||||
|
||||
private fun configureTileSources(mapView: MapView, mapSource: String, tiandituKey: String) {
|
||||
private fun configureTileSources(mapView: MapView, mapSource: String, tiandituKey: String, isLightUi: Boolean) {
|
||||
val key = tiandituKey.trim()
|
||||
val layers = tiandituLayers(mapSource)
|
||||
val sourceKey = if (layers == null || key.isBlank()) MapSource.OSM else "${layers.base}:$key"
|
||||
if (configuredTileMapView === mapView && configuredTileSource == sourceKey) return
|
||||
val offlineSource = layers == null || key.isBlank()
|
||||
offlineTilesActive = offlineSource
|
||||
val sourceKey = if (offlineSource) MapSource.OSM else "${layers.base}:$key"
|
||||
if (configuredTileMapView === mapView && configuredTileSource == sourceKey &&
|
||||
configuredTileIsLightUi == isLightUi
|
||||
) return
|
||||
|
||||
configuredTileMapView = mapView
|
||||
configuredTileSource = sourceKey
|
||||
configuredTileIsLightUi = isLightUi
|
||||
(mapView.overlays.getOrNull(OVERLAY_TDT_LABELS) as? TilesOverlay)?.onDetach(mapView)
|
||||
|
||||
if (layers == null || key.isBlank()) {
|
||||
if (offlineSource) {
|
||||
mapView.setUseDataConnection(false)
|
||||
mapView.setTileSource(offlineTileSource)
|
||||
mapView.maxZoomLevel = OFFLINE_MAX_ZOOM
|
||||
mapView.overlayManager.tilesOverlay.applyTileOverlayDefaults(createColorFilter())
|
||||
// The dark-map filter belongs to the dark theme only; the light theme shows plain tiles.
|
||||
mapView.overlayManager.tilesOverlay.applyTileOverlayDefaults(if (isLightUi) null else darkTileFilter)
|
||||
mapView.overlays[OVERLAY_TDT_LABELS] = FolderOverlay()
|
||||
requestTileRefresh(mapView)
|
||||
return
|
||||
@@ -1210,6 +1294,7 @@ private fun setAwardMode(award: AwardType, workedCodes: Set<String>, mapView: Ma
|
||||
isEnabled = true
|
||||
regions = AwardBoundaryData.load(mapView.context, asset)
|
||||
this.workedCodes = workedCodes
|
||||
applyTheme(mapIsLightUi == true)
|
||||
}
|
||||
}
|
||||
for (index in OVERLAY_STATION..OVERLAY_MOON) {
|
||||
@@ -1252,6 +1337,7 @@ private fun setGridMode(
|
||||
this.firstCallsByGrid = firstCallsByGrid
|
||||
this.markedGrids = markedGrids
|
||||
this.ownGrid = ownGridOf(stationPosition)
|
||||
applyTheme(mapIsLightUi == true)
|
||||
}
|
||||
}
|
||||
// Satellite-related layers are hidden in grid mode; the grid overlay
|
||||
@@ -1306,17 +1392,28 @@ private fun gridOfPoint(latitude: Double, longitude: Double): String? {
|
||||
return "${'A' + fieldLon}${'A' + fieldLat}$subLon$subLat"
|
||||
}
|
||||
|
||||
private fun setStationPosition(stationPos: GeoPos, mapView: MapView) {
|
||||
private fun setStationPosition(stationPos: GeoPos, mapView: MapView, isLightUi: Boolean) {
|
||||
try {
|
||||
val overlay = mapView.overlays[OVERLAY_STATION]
|
||||
val stationPoint = GeoPoint(stationPos.latitude, stationPos.longitude)
|
||||
if (overlay is Marker) {
|
||||
overlay.position = GeoPoint(stationPos.latitude, stationPos.longitude)
|
||||
overlay.position = stationPoint
|
||||
// Icon tint is baked into the drawable, so re-tint after a theme change.
|
||||
if ((overlay.relatedObject as? Boolean) != isLightUi) {
|
||||
overlay.icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)?.apply {
|
||||
setTint(mapAccentColor)
|
||||
}
|
||||
overlay.relatedObject = isLightUi
|
||||
}
|
||||
} else {
|
||||
mapView.overlays[OVERLAY_STATION] = Marker(mapView).apply {
|
||||
setInfoWindow(null)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM)
|
||||
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)
|
||||
position = GeoPoint(stationPos.latitude, stationPos.longitude)
|
||||
icon = ContextCompat.getDrawable(mapView.context, R.drawable.ic_position)?.apply {
|
||||
setTint(mapAccentColor)
|
||||
}
|
||||
position = stationPoint
|
||||
relatedObject = isLightUi
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -1539,26 +1636,37 @@ private fun setTerminator(sunLatDeg: Double, sunLonDeg: Double, mapView: MapView
|
||||
}
|
||||
}
|
||||
|
||||
/** Build the 48dp sun/moon marker icon, tinted with the theme-dependent paint. */
|
||||
private fun buildMarkerIcon(mapView: MapView, resId: Int, paint: Paint): Drawable {
|
||||
val iconSize = 48
|
||||
val bmp = createBitmap(iconSize, iconSize)
|
||||
ContextCompat.getDrawable(mapView.context, resId)?.apply {
|
||||
setBounds(0, 0, iconSize, iconSize)
|
||||
colorFilter = paint.colorFilter
|
||||
draw(Canvas(bmp))
|
||||
}
|
||||
return bmp.toDrawable(mapView.context.resources)
|
||||
}
|
||||
|
||||
/** Place an ic_sun icon marker at the sub-solar point. */
|
||||
private fun setSubSolarPoint(sunLatDeg: Double, sunLonDeg: Double, mapView: MapView) {
|
||||
private fun setSubSolarPoint(sunLatDeg: Double, sunLonDeg: Double, mapView: MapView, isLightUi: Boolean) {
|
||||
try {
|
||||
val overlay = mapView.overlays[OVERLAY_SUN]
|
||||
val sunPos = GeoPoint(sunLatDeg, sunLonDeg)
|
||||
if (overlay is Marker) {
|
||||
overlay.position = sunPos
|
||||
} else {
|
||||
val iconSize = 48
|
||||
val bmp = createBitmap(iconSize, iconSize)
|
||||
ContextCompat.getDrawable(mapView.context, R.drawable.ic_sun)?.apply {
|
||||
setBounds(0, 0, iconSize, iconSize)
|
||||
colorFilter = sunIconPaint.colorFilter
|
||||
draw(Canvas(bmp))
|
||||
// The icon bitmap bakes in the theme tint, so rebuild it after a theme change.
|
||||
if ((overlay.relatedObject as? Boolean) != isLightUi) {
|
||||
overlay.icon = buildMarkerIcon(mapView, R.drawable.ic_sun, sunIconPaint)
|
||||
overlay.relatedObject = isLightUi
|
||||
}
|
||||
} else {
|
||||
mapView.overlays[OVERLAY_SUN] = Marker(mapView).apply {
|
||||
setInfoWindow(null)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = bmp.toDrawable(mapView.context.resources)
|
||||
icon = buildMarkerIcon(mapView, R.drawable.ic_sun, sunIconPaint)
|
||||
position = sunPos
|
||||
relatedObject = isLightUi
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -1567,26 +1675,23 @@ private fun setSubSolarPoint(sunLatDeg: Double, sunLonDeg: Double, mapView: MapV
|
||||
}
|
||||
|
||||
/** Place an ic_moon icon marker at the sub-lunar point. */
|
||||
private fun setMoonPosition(moonLatDeg: Double, moonLonDeg: Double, mapView: MapView) {
|
||||
private fun setMoonPosition(moonLatDeg: Double, moonLonDeg: Double, mapView: MapView, isLightUi: Boolean) {
|
||||
try {
|
||||
val overlay = mapView.overlays[OVERLAY_MOON]
|
||||
val moonPos = GeoPoint(moonLatDeg, moonLonDeg)
|
||||
if (overlay is Marker) {
|
||||
overlay.position = moonPos
|
||||
} else {
|
||||
val iconSize = 48
|
||||
val bmp = createBitmap(iconSize, iconSize)
|
||||
val c = Canvas(bmp)
|
||||
ContextCompat.getDrawable(mapView.context, R.drawable.ic_moon)?.apply {
|
||||
setBounds(0, 0, iconSize, iconSize)
|
||||
colorFilter = moonIconPaint.colorFilter
|
||||
draw(c)
|
||||
if ((overlay.relatedObject as? Boolean) != isLightUi) {
|
||||
overlay.icon = buildMarkerIcon(mapView, R.drawable.ic_moon, moonIconPaint)
|
||||
overlay.relatedObject = isLightUi
|
||||
}
|
||||
} else {
|
||||
mapView.overlays[OVERLAY_MOON] = Marker(mapView).apply {
|
||||
setInfoWindow(null)
|
||||
setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_CENTER)
|
||||
icon = bmp.toDrawable(mapView.context.resources)
|
||||
icon = buildMarkerIcon(mapView, R.drawable.ic_moon, moonIconPaint)
|
||||
position = moonPos
|
||||
relatedObject = isLightUi
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
@@ -1631,10 +1736,12 @@ private fun rememberMapViewWithLifecycle(
|
||||
zoomController.setVisibility(CustomZoomButtonsController.Visibility.NEVER)
|
||||
overlayManager.tilesOverlay.loadingBackgroundColor = Color.TRANSPARENT
|
||||
overlayManager.tilesOverlay.loadingLineColor = Color.TRANSPARENT
|
||||
overlayManager.tilesOverlay.setColorFilter(createColorFilter())
|
||||
// The dark-map tile filter (or no filter in the light theme) is applied per theme from
|
||||
// the AndroidView update block, so it also follows a theme switch at runtime.
|
||||
setScrollableAreaLimitLatitude(maxLat, minLat, 0)
|
||||
overlays.addAll(Array(OVERLAY_COUNT) { FolderOverlay() })
|
||||
overlays[OVERLAY_GRID] = MaidenheadGridOverlay()
|
||||
overlays[OVERLAY_GRID] = MaidenheadGridOverlay().apply { applyTheme(mapIsLightUi == true) }
|
||||
currentMapView = this
|
||||
}
|
||||
}
|
||||
val lifecycleObserver = rememberMapViewLifecycleObserver(mapView)
|
||||
@@ -1647,7 +1754,10 @@ private fun rememberMapViewWithLifecycle(
|
||||
// released with the MapView or they keep the Activity and its bitmaps alive after disposal.
|
||||
// (The viewport save lives in MapDestination, where the grid/satellite mode is known.)
|
||||
DisposableEffect(mapView) {
|
||||
onDispose { clearMapCaches() }
|
||||
onDispose {
|
||||
clearMapCaches()
|
||||
if (currentMapView === mapView) currentMapView = null
|
||||
}
|
||||
}
|
||||
return mapView
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ class MapViewModel(
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUtc = settings.stateOfUtc,
|
||||
isLightUi = settings.stateOfLightTheme,
|
||||
mapSource = settings.mapSource,
|
||||
tiandituKey = settings.tiandituKey,
|
||||
isGridMode = settings.stateOfMapGrid,
|
||||
|
||||
+43
-29
@@ -35,6 +35,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -264,11 +265,13 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
if (dialogs.logbook) {
|
||||
LogbookDialog(
|
||||
records = uiState.logbookRecords,
|
||||
satelliteCandidates = uiState.satelliteCatalog,
|
||||
uploadBusy = uiState.logbookUploadBusy,
|
||||
uploadMessage = uiState.logbookUploadMessage,
|
||||
preview = uiState.logbookPreview,
|
||||
onDismiss = { dialogs.logbook = false },
|
||||
onDelete = { onAction(SettingsAction.DeleteLogbookRecord(it)) },
|
||||
onEdit = { onAction(SettingsAction.UpdateLogbookRecord(it)) },
|
||||
onUpload = { onAction(SettingsAction.PrepareLogbookUpload) },
|
||||
onConfirmUpload = { onAction(SettingsAction.ConfirmLogbookUpload) },
|
||||
onDismissPreview = { onAction(SettingsAction.DismissLogbookPreview) },
|
||||
@@ -292,10 +295,8 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
|
||||
// URLs for top bar
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val appUrl = stringResource(R.string.prefs_app_url)
|
||||
val donateUrl = stringResource(R.string.prefs_donate_url)
|
||||
val fdroidTitle = stringResource(R.string.prefs_fdroid_title)
|
||||
val fdroidUrl = stringResource(R.string.prefs_fdroid_url)
|
||||
val upstreamTitle = stringResource(R.string.prefs_upstream_title)
|
||||
val upstreamUrl = stringResource(R.string.prefs_upstream_url)
|
||||
val gitHubTitle = stringResource(R.string.prefs_github_title)
|
||||
val gitHubUrl = stringResource(R.string.prefs_github_url)
|
||||
val licenseUrl = stringResource(R.string.prefs_license_url)
|
||||
@@ -309,18 +310,17 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
if (isVerticalLayout) {
|
||||
TopBar {
|
||||
TopCard(
|
||||
onClick = { safeOpenUri(appUrl) },
|
||||
onClick = { showUpdateChecker = true },
|
||||
version = uiState.appVersionName,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound)
|
||||
}
|
||||
TopBar {
|
||||
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
BotCard(
|
||||
onClick = { safeOpenUri(fdroidUrl) },
|
||||
resId = R.drawable.ic_fdroid,
|
||||
text = fdroidTitle,
|
||||
onClick = { safeOpenUri(upstreamUrl) },
|
||||
resId = R.drawable.ic_github,
|
||||
text = upstreamTitle,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
BotCard(
|
||||
@@ -335,17 +335,16 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
}
|
||||
} else {
|
||||
TopBar {
|
||||
PrimaryIconCard(onClick = { safeOpenUri(donateUrl) }, resId = R.drawable.ic_pound)
|
||||
TopCard(
|
||||
onClick = { safeOpenUri(appUrl) },
|
||||
onClick = { showUpdateChecker = true },
|
||||
version = uiState.appVersionName,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Row(modifier = Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
BotCard(
|
||||
onClick = { safeOpenUri(fdroidUrl) },
|
||||
resId = R.drawable.ic_fdroid,
|
||||
text = fdroidTitle,
|
||||
onClick = { safeOpenUri(upstreamUrl) },
|
||||
resId = R.drawable.ic_github,
|
||||
text = upstreamTitle,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
BotCard(
|
||||
@@ -639,7 +638,7 @@ private fun OtherCard(
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(370.dp)
|
||||
.height(420.dp) // 370dp + one switch row (light theme)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
@@ -666,7 +665,16 @@ private fun OtherCard(
|
||||
text = stringResource(R.string.prefs_compass_calibration_button),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
|
||||
// Appearance: light theme + the red night filter (mutually exclusive —
|
||||
// the night filter is disabled while the light theme is on).
|
||||
SwitchRow(R.string.prefs_other_switch_light_theme, settings.stateOfLightTheme) {
|
||||
onAction(SettingsAction.ToggleLightTheme(it))
|
||||
}
|
||||
SwitchRow(
|
||||
R.string.prefs_other_switch_night_mode,
|
||||
settings.stateOfNightMode,
|
||||
enabled = !settings.stateOfLightTheme
|
||||
) {
|
||||
onAction(SettingsAction.ToggleNightMode(it))
|
||||
}
|
||||
}
|
||||
@@ -762,14 +770,19 @@ private fun CompassCalibrationDialog(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
private fun SwitchRow(
|
||||
labelResId: Int,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(text = stringResource(id = labelResId))
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -845,27 +858,28 @@ private fun CardCreditsPreview() = MainTheme { CardCredits() }
|
||||
|
||||
@Composable
|
||||
private fun CardCredits(modifier: Modifier = Modifier) {
|
||||
ElevatedCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(268.dp)
|
||||
) {
|
||||
ElevatedCard(modifier = modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
.fillMaxHeight()
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_outro_title),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_outro_thanks)
|
||||
text = stringResource(id = R.string.prefs_outro_notice),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
HorizontalDivider()
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_outro_thanks),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_outro_license),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 12.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ data class SettingsState(
|
||||
val lotwLastSyncEpochMs: Long = 0L,
|
||||
/** Logbook (QSO records + LoTW confirmations), newest first. */
|
||||
val logbookRecords: List<com.rtbishop.look4sat.core.domain.logbook.QsoRecord> = emptyList(),
|
||||
/** ARRL satellite names (config.tq6) — the only names a record may be signed with. */
|
||||
val satelliteCatalog: List<String> = emptyList(),
|
||||
/** Imported LoTW upload certificate (null when none). */
|
||||
val lotwCertificate: com.rtbishop.look4sat.core.domain.repository.LoTWCertificate? = null,
|
||||
/** LoTW upload station location (null when unset). */
|
||||
@@ -153,6 +155,8 @@ sealed interface SettingsAction {
|
||||
// Logbook (QSO records + LoTW confirmations)
|
||||
data object RefreshLogbook : SettingsAction
|
||||
data class DeleteLogbookRecord(val id: Long) : SettingsAction
|
||||
/** Persist an edited record (frequency/callsign/time/…) from the logbook dialog. */
|
||||
data class UpdateLogbookRecord(val record: com.rtbishop.look4sat.core.domain.logbook.QsoRecord) : SettingsAction
|
||||
data object PrepareLogbookUpload : SettingsAction
|
||||
data object ConfirmLogbookUpload : SettingsAction
|
||||
data object DismissLogbookPreview : SettingsAction
|
||||
|
||||
Reference in new issue
Block a user