mirror of
https://github.com/atsunatsu/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
feat(passes/radar): port pass history + compass calibration, cumulative what's new
- Pass history (Iu-yang1 fork): PassesSettings.hoursBefore look-back window; second 'Time before' slider in the pass filter; completed passes stay listed (progress clamped to [0,1]) in the history window. - Compass calibration (Iu-yang1 fork): OtherSettings.compassOffsetDegrees, CompassAccuracy from the magnetic sensor, calibration dialog with figure-eight accuracy meter, corrected heading and manual offset field; RadarViewModel applies the offset. - What's New dialog now shows the cumulative 4.4.6->4.4.7 changelog and follows the system language (values/ + values-zh/). - pass_filter_history_hours localized in es/ru/si/tr/uk.
This commit is contained in:
1 parent
3591960d55
commit
bf492e3f8b
20 files changed
+363
-29
No files matched your search
@@ -73,6 +73,7 @@ private val allModes = listOf(
|
||||
)
|
||||
|
||||
private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240)
|
||||
private val historyHourSteps = listOf(0) + hourSteps
|
||||
private const val dayMinutes = 24 * 60
|
||||
private const val minuteStep = 15
|
||||
private const val endOfDayMinute = dayMinutes - 1
|
||||
@@ -80,6 +81,7 @@ private const val quarterHourSlots = dayMinutes / minuteStep
|
||||
|
||||
data class PassFilterParams(
|
||||
val hours: Int,
|
||||
val hoursBefore: Int,
|
||||
val elevation: Double,
|
||||
val lowElevation: Double,
|
||||
val highElevation: Double,
|
||||
@@ -95,6 +97,7 @@ private fun PassesDialogPreview() {
|
||||
MainTheme {
|
||||
PassesFilterDialog(
|
||||
hours = 24,
|
||||
hoursBefore = 0,
|
||||
elevation = 16.0,
|
||||
lowElevation = 16.0,
|
||||
highElevation = 65.0,
|
||||
@@ -111,6 +114,7 @@ private fun PassesDialogPreview() {
|
||||
@Composable
|
||||
internal fun PassesFilterDialog(
|
||||
hours: Int,
|
||||
hoursBefore: Int,
|
||||
elevation: Double,
|
||||
lowElevation: Double,
|
||||
highElevation: Double,
|
||||
@@ -122,6 +126,9 @@ internal fun PassesFilterDialog(
|
||||
accept: (PassFilterParams) -> Unit
|
||||
) {
|
||||
val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) }
|
||||
val hoursBeforeIndex = remember {
|
||||
mutableIntStateOf(historyHourSteps.indexOfFirst { it >= hoursBefore }.coerceAtLeast(0))
|
||||
}
|
||||
val elevationValueNew = remember { mutableDoubleStateOf(elevation) }
|
||||
val highlightBounds = 0f..90f
|
||||
var highlightRange by remember(lowElevation, highElevation) {
|
||||
@@ -136,6 +143,7 @@ internal fun PassesFilterDialog(
|
||||
accept(
|
||||
PassFilterParams(
|
||||
hours = hourSteps[hoursIndex.intValue],
|
||||
hoursBefore = historyHourSteps[hoursBeforeIndex.intValue],
|
||||
elevation = elevationValueNew.doubleValue,
|
||||
lowElevation = highlightRange.start.roundToInt().toDouble(),
|
||||
highElevation = highlightRange.endInclusive.roundToInt().toDouble(),
|
||||
@@ -164,6 +172,14 @@ internal fun PassesFilterDialog(
|
||||
valueRange = 0f..(hourSteps.size - 1).toFloat(),
|
||||
steps = hourSteps.size - 2
|
||||
) { hoursIndex.intValue = it.toInt().coerceIn(0, hourSteps.size - 1) }
|
||||
SliderRow(
|
||||
title = stringResource(R.string.pass_filter_history_hours),
|
||||
value = hoursBeforeIndex.intValue.toDouble(),
|
||||
displayValue = formatHoursLabel(historyHourSteps[hoursBeforeIndex.intValue]),
|
||||
valueResId = R.drawable.ic_clock,
|
||||
valueRange = 0f..(historyHourSteps.size - 1).toFloat(),
|
||||
steps = historyHourSteps.size - 2
|
||||
) { hoursBeforeIndex.intValue = it.toInt().coerceIn(0, historyHourSteps.size - 1) }
|
||||
ToggleRow(
|
||||
title = stringResource(R.string.pass_filter_deep_space),
|
||||
checked = deepSpaceEnabled,
|
||||
|
||||
@@ -103,6 +103,7 @@ private fun PassesScreen(
|
||||
if (uiState.isPassesDialogShown) {
|
||||
PassesFilterDialog(
|
||||
hours = uiState.hours,
|
||||
hoursBefore = uiState.hoursBefore,
|
||||
elevation = uiState.elevation,
|
||||
lowElevation = uiState.lowElevation,
|
||||
highElevation = uiState.highElevation,
|
||||
@@ -115,6 +116,7 @@ private fun PassesScreen(
|
||||
onAction(
|
||||
PassesAction.FilterPasses(
|
||||
hoursAhead = params.hours,
|
||||
hoursBefore = params.hoursBefore,
|
||||
minElevation = params.elevation,
|
||||
lowElevation = params.lowElevation,
|
||||
highElevation = params.highElevation,
|
||||
|
||||
@@ -28,6 +28,8 @@ data class PassesState(
|
||||
val nextTime: String = "00:00:00",
|
||||
val isNextTimeAos: Boolean = true,
|
||||
val hours: Int = 24,
|
||||
/** 历史过境回看窗口(小时), 0 = 只看未来. */
|
||||
val hoursBefore: Int = 0,
|
||||
val elevation: Double = 16.0,
|
||||
val lowElevation: Double = 16.0,
|
||||
val highElevation: Double = 65.0,
|
||||
@@ -47,6 +49,7 @@ sealed interface PassesAction {
|
||||
data object DismissWhatsNew : PassesAction
|
||||
data class FilterPasses(
|
||||
val hoursAhead: Int,
|
||||
val hoursBefore: Int,
|
||||
val minElevation: Double,
|
||||
val lowElevation: Double,
|
||||
val highElevation: Double,
|
||||
|
||||
+14
-7
@@ -55,6 +55,7 @@ class PassesViewModel(
|
||||
isUtc = settingsRepo.otherSettings.value.stateOfUtc,
|
||||
nextPass = defaultPass,
|
||||
hours = settingsRepo.passesSettings.value.hoursAhead,
|
||||
hoursBefore = settingsRepo.passesSettings.value.hoursBefore,
|
||||
elevation = settingsRepo.passesSettings.value.minElevation,
|
||||
lowElevation = settingsRepo.otherSettings.value.lowElevation,
|
||||
highElevation = settingsRepo.otherSettings.value.highElevation,
|
||||
@@ -123,6 +124,7 @@ class PassesViewModel(
|
||||
is PassesAction.FilterPasses ->
|
||||
applyFilter(
|
||||
hoursAhead = action.hoursAhead,
|
||||
hoursBefore = action.hoursBefore,
|
||||
minElevation = action.minElevation,
|
||||
lowElevation = action.lowElevation,
|
||||
highElevation = action.highElevation,
|
||||
@@ -135,6 +137,7 @@ class PassesViewModel(
|
||||
is PassesAction.FilterRadios ->
|
||||
applyFilter(
|
||||
hoursAhead = _uiState.value.hours,
|
||||
hoursBefore = _uiState.value.hoursBefore,
|
||||
minElevation = _uiState.value.elevation,
|
||||
lowElevation = _uiState.value.lowElevation,
|
||||
highElevation = _uiState.value.highElevation,
|
||||
@@ -246,7 +249,7 @@ class PassesViewModel(
|
||||
return ordered
|
||||
}
|
||||
|
||||
/** Computes live progress for each pass, filtering out expired ones. */
|
||||
/** Computes live progress while retaining completed passes in the requested history window. */
|
||||
private fun computePassProgress(passList: List<OrbitalPass>, time: Long): List<OrbitalPass> {
|
||||
val result = ArrayList<OrbitalPass>(passList.size)
|
||||
for (pass in passList) {
|
||||
@@ -254,9 +257,9 @@ class PassesViewModel(
|
||||
val deltaNow = time.minus(pass.aosTime).toFloat()
|
||||
val deltaTotal = pass.losTime.minus(pass.aosTime).toFloat()
|
||||
val newProgress = (deltaNow / deltaTotal).round(2)
|
||||
if (newProgress >= 1.0f) continue
|
||||
if (newProgress != pass.progress) {
|
||||
result.add(pass.copy(progress = newProgress))
|
||||
val boundedProgress = newProgress.coerceIn(0f, 1f)
|
||||
if (boundedProgress != pass.progress) {
|
||||
result.add(pass.copy(progress = boundedProgress))
|
||||
} else {
|
||||
result.add(pass)
|
||||
}
|
||||
@@ -285,6 +288,7 @@ class PassesViewModel(
|
||||
|
||||
private fun applyFilter(
|
||||
hoursAhead: Int,
|
||||
hoursBefore: Int,
|
||||
minElevation: Double,
|
||||
lowElevation: Double,
|
||||
highElevation: Double,
|
||||
@@ -302,13 +306,15 @@ class PassesViewModel(
|
||||
aosStartMinute,
|
||||
aosEndMinute,
|
||||
invertAosTimeWindow,
|
||||
modes
|
||||
modes,
|
||||
hoursBefore
|
||||
)
|
||||
)
|
||||
settingsRepo.updateOtherSettings { it.copy(lowElevation = lowElevation, highElevation = highElevation) }
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
hours = hoursAhead,
|
||||
hoursBefore = hoursBefore,
|
||||
elevation = minElevation,
|
||||
lowElevation = lowElevation,
|
||||
highElevation = highElevation,
|
||||
@@ -319,9 +325,10 @@ class PassesViewModel(
|
||||
modes = modes
|
||||
)
|
||||
}
|
||||
val now = System.currentTimeMillis()
|
||||
satelliteRepo.calculatePasses(
|
||||
time = System.currentTimeMillis(),
|
||||
hoursAhead = hoursAhead,
|
||||
time = now - hoursBefore * 60L * 60L * 1000L,
|
||||
hoursAhead = hoursBefore + hoursAhead,
|
||||
minElevation = minElevation,
|
||||
aosStartMinute = aosStartMinute,
|
||||
aosEndMinute = aosEndMinute,
|
||||
|
||||
@@ -82,7 +82,7 @@ class RadarViewModel(
|
||||
private val _uiState = MutableStateFlow(
|
||||
RadarState(
|
||||
isUtc = settingsRepo.otherSettings.value.stateOfUtc,
|
||||
orientationValues = sensorsRepo.sensorData.value,
|
||||
orientationValues = correctedOrientation(sensorsRepo.sensorData.value),
|
||||
shouldShowSweep = settingsRepo.otherSettings.value.stateOfSweep,
|
||||
shouldUseCompass = settingsRepo.otherSettings.value.stateOfSensors,
|
||||
sstv = SstvSubState(selectedMode = settingsRepo.otherSettings.value.sstvMode)
|
||||
@@ -102,12 +102,19 @@ class RadarViewModel(
|
||||
viewModelScope.launch {
|
||||
sensorsRepo.enableSensor()
|
||||
sensorsRepo.sensorData.collect { data ->
|
||||
val orientationValues = (data.first + magDeclination) to data.second
|
||||
val orientationValues = correctedOrientation(data)
|
||||
_uiState.update { it.copy(orientationValues = orientationValues) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 指南针方位 = 传感器方位 + 磁偏角 + 手动偏置(compassOffsetDegrees, 校准用). */
|
||||
private fun correctedOrientation(data: Pair<Float, Float>): Pair<Float, Float> {
|
||||
val offset = settingsRepo.otherSettings.value.compassOffsetDegrees
|
||||
val azimuth = ((data.first + magDeclination + offset) % 360f + 360f) % 360f
|
||||
return azimuth to data.second
|
||||
}
|
||||
|
||||
private fun collectSettingsChanges() {
|
||||
viewModelScope.launch {
|
||||
settingsRepo.otherSettings.collectLatest { settings ->
|
||||
|
||||
+129
-5
@@ -38,8 +38,10 @@ import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -56,6 +58,8 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -66,9 +70,11 @@ import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.CompassAccuracy
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||
import com.rtbishop.look4sat.core.presentation.SharedDialog
|
||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import com.rtbishop.look4sat.core.presentation.PrimaryIconCard
|
||||
@@ -204,6 +210,19 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
onSave = { onAction(SettingsAction.UpdateRadioControl(it)) }
|
||||
)
|
||||
}
|
||||
if (dialogs.compassCalibration) {
|
||||
LaunchedEffect(Unit) { onAction(SettingsAction.StartCompassCalibration) }
|
||||
CompassCalibrationDialog(
|
||||
accuracy = uiState.compassAccuracy,
|
||||
headingDegrees = uiState.compassHeadingDegrees,
|
||||
initialOffsetDegrees = uiState.otherSettings.compassOffsetDegrees,
|
||||
onDismiss = {
|
||||
onAction(SettingsAction.StopCompassCalibration)
|
||||
dialogs.compassCalibration = false
|
||||
},
|
||||
onSave = { onAction(SettingsAction.SetCompassOffset(it)) }
|
||||
)
|
||||
}
|
||||
if (dialogs.lotw) {
|
||||
// LoTW sync failures are typed codes from the ViewModel; render them
|
||||
// through string resources so the dialog follows the system language.
|
||||
@@ -342,7 +361,13 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
|
||||
showLoTWDialog = { dialogs.lotw = true }
|
||||
)
|
||||
}
|
||||
item { OtherCard(uiState.otherSettings, onAction) }
|
||||
item {
|
||||
OtherCard(
|
||||
settings = uiState.otherSettings,
|
||||
onCompassCalibration = { dialogs.compassCalibration = true },
|
||||
onAction = onAction
|
||||
)
|
||||
}
|
||||
item { CardCredits() }
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
CardButton(
|
||||
@@ -530,15 +555,19 @@ private fun OtherCardPreview() = MainTheme {
|
||||
shouldSeeWarning = false,
|
||||
shouldSeeWhatsNew = false
|
||||
)
|
||||
OtherCard(settings = values) {}
|
||||
OtherCard(settings = values, onCompassCalibration = {}, onAction = {})
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) {
|
||||
private fun OtherCard(
|
||||
settings: OtherSettings,
|
||||
onCompassCalibration: () -> Unit,
|
||||
onAction: (SettingsAction) -> Unit
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(320.dp)
|
||||
.height(370.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
|
||||
Text(
|
||||
@@ -560,6 +589,11 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
SwitchRow(R.string.prefs_other_switch_sensors, settings.stateOfSensors) {
|
||||
onAction(SettingsAction.ToggleSensor(it))
|
||||
}
|
||||
CardButton(
|
||||
onClick = onCompassCalibration,
|
||||
text = stringResource(R.string.prefs_compass_calibration_button),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
SwitchRow(R.string.prefs_other_switch_night_mode, settings.stateOfNightMode) {
|
||||
onAction(SettingsAction.ToggleNightMode(it))
|
||||
}
|
||||
@@ -567,6 +601,94 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指南针校准对话框: 8 字动作提升精度, 实时显示校正后航向, 可手动输入偏置(±180°).
|
||||
*/
|
||||
@Composable
|
||||
private fun CompassCalibrationDialog(
|
||||
accuracy: CompassAccuracy,
|
||||
headingDegrees: Float,
|
||||
initialOffsetDegrees: Float,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (Float) -> Unit
|
||||
) {
|
||||
var offsetText by rememberSaveable(initialOffsetDegrees) {
|
||||
mutableStateOf(initialOffsetDegrees.toString())
|
||||
}
|
||||
val parsedOffset = offsetText.replace(',', '.').toFloatOrNull()?.takeIf { it in -180f..180f }
|
||||
val statusRes = when (accuracy) {
|
||||
CompassAccuracy.HIGH -> R.string.prefs_compass_accuracy_high
|
||||
CompassAccuracy.MEDIUM -> R.string.prefs_compass_accuracy_medium
|
||||
CompassAccuracy.LOW -> R.string.prefs_compass_accuracy_low
|
||||
CompassAccuracy.UNRELIABLE -> R.string.prefs_compass_accuracy_unreliable
|
||||
CompassAccuracy.UNAVAILABLE -> R.string.prefs_compass_accuracy_unavailable
|
||||
}
|
||||
val progress = when (accuracy) {
|
||||
CompassAccuracy.HIGH -> 1f
|
||||
CompassAccuracy.MEDIUM -> 0.66f
|
||||
CompassAccuracy.LOW -> 0.33f
|
||||
CompassAccuracy.UNRELIABLE, CompassAccuracy.UNAVAILABLE -> 0f
|
||||
}
|
||||
SharedDialog(
|
||||
title = stringResource(R.string.prefs_compass_calibration_title),
|
||||
onDismissRequest = onDismiss,
|
||||
onCancel = onDismiss,
|
||||
onAccept = {
|
||||
parsedOffset?.let {
|
||||
onSave(it)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.padding(horizontal = padding)
|
||||
) {
|
||||
Text(
|
||||
text = "∞",
|
||||
fontSize = 92.sp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
lineHeight = 92.sp
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_compass_calibration_instruction),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_compass_accuracy, stringResource(statusRes)),
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = if (accuracy == CompassAccuracy.HIGH) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurface
|
||||
}
|
||||
)
|
||||
LinearProgressIndicator(progress = { progress }, modifier = Modifier.fillMaxWidth())
|
||||
Text(
|
||||
text = stringResource(R.string.prefs_compass_heading, headingDegrees),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = offsetText,
|
||||
onValueChange = { offsetText = it },
|
||||
label = { Text(stringResource(R.string.prefs_compass_offset)) },
|
||||
supportingText = { Text(stringResource(R.string.prefs_compass_offset_support)) },
|
||||
isError = parsedOffset == null,
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal, imeAction = ImeAction.Done),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
CardButton(
|
||||
onClick = { offsetText = "0" },
|
||||
text = stringResource(R.string.prefs_compass_offset_reset),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
@@ -741,6 +863,7 @@ private class DialogVisibility {
|
||||
var radioControl by mutableStateOf(false)
|
||||
var wavelog by mutableStateOf(false)
|
||||
var lotw by mutableStateOf(false)
|
||||
var compassCalibration by mutableStateOf(false)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -748,13 +871,14 @@ private fun rememberDialogVisibility(): DialogVisibility {
|
||||
return rememberSaveable(saver = run {
|
||||
androidx.compose.runtime.saveable.Saver(
|
||||
save = {
|
||||
listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog, it.lotw)
|
||||
listOf(it.position, it.locator, it.dataSources, it.network, it.bluetooth, it.radioControl, it.wavelog, it.lotw, it.compassCalibration)
|
||||
},
|
||||
restore = {
|
||||
DialogVisibility().apply {
|
||||
position = it[0]; locator = it[1]; dataSources = it[2]
|
||||
network = it[3]; bluetooth = it[4]; radioControl = it[5]; wavelog = it[6]
|
||||
lotw = it.getOrElse(7) { false }
|
||||
compassCalibration = it.getOrElse(8) { false }
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.rtbishop.look4sat.core.domain.model.RCSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
|
||||
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.repository.CompassAccuracy
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWSyncMode
|
||||
import java.io.File
|
||||
|
||||
@@ -67,6 +68,10 @@ data class SettingsState(
|
||||
val lotwError: LoTWError? = null,
|
||||
/** Epoch ms of the last successful LoTW sync (0 = never) — shown like the ephemeris update time. */
|
||||
val lotwLastSyncEpochMs: Long = 0L,
|
||||
/** 指南针校准精度等级 (校准对话框进度条). */
|
||||
val compassAccuracy: CompassAccuracy = CompassAccuracy.UNRELIABLE,
|
||||
/** 校正后航向(度, 含磁偏角+手动偏置), 校准对话框实时显示. */
|
||||
val compassHeadingDegrees: Float = 0f,
|
||||
val updateChecker: UpdateCheckerState = UpdateCheckerState()
|
||||
)
|
||||
|
||||
@@ -98,6 +103,9 @@ sealed interface SettingsAction {
|
||||
data class ToggleAutoLotwSync(val value: Boolean) : SettingsAction
|
||||
data class ToggleSweep(val value: Boolean) : SettingsAction
|
||||
data class ToggleSensor(val value: Boolean) : SettingsAction
|
||||
data object StartCompassCalibration : SettingsAction
|
||||
data object StopCompassCalibration : SettingsAction
|
||||
data class SetCompassOffset(val degrees: Float) : SettingsAction
|
||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
||||
data class ToggleNightMode(val value: Boolean) : SettingsAction
|
||||
|
||||
|
||||
+42
-2
@@ -25,6 +25,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
|
||||
import com.rtbishop.look4sat.core.domain.model.WavelogSettings
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISensorsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository
|
||||
import com.rtbishop.look4sat.core.domain.repository.LoTWResult
|
||||
@@ -49,6 +50,7 @@ class SettingsViewModel(
|
||||
private val updateRepo: IUpdateRepository,
|
||||
private val wavelogRepo: IWavelogRepository,
|
||||
private val lotwRepo: com.rtbishop.look4sat.core.domain.repository.ILoTWRepository,
|
||||
private val sensorsRepo: ISensorsRepo,
|
||||
private val apkFile: File,
|
||||
private val showToast: IShowToast
|
||||
) : ViewModel() {
|
||||
@@ -57,6 +59,8 @@ class SettingsViewModel(
|
||||
private val defaultDataSettings = DataSettings(false, 0, 0, 0L)
|
||||
/** In-flight LoTW sync job, cancelled by CancelLoTWSync. */
|
||||
private var lotwSyncJob: kotlinx.coroutines.Job? = null
|
||||
/** 指南针校准: 传感器原始方位(未加磁偏角/偏置), 校准对话框实时显示校正后航向. */
|
||||
private var rawCompassHeading = sensorsRepo.sensorData.value.first
|
||||
private val _uiState = MutableStateFlow(
|
||||
SettingsState(
|
||||
appVersionName = settingsRepo.appVersionName,
|
||||
@@ -69,6 +73,8 @@ class SettingsViewModel(
|
||||
dataSourcesStatus = settingsRepo.dataSourcesStatus.value,
|
||||
wavelogSettings = settingsRepo.wavelogSettings.value,
|
||||
workedGridsCount = settingsRepo.getWorkedGrids().size,
|
||||
compassAccuracy = sensorsRepo.compassAccuracy.value,
|
||||
compassHeadingDegrees = correctedCompassHeading(),
|
||||
lotwLastSyncEpochMs = lotwCursorEpochMs(settingsRepo.getLastLotwSyncDate())
|
||||
)
|
||||
)
|
||||
@@ -79,7 +85,10 @@ class SettingsViewModel(
|
||||
viewModelScope.launch {
|
||||
settingsRepo.stationPosition.collect { geoPos ->
|
||||
_uiState.update {
|
||||
it.copy(positionSettings = it.positionSettings.copy(isUpdating = false, stationPos = geoPos))
|
||||
it.copy(
|
||||
positionSettings = it.positionSettings.copy(isUpdating = false, stationPos = geoPos),
|
||||
compassHeadingDegrees = correctedCompassHeading()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,7 +113,20 @@ class SettingsViewModel(
|
||||
}
|
||||
viewModelScope.launch {
|
||||
settingsRepo.otherSettings.collect { settings ->
|
||||
_uiState.update { it.copy(otherSettings = settings) }
|
||||
_uiState.update {
|
||||
it.copy(otherSettings = settings, compassHeadingDegrees = correctedCompassHeading())
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
sensorsRepo.compassAccuracy.collect { accuracy ->
|
||||
_uiState.update { it.copy(compassAccuracy = accuracy) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
sensorsRepo.sensorData.collect { orientation ->
|
||||
rawCompassHeading = orientation.first
|
||||
_uiState.update { it.copy(compassHeadingDegrees = correctedCompassHeading()) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
@@ -157,6 +179,11 @@ class SettingsViewModel(
|
||||
is SettingsAction.ToggleAutoLotwSync -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoLotwSync = action.value) }
|
||||
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||
SettingsAction.StartCompassCalibration -> sensorsRepo.enableSensor()
|
||||
SettingsAction.StopCompassCalibration -> sensorsRepo.disableSensor()
|
||||
is SettingsAction.SetCompassOffset -> settingsRepo.updateOtherSettings {
|
||||
it.copy(compassOffsetDegrees = action.degrees.coerceIn(-180f, 180f))
|
||||
}
|
||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
|
||||
// Remote control & data sources
|
||||
@@ -358,6 +385,18 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
/** 校正后航向 = 传感器原始方位 + 磁偏角 + 手动偏置. */
|
||||
private fun correctedCompassHeading(): Float {
|
||||
val declination = sensorsRepo.getMagDeclination(settingsRepo.stationPosition.value)
|
||||
val offset = settingsRepo.otherSettings.value.compassOffsetDegrees
|
||||
return ((rawCompassHeading + declination + offset) % 360f + 360f) % 360f
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
sensorsRepo.disableSensor()
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Data update helpers — consolidated from 3 near-identical functions
|
||||
@@ -405,6 +444,7 @@ class SettingsViewModel(
|
||||
updateRepo = container.updateRepo,
|
||||
wavelogRepo = container.wavelogRepo,
|
||||
lotwRepo = container.lotwRepo,
|
||||
sensorsRepo = container.provideSensorsRepo(),
|
||||
apkFile = File(context.cacheDir, "look4sat-update.apk"),
|
||||
showToast = container.provideShowToast()
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user