diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt
index 10efa69f..d908e63a 100644
--- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt
+++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/SettingsRepo.kt
@@ -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
diff --git a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt
index a3cf10fa..7af866cc 100644
--- a/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt
+++ b/core/domain/src/main/java/com/rtbishop/look4sat/core/domain/model/Settings.kt
@@ -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(
diff --git a/core/presentation/src/main/res/values-es/strings.xml b/core/presentation/src/main/res/values-es/strings.xml
index d99318d5..dd4d6151 100644
--- a/core/presentation/src/main/res/values-es/strings.xml
+++ b/core/presentation/src/main/res/values-es/strings.xml
@@ -173,4 +173,6 @@
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.
Sin informes
Sin datos
+ AMSAT: una franja por franja de 2 horas
+ 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.
diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml
index 503ca43c..e798853b 100644
--- a/core/presentation/src/main/res/values-id/strings.xml
+++ b/core/presentation/src/main/res/values-id/strings.xml
@@ -306,4 +306,6 @@
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.
Tidak ada laporan
Tidak ada data
+ AMSAT: satu garis per slot 2 jam
+ 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.
diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml
index 6ea79107..ede6eed9 100644
--- a/core/presentation/src/main/res/values-in/strings.xml
+++ b/core/presentation/src/main/res/values-in/strings.xml
@@ -306,4 +306,6 @@
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.
Tidak ada laporan
Tidak ada data
+ AMSAT: satu garis per slot 2 jam
+ 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.
diff --git a/core/presentation/src/main/res/values-ru/strings.xml b/core/presentation/src/main/res/values-ru/strings.xml
index 0a166b52..8a5da46f 100644
--- a/core/presentation/src/main/res/values-ru/strings.xml
+++ b/core/presentation/src/main/res/values-ru/strings.xml
@@ -173,4 +173,6 @@
DeepCW анализирует только 400-1200 Гц. Если включено, тон вне этого диапазона переносится внутрь перед декодированием; тон внутри диапазона не изменяется.
Нет отчётов
Нет данных
+ AMSAT: полоса на каждые 2 часа
+ Включено — каждый день это двенадцать двухчасовых полос, поэтому перерыв внутри дня виден. Выключено — каждый день это один цвет и число отчётов; цвет соответствует худшему состоянию за день, так что даже один сбой остаётся заметен.
diff --git a/core/presentation/src/main/res/values-si/strings.xml b/core/presentation/src/main/res/values-si/strings.xml
index 05167d8d..a33467c2 100644
--- a/core/presentation/src/main/res/values-si/strings.xml
+++ b/core/presentation/src/main/res/values-si/strings.xml
@@ -173,4 +173,6 @@
DeepCW විශ්ලේෂණය කරන්නේ 400-1200 Hz පමණි. සක්රීය විට, එම පරාසයෙන් පිටත ස්වරයක් විකේතනයට පෙර පරාසය තුළට ගෙන එයි; දැනටමත් පරාසය තුළ ඇති ස්වරයක් වෙනස් නොකරයි.
වාර්තා නැත
දත්ත නැත
+ AMSAT: පැය 2 කට එක තීරුවක්
+ සක්රිය විට සෑම දිනයක් පැය දෙකේ තීරු දොළහකි, එබැවින් දිනක් තුළ ඇති බිඳවැටීම දැකිය හැක. අක්රිය විට සෑම දිනයක් එක් වර්ණයක් සහ වාර්තා ගණනකි — වර්ණය එදින නරකම තත්ත්වයයි, එබැවින් එක් අසාර්ථකත්වයක් වුවද පෙනේ.
diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml
index 44d8e583..5e432a48 100644
--- a/core/presentation/src/main/res/values-tr/strings.xml
+++ b/core/presentation/src/main/res/values-tr/strings.xml
@@ -318,4 +318,6 @@
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.
Rapor yok
Veri yok
+ AMSAT: 2 saatlik dilim başına şerit
+ 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.
diff --git a/core/presentation/src/main/res/values-uk/strings.xml b/core/presentation/src/main/res/values-uk/strings.xml
index 67605704..56dcd62d 100644
--- a/core/presentation/src/main/res/values-uk/strings.xml
+++ b/core/presentation/src/main/res/values-uk/strings.xml
@@ -173,4 +173,6 @@
DeepCW аналізує лише 400-1200 Гц. Якщо увімкнено, тон поза цим діапазоном переноситься в нього перед декодуванням; тон, що вже в діапазоні, не змінюється.
Немає звітів
Немає даних
+ AMSAT: смуга на кожні 2 години
+ Увімкнено — кожен день це дванадцять двогодинних смуг, тож перерва всередині дня видна. Вимкнено — кожен день це один колір і кількість звітів; колір відповідає найгіршому стану за день, тож навіть один збій залишається помітним.
diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml
index 5f32cc5d..4f32c7aa 100644
--- a/core/presentation/src/main/res/values-zh/strings.xml
+++ b/core/presentation/src/main/res/values-zh/strings.xml
@@ -308,4 +308,6 @@
DeepCW 仅分析 400-1200 Hz。开启后,超出该范围的音调会先搬移到范围内再解码;已在范围内的音调不作处理。
无人上报
无数据
+ AMSAT:每 2 小时一条纹
+ 开启时每天画成 12 条两小时条纹,一天之内的中断也看得见。关闭时每天显示一个颜色和报告总数——颜色取当天最差状态,所以出现过一次故障也不会被藏起来。
diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml
index 8d2b2195..319e7812 100644
--- a/core/presentation/src/main/res/values/strings.xml
+++ b/core/presentation/src/main/res/values/strings.xml
@@ -345,4 +345,6 @@
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.
No report
No data
+ AMSAT: stripe per 2-hour slot
+ 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.
diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt
index fb898102..d6418503 100644
--- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt
+++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsScreen.kt
@@ -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,
diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt
index 5c9d6c23..3adb9934 100644
--- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt
+++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsState.kt
@@ -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
diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt
index 27903e4b..1b52a007 100644
--- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt
+++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/SettingsViewModel.kt
@@ -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) }
diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
index eaa441e3..27d863a9 100644
--- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
+++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusScreen.kt
@@ -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) {
/** 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,19 +328,46 @@ private fun StatusRow(status: SatStatus, onClickDay: (SatDay) -> Unit) {
* the grid.
*/
@Composable
-private fun DayCell(day: SatDay, modifier: Modifier, onClick: () -> Unit) {
- Row(
- modifier = modifier
- .height(28.dp)
- .clip(RoundedCornerShape(4.dp))
- .clickable(onClick = onClick)
+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
+ .weight(1f)
+ .fillMaxHeight()
+ .background(Color(slot.statusColor))
+ )
+ }
+ }
+ 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
) {
- day.slots.forEach { slot ->
- Box(
- modifier = Modifier
- .weight(1f)
- .fillMaxHeight()
- .background(Color(slot.statusColor))
+ if (count > 0) {
+ Text(
+ text = count.toString(),
+ fontSize = 13.sp,
+ fontWeight = FontWeight.Bold,
+ color = readableOn(colour)
)
}
}
diff --git a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt
index 49371011..7e446fe4 100644
--- a/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt
+++ b/feature/status/src/main/java/com/rtbishop/look4sat/feature/status/SatStatusViewModel.kt
@@ -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 = emptyList(),
val reports: Map = 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 = _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)
}
}
}