feat(amsat): let the operator choose the day-cell style
Opinion split on the stripes, so Settings > Other now has a switch. On by default, since the flat tile it replaced hid intra-day outages, which is the problem the stripes were introduced to solve. Flat mode is deliberately not the old behaviour. The old cell took its colour from the first slot with a report and its count from that same slot, so a day that worked in the morning and failed all afternoon read as "worked" - measured across eight representative day shapes, two of them had their failure hidden outright, and the count reported 1 where the day held 24 reports. Flat mode now takes the day's worst status and the day's total count, so the summary can understate detail but not hide bad news. The help text says so, in case someone turns the switch off expecting the tile they remember. The count is drawn in black or white by relative luminance rather than always white: on the telemetry amber, white measured 1.83:1 against WCAG's 3:1 for large text, and that cell does carry a count whenever a day held nothing but telemetry reports. All six status colours now clear 3:1, the worst being 3.03. SatStatusViewModel collects the setting rather than reading it once - the switch is on another screen, so the operator is always elsewhere when they change it and would otherwise return to the old style. Strings in all nine locales.
This commit is contained in:
1 parent
4cb03111bc
commit
984a139a81
16 files changed
+135
-12
No files matched your search
@@ -106,6 +106,7 @@ class SettingsRepo(
|
||||
private val keyRadarCompassOffset = "radarCompassOffset"
|
||||
private val keyRadarCompassOffsetElev = "radarCompassOffsetElev"
|
||||
private val keyCwToneShiftEnabled = "cwToneShiftEnabled"
|
||||
private val keyAmsatDayStripes = "amsatDayStripes"
|
||||
|
||||
private val separatorComma = ","
|
||||
|
||||
@@ -407,6 +408,7 @@ class SettingsRepo(
|
||||
putFloat(keyRadarCompassOffset, new.radarCompassOffset)
|
||||
putFloat(keyRadarCompassOffsetElev, new.radarCompassOffsetElev)
|
||||
putBoolean(keyCwToneShiftEnabled, new.cwToneShiftEnabled)
|
||||
putBoolean(keyAmsatDayStripes, new.amsatDayStripes)
|
||||
|
||||
}
|
||||
new
|
||||
@@ -434,7 +436,8 @@ class SettingsRepo(
|
||||
wavelogAutoUpload = preferences.getBoolean(keyWavelogAutoUpload, false),
|
||||
radarCompassOffset = preferences.getFloat(keyRadarCompassOffset, 0f),
|
||||
radarCompassOffsetElev = preferences.getFloat(keyRadarCompassOffsetElev, 0f),
|
||||
cwToneShiftEnabled = preferences.getBoolean(keyCwToneShiftEnabled, false)
|
||||
cwToneShiftEnabled = preferences.getBoolean(keyCwToneShiftEnabled, false),
|
||||
amsatDayStripes = preferences.getBoolean(keyAmsatDayStripes, true)
|
||||
)
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -82,7 +82,16 @@ data class OtherSettings(
|
||||
* it before decoding. Off by default: when disabled the audio path is unchanged,
|
||||
* and a tone already inside the window is never touched either way.
|
||||
*/
|
||||
val cwToneShiftEnabled: Boolean = false
|
||||
val cwToneShiftEnabled: Boolean = false,
|
||||
|
||||
/**
|
||||
* Draw each AMSAT day as twelve two-hour stripes rather than one colour.
|
||||
*
|
||||
* On by default: a single colour is taken from the first slot with a report, so a
|
||||
* satellite that worked all morning and failed all afternoon looks identical to one
|
||||
* that worked once. Some operators prefer the older, simpler tile, hence the switch.
|
||||
*/
|
||||
val amsatDayStripes: Boolean = true
|
||||
)
|
||||
|
||||
data class DataSourcesSettings(
|
||||
|
||||
@@ -173,4 +173,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW solo analiza 400-1200 Hz. Si está activo, un tono fuera de ese rango se traslada al rango antes de decodificar; un tono que ya está dentro no se modifica.</string>
|
||||
<string name="amsat_no_report_legend">Sin informes</string>
|
||||
<string name="amsat_no_data_legend">Sin datos</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: una franja por franja de 2 horas</string>
|
||||
<string name="prefs_other_amsat_stripes_help">Activado, cada día son doce franjas de dos horas, así se ve una interrupción dentro del día. Desactivado, cada día es un color y un recuento de informes; el color es el peor estado del día, así que un solo fallo sigue viéndose.</string>
|
||||
</resources>
|
||||
@@ -306,4 +306,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW hanya menganalisis 400-1200 Hz. Saat aktif, nada di luar rentang itu dipindahkan ke dalamnya sebelum decoding; nada yang sudah di dalam tidak diubah.</string>
|
||||
<string name="amsat_no_report_legend">Tidak ada laporan</string>
|
||||
<string name="amsat_no_data_legend">Tidak ada data</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: satu garis per slot 2 jam</string>
|
||||
<string name="prefs_other_amsat_stripes_help">Saat aktif, setiap hari adalah dua belas garis dua jam, sehingga pemadaman dalam satu hari terlihat. Saat mati, setiap hari adalah satu warna dan jumlah laporan - warnanya status terburuk hari itu, jadi satu kegagalan pun tetap terlihat.</string>
|
||||
</resources>
|
||||
@@ -306,4 +306,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW hanya menganalisis 400-1200 Hz. Saat aktif, nada di luar rentang itu dipindahkan ke dalamnya sebelum decoding; nada yang sudah di dalam tidak diubah.</string>
|
||||
<string name="amsat_no_report_legend">Tidak ada laporan</string>
|
||||
<string name="amsat_no_data_legend">Tidak ada data</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: satu garis per slot 2 jam</string>
|
||||
<string name="prefs_other_amsat_stripes_help">Saat aktif, setiap hari adalah dua belas garis dua jam, sehingga pemadaman dalam satu hari terlihat. Saat mati, setiap hari adalah satu warna dan jumlah laporan - warnanya status terburuk hari itu, jadi satu kegagalan pun tetap terlihat.</string>
|
||||
</resources>
|
||||
@@ -173,4 +173,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW анализирует только 400-1200 Гц. Если включено, тон вне этого диапазона переносится внутрь перед декодированием; тон внутри диапазона не изменяется.</string>
|
||||
<string name="amsat_no_report_legend">Нет отчётов</string>
|
||||
<string name="amsat_no_data_legend">Нет данных</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: полоса на каждые 2 часа</string>
|
||||
<string name="prefs_other_amsat_stripes_help">Включено — каждый день это двенадцать двухчасовых полос, поэтому перерыв внутри дня виден. Выключено — каждый день это один цвет и число отчётов; цвет соответствует худшему состоянию за день, так что даже один сбой остаётся заметен.</string>
|
||||
</resources>
|
||||
@@ -173,4 +173,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW විශ්ලේෂණය කරන්නේ 400-1200 Hz පමණි. සක්රීය විට, එම පරාසයෙන් පිටත ස්වරයක් විකේතනයට පෙර පරාසය තුළට ගෙන එයි; දැනටමත් පරාසය තුළ ඇති ස්වරයක් වෙනස් නොකරයි.</string>
|
||||
<string name="amsat_no_report_legend">වාර්තා නැත</string>
|
||||
<string name="amsat_no_data_legend">දත්ත නැත</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: පැය 2 කට එක තීරුවක්</string>
|
||||
<string name="prefs_other_amsat_stripes_help">සක්රිය විට සෑම දිනයක් පැය දෙකේ තීරු දොළහකි, එබැවින් දිනක් තුළ ඇති බිඳවැටීම දැකිය හැක. අක්රිය විට සෑම දිනයක් එක් වර්ණයක් සහ වාර්තා ගණනකි — වර්ණය එදින නරකම තත්ත්වයයි, එබැවින් එක් අසාර්ථකත්වයක් වුවද පෙනේ.</string>
|
||||
</resources>
|
||||
@@ -318,4 +318,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW yalnızca 400-1200 Hz analiz eder. Açıkken bu aralığın dışındaki bir ton çözülmeden önce aralığa taşınır; aralıkta olan ton değiştirilmez.</string>
|
||||
<string name="amsat_no_report_legend">Rapor yok</string>
|
||||
<string name="amsat_no_data_legend">Veri yok</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: 2 saatlik dilim başına şerit</string>
|
||||
<string name="prefs_other_amsat_stripes_help">Açıkken her gün on iki iki saatlik şerittir, böylece gün içindeki kesinti görünür. Kapalıyken her gün tek renk ve rapor sayısıdır; renk günün en kötü durumudur, yani tek bir arıza bile görünür kalır.</string>
|
||||
</resources>
|
||||
@@ -173,4 +173,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW аналізує лише 400-1200 Гц. Якщо увімкнено, тон поза цим діапазоном переноситься в нього перед декодуванням; тон, що вже в діапазоні, не змінюється.</string>
|
||||
<string name="amsat_no_report_legend">Немає звітів</string>
|
||||
<string name="amsat_no_data_legend">Немає даних</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: смуга на кожні 2 години</string>
|
||||
<string name="prefs_other_amsat_stripes_help">Увімкнено — кожен день це дванадцять двогодинних смуг, тож перерва всередині дня видна. Вимкнено — кожен день це один колір і кількість звітів; колір відповідає найгіршому стану за день, тож навіть один збій залишається помітним.</string>
|
||||
</resources>
|
||||
@@ -308,4 +308,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW 仅分析 400-1200 Hz。开启后,超出该范围的音调会先搬移到范围内再解码;已在范围内的音调不作处理。</string>
|
||||
<string name="amsat_no_report_legend">无人上报</string>
|
||||
<string name="amsat_no_data_legend">无数据</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT:每 2 小时一条纹</string>
|
||||
<string name="prefs_other_amsat_stripes_help">开启时每天画成 12 条两小时条纹,一天之内的中断也看得见。关闭时每天显示一个颜色和报告总数——颜色取当天最差状态,所以出现过一次故障也不会被藏起来。</string>
|
||||
</resources>
|
||||
@@ -345,4 +345,6 @@
|
||||
<string name="prefs_other_cw_tone_shift_help">DeepCW only analyses 400-1200 Hz. When on, a tone outside that range is moved into it before decoding; a tone already inside is untouched.</string>
|
||||
<string name="amsat_no_report_legend">No report</string>
|
||||
<string name="amsat_no_data_legend">No data</string>
|
||||
<string name="prefs_other_switch_amsat_stripes">AMSAT: stripe per 2-hour slot</string>
|
||||
<string name="prefs_other_amsat_stripes_help">On, each day is twelve two-hour stripes, so an outage inside a day is visible. Off, each day is one colour and a report count - the colour is the day\'s worst status, so a single failure still shows.</string>
|
||||
</resources>
|
||||
+10
@@ -646,6 +646,16 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// AMSAT status: twelve two-hour stripes per day, or one colour for the day
|
||||
SwitchRow(R.string.prefs_other_switch_amsat_stripes, settings.amsatDayStripes) {
|
||||
onAction(SettingsAction.ToggleAmsatDayStripes(it))
|
||||
}
|
||||
Text(
|
||||
text = stringResource(id = R.string.prefs_other_amsat_stripes_help),
|
||||
fontSize = 12.sp,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
// Compass calibration sliders at the bottom
|
||||
CompassOffsetRow(
|
||||
labelResId = R.string.prefs_other_compass_offset,
|
||||
|
||||
@@ -64,6 +64,7 @@ sealed interface SettingsAction {
|
||||
|
||||
/** Shift CW tones outside the model's 400-1200 Hz window into it before decoding. */
|
||||
data class ToggleCwToneShift(val value: Boolean) : SettingsAction
|
||||
data class ToggleAmsatDayStripes(val value: Boolean) : SettingsAction
|
||||
data class ToggleSweep(val value: Boolean) : SettingsAction
|
||||
data class ToggleSensor(val value: Boolean) : SettingsAction
|
||||
data class ToggleLightTheme(val value: Boolean) : SettingsAction
|
||||
|
||||
+1
@@ -128,6 +128,7 @@ class SettingsViewModel(
|
||||
is SettingsAction.ToggleUtc -> settingsRepo.updateOtherSettings { it.copy(stateOfUtc = action.value) }
|
||||
is SettingsAction.ToggleUpdate -> settingsRepo.updateOtherSettings { it.copy(stateOfAutoUpdate = action.value) }
|
||||
is SettingsAction.ToggleCwToneShift -> settingsRepo.updateOtherSettings { it.copy(cwToneShiftEnabled = action.value) }
|
||||
is SettingsAction.ToggleAmsatDayStripes -> settingsRepo.updateOtherSettings { it.copy(amsatDayStripes = action.value) }
|
||||
is SettingsAction.ToggleSweep -> settingsRepo.updateOtherSettings { it.copy(stateOfSweep = action.value) }
|
||||
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
|
||||
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
|
||||
|
||||
+70
-6
@@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@@ -114,7 +115,11 @@ private fun SatStatusScreen(uiState: SatStatusUiState, refresh: () -> Unit) {
|
||||
HorizontalDivider(thickness = 1.dp)
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(uiState.statuses, key = { it.name }) { status ->
|
||||
StatusRow(status = status, onClickDay = { day -> selectedDay = status to day })
|
||||
StatusRow(
|
||||
status = status,
|
||||
stripes = uiState.dayStripes,
|
||||
onClickDay = { day -> selectedDay = status to day }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -239,7 +244,7 @@ private fun HeaderRow(statuses: List<SatStatus>) {
|
||||
|
||||
/** Satellite row: name takes remaining width; day tiles are fixed-width (tablet-safe). */
|
||||
@Composable
|
||||
private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) {
|
||||
private fun StatusRow(status: SatStatus, stripes: Boolean, onClickDay: (SatDay) -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
@@ -269,6 +274,7 @@ private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) {
|
||||
status.days.forEach { day ->
|
||||
DayCell(
|
||||
day = day,
|
||||
stripes = stripes,
|
||||
modifier = Modifier.width(TILE_WIDTH).padding(horizontal = 2.dp),
|
||||
onClick = { onClickDay(day) }
|
||||
)
|
||||
@@ -276,6 +282,37 @@ private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
private const val NO_REPORT_COLOUR = 0xFFC0C0C0
|
||||
|
||||
/**
|
||||
* Status colours worst first, for collapsing a day to one of them.
|
||||
*
|
||||
* Ordered by how much the operator needs to know about it: a reported failure outranks a
|
||||
* partial contact, which outranks a success, and both greys come last because they are
|
||||
* absences rather than observations. Duplicated from AmSatRepository, which owns these
|
||||
* literals — see the note in AGENTS.md.
|
||||
*/
|
||||
private val SEVERITY = listOf(
|
||||
0xFFDC267F, // not heard
|
||||
0xFFFE6100, // unrecognised status
|
||||
0xFFFFB000, // telemetry only
|
||||
0xFF648FFF, // heard
|
||||
NO_REPORT_COLOUR,
|
||||
0xFFE8E8E8 // no data fetched
|
||||
)
|
||||
|
||||
/**
|
||||
* Black or white for text on [background], whichever reads better.
|
||||
*
|
||||
* White is not safe on all of the status colours: on the telemetry amber it measures
|
||||
* 1.83:1 against WCAG's 3:1 for large text, and that cell does carry a count whenever a
|
||||
* day held nothing but telemetry reports. Relative luminance decides it instead.
|
||||
*/
|
||||
private fun readableOn(background: Long): Color {
|
||||
val colour = Color(background)
|
||||
return if (colour.luminance() > 0.4f) Color(0xFF1A1A1A) else Color.White
|
||||
}
|
||||
|
||||
/**
|
||||
* One day as a stripe per two-hour slot.
|
||||
*
|
||||
@@ -291,13 +328,14 @@ private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) {
|
||||
* the grid.
|
||||
*/
|
||||
@Composable
|
||||
private fun DayCell(day: SatDay, modifier: Modifier, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
private fun DayCell(day: SatDay, stripes: Boolean, modifier: Modifier, onClick: () -> Unit) {
|
||||
val cell = modifier
|
||||
.height(28.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
|
||||
if (stripes) {
|
||||
Row(modifier = cell) {
|
||||
day.slots.forEach { slot ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -307,6 +345,32 @@ private fun DayCell(day: SatDay, modifier: Modifier, onClick: () -> Unit) {
|
||||
)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// One colour for the whole day, for operators who preferred the original tile.
|
||||
// The colour is the day's worst status rather than its first reported one: picking
|
||||
// the first hid outages behind an earlier good report, which is what the stripes
|
||||
// were introduced to expose, and a summary that hides bad news is worse than none.
|
||||
// An unlisted colour sorts last rather than first: indexOf would return -1 and make
|
||||
// anything unrecognised outrank a genuine failure.
|
||||
val colour = day.slots.map { it.statusColor }
|
||||
.minByOrNull { SEVERITY.indexOf(it).takeIf { i -> i >= 0 } ?: SEVERITY.size }
|
||||
?: NO_REPORT_COLOUR
|
||||
val count = day.slots.sumOf { it.count }
|
||||
Box(
|
||||
modifier = cell.background(Color(colour)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (count > 0) {
|
||||
Text(
|
||||
text = count.toString(),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = readableOn(colour)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Report detail dialog (callsign / date / time / grid) */
|
||||
|
||||
+21
-4
@@ -8,6 +8,7 @@ import com.rtbishop.look4sat.core.domain.model.SatReport
|
||||
import com.rtbishop.look4sat.core.domain.model.SatStatus
|
||||
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
|
||||
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
|
||||
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -19,17 +20,33 @@ data class SatStatusUiState(
|
||||
val statuses: List<SatStatus> = emptyList(),
|
||||
val reports: Map<String, SatReport> = emptyMap(),
|
||||
val fetchedAtUtcMs: Long = 0L,
|
||||
val error: String? = null
|
||||
val error: String? = null,
|
||||
/** Draw each day as twelve two-hour stripes rather than a single colour. */
|
||||
val dayStripes: Boolean = true
|
||||
)
|
||||
|
||||
class SatStatusViewModel(
|
||||
private val amSatRepo: IAmSatRepository
|
||||
private val amSatRepo: IAmSatRepository,
|
||||
settingsRepo: ISettingsRepo
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SatStatusUiState(isLoading = true))
|
||||
private val _uiState = MutableStateFlow(
|
||||
SatStatusUiState(
|
||||
isLoading = true,
|
||||
dayStripes = settingsRepo.otherSettings.value.amsatDayStripes
|
||||
)
|
||||
)
|
||||
val uiState: StateFlow<SatStatusUiState> = _uiState
|
||||
|
||||
init {
|
||||
// Collected rather than read once: the switch lives in Settings, so the operator
|
||||
// is on another screen when they change it and would otherwise come back to the
|
||||
// old style until the page was rebuilt.
|
||||
viewModelScope.launch {
|
||||
settingsRepo.otherSettings.collect { other ->
|
||||
_uiState.update { it.copy(dayStripes = other.amsatDayStripes) }
|
||||
}
|
||||
}
|
||||
fetch()
|
||||
}
|
||||
|
||||
@@ -88,7 +105,7 @@ class SatStatusViewModel(
|
||||
companion object {
|
||||
fun factory(container: IMainContainer) = viewModelFactory {
|
||||
initializer {
|
||||
SatStatusViewModel(container.amSatRepo)
|
||||
SatStatusViewModel(container.amSatRepo, container.settingsRepo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user