mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(roaming): faithful QTH定位器 port, live-update toggle, night-mode safe
Rework the Roaming page after user review. It is now a faithful port of the QTH定位器 location panel, not a loose re-skin: UI (matching the reference layout): - GPS status dot (real: green when a fresh fix exists, outline when stale/missing) + date + time header - Lat/Lon rows with DMS and 5-decimal display, big 8-char locator centered below - 3x3 grid of neighboring 4-char squares with the reference proportions: center column ~2.6x wider (21.4% : 56.2% : 21.4%) and center row the tallest (31.5% : 35.9% : 31.7%); red position marker now placed at the fractional position of the fix inside the center cell (was fixed center) - No oversized GPS button: live updating is now a Settings toggle '漫游位置实时更新' (stateOfRoamingLive, default on) that drives periodic location refresh Style & night-mode safety: - All colors come from MaterialTheme.colorScheme (surfaceVariant/secondaryContainer/error) — no hardcoded cyan/blue from the original app. The red night filter (ColorMatrix keeping only the R channel) blanked the old hardcoded palette; theme colors survive it. - Chinese nav label '漫游' added to values-zh (was missing, showing English 'Roaming') Settings: - OtherSettings.stateOfRoamingLive persisted (default true), toggle row in Other card, height adjusted Verified: feature:roaming, feature:settings, app compile clean.
This commit is contained in:
1 parent
8a920acead
commit
6dbe30b3f6
10 files changed
+221
-110
No files matched your search
@@ -76,6 +76,7 @@ class SettingsRepo(
|
||||
private val keyStateOfUtc = "stateOfUtc"
|
||||
private val keyStateOfLightTheme = "stateOfLightTheme"
|
||||
private val keyStateOfNightMode = "stateOfNightMode"
|
||||
private val keyStateOfRoamingLive = "stateOfRoamingLive"
|
||||
private val keyStationAltitude = "stationAltitude"
|
||||
private val keyStationLatitude = "stationLatitude"
|
||||
private val keyStationLongitude = "stationLongitude"
|
||||
@@ -351,6 +352,7 @@ class SettingsRepo(
|
||||
putBoolean(keyStateOfUtc, new.stateOfUtc)
|
||||
putBoolean(keyStateOfLightTheme, new.stateOfLightTheme)
|
||||
putBoolean(keyStateOfNightMode, new.stateOfNightMode)
|
||||
putBoolean(keyStateOfRoamingLive, new.stateOfRoamingLive)
|
||||
putBoolean(keyShouldSeeWarning, new.shouldSeeWarning)
|
||||
putBoolean(keyShouldSeeWhatsNew, new.shouldSeeWhatsNew)
|
||||
putString(keySstvMode, new.sstvMode)
|
||||
@@ -368,6 +370,7 @@ class SettingsRepo(
|
||||
stateOfUtc = preferences.getBoolean(keyStateOfUtc, false),
|
||||
stateOfLightTheme = preferences.getBoolean(keyStateOfLightTheme, false),
|
||||
stateOfNightMode = preferences.getBoolean(keyStateOfNightMode, false),
|
||||
stateOfRoamingLive = preferences.getBoolean(keyStateOfRoamingLive, true),
|
||||
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
|
||||
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
|
||||
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
|
||||
|
||||
@@ -58,6 +58,7 @@ data class OtherSettings(
|
||||
val stateOfUtc: Boolean,
|
||||
val stateOfLightTheme: Boolean,
|
||||
val stateOfNightMode: Boolean = false,
|
||||
val stateOfRoamingLive: Boolean = true,
|
||||
val shouldSeeWarning: Boolean,
|
||||
val shouldSeeWhatsNew: Boolean,
|
||||
val sstvMode: String = "Auto",
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string>
|
||||
<string name="prefs_other_switch_sensors">Radar görünümünü döndürmek için sensörleri kullan</string>
|
||||
<string name="prefs_other_switch_night_mode">Kırmızı gece filtresini etkinleştir</string>
|
||||
<string name="prefs_other_switch_roaming_live">Roaming konumunu canlı güncelle</string>
|
||||
|
||||
<string name="prefs_highlight_title">Elevasyon vurgusu</string>
|
||||
<string name="prefs_highlight_low">Düşük < %1$d°</string>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<string name="nav_pass">过境</string>
|
||||
<string name="nav_radar">雷达</string>
|
||||
<string name="nav_mutual">匹配</string>
|
||||
<string name="nav_roaming">漫游</string>
|
||||
<string name="nav_map">地图</string>
|
||||
<string name="nav_prefs">设置</string>
|
||||
|
||||
@@ -161,6 +162,7 @@
|
||||
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
|
||||
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
|
||||
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
|
||||
<string name="prefs_other_switch_roaming_live">漫游位置实时更新</string>
|
||||
|
||||
<string name="prefs_highlight_title">仰角高亮</string>
|
||||
<string name="prefs_highlight_low">低 < %1$d°</string>
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
|
||||
<string name="prefs_other_switch_sensors">Use sensors to rotate radar view</string>
|
||||
<string name="prefs_other_switch_night_mode">Enable red night mode filter</string>
|
||||
<string name="prefs_other_switch_roaming_live">Update Roaming position live</string>
|
||||
|
||||
<string name="prefs_highlight_title">Elevation highlight</string>
|
||||
<string name="prefs_highlight_low">Low < %1$d°</string>
|
||||
|
||||
+149
-98
@@ -18,20 +18,21 @@
|
||||
package com.rtbishop.look4sat.feature.roaming
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -39,164 +40,214 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||
|
||||
private val InfoCyan = Color(0xFF0CCFE2)
|
||||
private val GridBlue = Color(0xFF12ACE0)
|
||||
private val LabelBlue = Color(0xFF135678)
|
||||
private val PositionRed = Color(0xFFFF0000)
|
||||
private val DividerWhite = Color(0xFFF7F7F7)
|
||||
import com.rtbishop.look4sat.core.presentation.LocalSpacing
|
||||
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Roaming (漫游) page: a faithful port of the QTH定位器 location panel —
|
||||
* GPS status + date/time header, DMS & decimal coordinates, the big 8-char
|
||||
* Maidenhead locator, and a 3x3 grid of neighboring squares (center column
|
||||
* and row are wider/taller, matching the reference app). Styled with the
|
||||
* Look4Sat Material 3 theme.
|
||||
*/
|
||||
@Composable
|
||||
fun RoamingScreen(viewModel: RoamingViewModel) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
InfoHeader(state = state, onGpsClick = viewModel::onGpsClick)
|
||||
GridPanel(state = state)
|
||||
ScreenColumn(
|
||||
topBar = {
|
||||
TopBar { Text(text = "漫游", fontSize = 20.sp, fontWeight = FontWeight.Bold) }
|
||||
}
|
||||
) {
|
||||
val spacing = LocalSpacing.current
|
||||
Column(verticalArrangement = Arrangement.spacedBy(spacing.medium)) {
|
||||
InfoCard(state = state)
|
||||
GridCard(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoHeader(state: RoamingState, onGpsClick: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(InfoCyan)
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp)
|
||||
) {
|
||||
// Top row: GPS status + big locator
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "GPS",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Black
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (state.qthLocator != "----") Color(0xFF3BAF52) else Color.Gray)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
private fun InfoCard(state: RoamingState) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// Top bar: GPS status + date + time
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "GPS",
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(10.dp)
|
||||
.clip(CircleShape)
|
||||
.background(if (state.gpsEnabled) colorScheme.primary else colorScheme.outline)
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date()),
|
||||
fontSize = 14.sp,
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()),
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
// Coordinates: DMS + decimal
|
||||
CoordinateRow(label = "纬度", value = state.latitude, isLat = true)
|
||||
CoordinateRow(label = "经度", value = state.longitude, isLat = false)
|
||||
// Big locator, centered
|
||||
Text(
|
||||
text = state.qthLocator,
|
||||
fontSize = 30.sp,
|
||||
fontSize = 34.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = Color.Black
|
||||
color = colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
// Lat / Lon rows with DMS + decimal
|
||||
CoordinateRow(label = "Lat", value = state.latitude, isLat = true)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
CoordinateRow(label = "Lon", value = state.longitude, isLat = false)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
CardButton(
|
||||
onClick = onGpsClick,
|
||||
text = "GPS 定位",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CoordinateRow(label: String, value: Double, isLat: Boolean) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 16.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color.Black,
|
||||
modifier = Modifier.width(44.dp)
|
||||
)
|
||||
Text(
|
||||
text = formatDms(value, isLat),
|
||||
fontSize = 15.sp,
|
||||
color = Color.Black
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = colorScheme.onSurface,
|
||||
modifier = Modifier.width(40.dp)
|
||||
)
|
||||
Text(text = formatDms(value, isLat), fontSize = 15.sp, color = colorScheme.onSurface)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${formatDecimal(value)}°",
|
||||
fontSize = 14.sp,
|
||||
color = LabelBlue
|
||||
color = colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 3x3 grid of 4-char squares. Column widths 21.4% : 56.2% : 21.4% and row
|
||||
* heights 31.5% : 35.9% : 31.7% mirror the reference app: the center column
|
||||
* is ~2.6x wider and the center row is the tallest, so the current square
|
||||
* dominates the panel.
|
||||
*/
|
||||
@Composable
|
||||
private fun GridPanel(state: RoamingState) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(GridBlue)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
// 3x3 grid: row 0 = north
|
||||
for (row in 0..2) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
for (col in 0..2) {
|
||||
val index = row * 3 + col
|
||||
val isCenter = index == 4
|
||||
GridCell(
|
||||
label = state.gridSquares.getOrElse(index) { "----" },
|
||||
isCenter = isCenter,
|
||||
showMarker = isCenter && state.qthLocator != "----",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (row < 2) Spacer(modifier = Modifier.height(4.dp))
|
||||
private fun GridCard(state: RoamingState) {
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
GridRow(state, row = 0, heightFraction = 0.315f)
|
||||
GridRow(state, row = 1, heightFraction = 0.359f)
|
||||
GridRow(state, row = 2, heightFraction = 0.317f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GridRow(state: RoamingState, row: Int, heightFraction: Float) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp)
|
||||
) {
|
||||
GridCell(
|
||||
label = state.gridSquares.getOrElse(row * 3 + 0) { "----" },
|
||||
isCenter = false,
|
||||
markerX = state.markerX,
|
||||
markerY = state.markerY,
|
||||
modifier = Modifier.weight(0.214f)
|
||||
)
|
||||
GridCell(
|
||||
label = state.gridSquares.getOrElse(row * 3 + 1) { "----" },
|
||||
isCenter = row == 1,
|
||||
markerX = state.markerX,
|
||||
markerY = state.markerY,
|
||||
showMarker = row == 1 && state.qthLocator != "----",
|
||||
modifier = Modifier.weight(0.562f)
|
||||
)
|
||||
GridCell(
|
||||
label = state.gridSquares.getOrElse(row * 3 + 2) { "----" },
|
||||
isCenter = false,
|
||||
markerX = state.markerX,
|
||||
markerY = state.markerY,
|
||||
modifier = Modifier.weight(0.214f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GridCell(
|
||||
label: String,
|
||||
isCenter: Boolean,
|
||||
showMarker: Boolean,
|
||||
markerX: Float,
|
||||
markerY: Float,
|
||||
showMarker: Boolean = false,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
val colorScheme = MaterialTheme.colorScheme
|
||||
val cellBackground = if (isCenter) colorScheme.secondaryContainer else colorScheme.surfaceVariant
|
||||
val labelColor = if (isCenter) colorScheme.onSecondaryContainer else colorScheme.onSurfaceVariant
|
||||
BoxWithConstraints(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.aspectRatio(1f)
|
||||
.background(if (isCenter) InfoCyan else GridBlue)
|
||||
.border(1.dp, DividerWhite)
|
||||
.height(84.dp)
|
||||
.clip(MaterialTheme.shapes.medium)
|
||||
.background(cellBackground)
|
||||
) {
|
||||
// Capture constraints at the BoxWithConstraints scope (Column scope would shadow them)
|
||||
val width = maxWidth
|
||||
val height = maxHeight
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = if (isCenter) 26.sp else 16.sp,
|
||||
fontSize = if (isCenter) 22.sp else 14.sp,
|
||||
fontWeight = if (isCenter) FontWeight.Bold else FontWeight.Normal,
|
||||
color = LabelBlue,
|
||||
color = labelColor,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
if (showMarker) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// Red marker at the fractional position inside the center cell
|
||||
val density = LocalDensity.current
|
||||
val offsetX = with(density) { (width * (markerX - 0.5f)).toPx().toDp() }
|
||||
val offsetY = with(density) { (height * (markerY - 0.5f)).toPx().toDp() }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.background(PositionRed)
|
||||
.border(2.dp, DividerWhite)
|
||||
.background(colorScheme.error)
|
||||
.offset(x = offsetX, y = offsetY)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+58
-11
@@ -27,19 +27,24 @@ import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.utility.positionToQth
|
||||
import com.rtbishop.look4sat.core.domain.utility.qthNeighbors
|
||||
import com.rtbishop.look4sat.core.domain.utility.qthToSquare
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class RoamingState(
|
||||
val latitude: Double = 0.0,
|
||||
val longitude: Double = 0.0,
|
||||
val altitude: Double = 0.0,
|
||||
val qthLocator: String = "----",
|
||||
val gridSquares: List<String> = List(9) { "----" },
|
||||
val isUpdating: Boolean = false,
|
||||
val messageResId: Int = 0
|
||||
/** Fractional position of the red marker inside the center cell, 0..1. */
|
||||
val markerX: Float = 0.5f,
|
||||
val markerY: Float = 0.5f,
|
||||
/** True when a fresh GPS fix is available (station position updated recently). */
|
||||
val gpsEnabled: Boolean = false,
|
||||
val isUpdating: Boolean = false
|
||||
)
|
||||
|
||||
class RoamingViewModel(private val settingsRepo: ISettingsRepo) : ViewModel() {
|
||||
@@ -47,19 +52,42 @@ class RoamingViewModel(private val settingsRepo: ISettingsRepo) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(RoamingState())
|
||||
val uiState: StateFlow<RoamingState> = _uiState
|
||||
|
||||
private var liveEnabled: Boolean = true
|
||||
|
||||
init {
|
||||
// Follow station position updates
|
||||
viewModelScope.launch {
|
||||
settingsRepo.stationPosition.collect { geoPos ->
|
||||
_uiState.update { it.copy(isUpdating = false, messageResId = 0).withPosition(geoPos) }
|
||||
_uiState.update { state ->
|
||||
state.copy(isUpdating = false, gpsEnabled = isFixFresh(geoPos)).withPosition(geoPos)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Follow the "live update" preference; when enabled, periodically refresh the fix
|
||||
viewModelScope.launch {
|
||||
settingsRepo.otherSettings.collect { other ->
|
||||
liveEnabled = other.stateOfRoamingLive
|
||||
if (liveEnabled) refreshPosition()
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(30_000L)
|
||||
if (liveEnabled) refreshPosition()
|
||||
_uiState.update { state ->
|
||||
state.copy(gpsEnabled = isFixFresh(settingsRepo.stationPosition.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onGpsClick() {
|
||||
val success = settingsRepo.setStationPosition()
|
||||
if (success) {
|
||||
_uiState.update { it.copy(isUpdating = true) }
|
||||
}
|
||||
/** Ask the settings repo for the latest fix (triggers GPS request if stale). */
|
||||
private fun refreshPosition() {
|
||||
settingsRepo.setStationPosition()
|
||||
}
|
||||
|
||||
private fun isFixFresh(pos: GeoPos): Boolean {
|
||||
return pos.timestamp > 0L && System.currentTimeMillis() - pos.timestamp < 600_000L
|
||||
}
|
||||
|
||||
private fun RoamingState.withPosition(pos: GeoPos): RoamingState {
|
||||
@@ -69,12 +97,31 @@ class RoamingViewModel(private val settingsRepo: ISettingsRepo) : ViewModel() {
|
||||
return copy(
|
||||
latitude = pos.latitude,
|
||||
longitude = pos.longitude,
|
||||
altitude = pos.altitude,
|
||||
qthLocator = locator,
|
||||
gridSquares = squares
|
||||
gridSquares = squares,
|
||||
markerX = markerFraction(pos.longitude, square, isLat = false),
|
||||
markerY = markerFraction(pos.latitude, square, isLat = true)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fractional position of [value] (lon or lat) inside its 4-char square.
|
||||
* A square spans 2° of longitude and 1° of latitude; the fraction is
|
||||
* measured from the square's south-west corner, 0..1.
|
||||
*/
|
||||
private fun markerFraction(value: Double, square: String, isLat: Boolean): Float {
|
||||
if (square.length != 4) return 0.5f
|
||||
val fieldIndex = (square[if (isLat) 1 else 0].uppercaseChar().code - 65).coerceIn(0, 17)
|
||||
val squareIndex = square[if (isLat) 3 else 2].digitToIntOrNull() ?: return 0.5f
|
||||
val cellSize = if (isLat) 1.0 else 2.0
|
||||
val cellStart = if (isLat) {
|
||||
fieldIndex * 10.0 + squareIndex - 90.0
|
||||
} else {
|
||||
fieldIndex * 20.0 + squareIndex * 2.0 - 180.0
|
||||
}
|
||||
return ((value - cellStart) / cellSize).coerceIn(0.0, 1.0).toFloat()
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun factory(container: IMainContainer) = viewModelFactory {
|
||||
initializer { RoamingViewModel(container.settingsRepo) }
|
||||
|
||||
+4
-1
@@ -488,7 +488,7 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(268.dp)
|
||||
.height(296.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
@@ -510,6 +510,9 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
|
||||
onAction(SettingsAction.ToggleNightMode(it))
|
||||
}
|
||||
SwitchRow(R.string.prefs_other_switch_roaming_live, settings.stateOfRoamingLive) {
|
||||
onAction(SettingsAction.ToggleRoamingLive(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ sealed interface SettingsAction {
|
||||
data class ToggleSensor(val value: Boolean) : SettingsAction
|
||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
||||
data class ToggleNightMode(val value: Boolean) : SettingsAction
|
||||
data class ToggleRoamingLive(val value: Boolean) : SettingsAction
|
||||
|
||||
// Remote control
|
||||
data class UpdateRC(val settings: RCSettings) : SettingsAction
|
||||
|
||||
+1
@@ -121,6 +121,7 @@ class SettingsViewModel(
|
||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
|
||||
is SettingsAction.ToggleRoamingLive -> settingsRepo.updateOtherSettings { it.copy(stateOfRoamingLive = action.value) }
|
||||
// Remote control & data sources
|
||||
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
|
||||
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
|
||||
|
||||
Reference in new issue
Block a user