Implemented AOS window and elevation highlight filters

This commit is contained in:
Arty Bishop committed 2026-07-30 13:28:37 +02:00
1 parent e86bc2c700
commit 6d2b0ced49
20 files changed
+777 -252

No files matched your search

@@ -50,6 +50,7 @@ import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaul
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@@ -70,6 +71,8 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
import com.rtbishop.look4sat.core.presentation.RadarDestination import com.rtbishop.look4sat.core.presentation.RadarDestination
import com.rtbishop.look4sat.core.presentation.Screen import com.rtbishop.look4sat.core.presentation.Screen
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
@@ -127,120 +130,128 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
val context = LocalContext.current val context = LocalContext.current
val container = (context.applicationContext as IContainerProvider).getMainContainer() val container = (context.applicationContext as IContainerProvider).getMainContainer()
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle() val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
NavigationSuiteScaffold( CompositionLocalProvider(
navigationSuiteItems = { LocalElevationThresholds provides ElevationThresholds(
navItems.forEach { screen -> low = otherSettings.lowElevation,
val isSelected = when (currentKey) { high = otherSettings.highElevation
is Screen.Satellites -> screen is Screen.Satellites )
is Screen.Passes -> screen is Screen.Passes
is Screen.Radar -> screen is Screen.Radar
is Screen.Map -> screen is Screen.Map
is Screen.Settings -> screen is Screen.Settings
else -> false
}
item(
icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) },
label = { Text(stringResource(screen.titleResId)) },
selected = isSelected,
onClick = {
if (isSelected) return@item
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen)
}
)
}
},
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
),
layoutType = when {
!hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail
!hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact
else -> NavigationSuiteType.ShortNavigationBarMedium
}
) { ) {
Column { NavigationSuiteScaffold(
NavDisplay( navigationSuiteItems = {
backStack = backStack, navItems.forEach { screen ->
modifier = Modifier.weight(1f), val isSelected = when (currentKey) {
onBack = navigateBack, is Screen.Satellites -> screen is Screen.Satellites
transitionSpec = { fadeTransition }, is Screen.Passes -> screen is Screen.Passes
popTransitionSpec = { fadeTransition }, is Screen.Radar -> screen is Screen.Radar
predictivePopTransitionSpec = { fadeTransition }, is Screen.Map -> screen is Screen.Map
entryDecorators = listOf( is Screen.Settings -> screen is Screen.Settings
// Required for saving Compose state per entry else -> false
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
} }
entry<Screen.Passes> { item(
PassesDestination { catNum, aosTime -> icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) },
container.satelliteRepo.selectPass(catNum, aosTime) label = { Text(stringResource(screen.titleResId)) },
backStack.add(Screen.Radar) selected = isSelected,
// navigateToRadar() onClick = {
if (isSelected) return@item
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen)
} }
} )
entry<Screen.Radar> {
RadarDestination(navigateUp = navigateBack)
}
entry<Screen.Map> {
MapDestination()
}
entry<Screen.Settings> {
SettingsDestination()
}
} }
) },
// Radio tracking status banner navigationSuiteColors = NavigationSuiteDefaults.colors(
if (trackingState.isActive) { navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse") ),
val alpha by infiniteTransition.animateFloat( layoutType = when {
initialValue = 1f, targetValue = 0.4f, !hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail
animationSpec = infiniteRepeatable( !hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact
animation = tween(1000, easing = LinearEasing), else -> NavigationSuiteType.ShortNavigationBarMedium
repeatMode = RepeatMode.Reverse }
), label = "pulseAlpha" ) {
) Column {
Row( NavDisplay(
verticalAlignment = Alignment.CenterVertically, backStack = backStack,
modifier = Modifier modifier = Modifier.weight(1f),
.fillMaxWidth() onBack = navigateBack,
.background(MaterialTheme.colorScheme.primaryContainer) transitionSpec = { fadeTransition },
.clickable { popTransitionSpec = { fadeTransition },
val pass = trackingState.currentPass predictivePopTransitionSpec = { fadeTransition },
if (pass != null) { entryDecorators = listOf(
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime) // Required for saving Compose state per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
entry<Screen.Passes> {
PassesDestination { catNum, aosTime ->
container.satelliteRepo.selectPass(catNum, aosTime)
backStack.add(Screen.Radar) backStack.add(Screen.Radar)
// navigateToRadar()
} }
} }
.padding(horizontal = 12.dp, vertical = 6.dp) entry<Screen.Radar> {
) { RadarDestination(navigateUp = navigateBack)
Box( }
entry<Screen.Map> {
MapDestination()
}
entry<Screen.Settings> {
SettingsDestination()
}
}
)
// Radio tracking status banner
if (trackingState.isActive) {
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f, targetValue = 0.4f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = "pulseAlpha"
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.size(8.dp) .fillMaxWidth()
.clip(CircleShape) .background(MaterialTheme.colorScheme.primaryContainer)
.background(Color(0xFF4CAF50).copy(alpha = alpha)) .clickable {
) val pass = trackingState.currentPass
Spacer(modifier = Modifier.width(8.dp)) if (pass != null) {
Text( container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
text = "Tracking: ${trackingState.currentPass?.name ?: ""}", backStack.add(Screen.Radar)
fontSize = 13.sp, }
fontWeight = FontWeight.Medium, }
color = MaterialTheme.colorScheme.onPrimaryContainer, .padding(horizontal = 12.dp, vertical = 6.dp)
modifier = Modifier.weight(1f) ) {
) Box(
val txOk = if (trackingState.txConnected) "TX" else "" modifier = Modifier
val rxOk = if (trackingState.rxConnected) "RX" else "" .size(8.dp)
Text( .clip(CircleShape)
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"), .background(Color(0xFF4CAF50).copy(alpha = alpha))
fontSize = 12.sp, )
color = MaterialTheme.colorScheme.onPrimaryContainer Spacer(modifier = Modifier.width(8.dp))
) Text(
text = "Tracking: ${trackingState.currentPass?.name ?: ""}",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.weight(1f)
)
val txOk = if (trackingState.txConnected) "TX" else ""
val rxOk = if (trackingState.rxConnected) "RX" else ""
Text(
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
} }
} }
} }
@@ -36,6 +36,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.TimeZone
class SatelliteRepo( class SatelliteRepo(
private val dispatcher: CoroutineDispatcher, private val dispatcher: CoroutineDispatcher,
@@ -64,8 +65,16 @@ class SatelliteRepo(
override suspend fun initRepository() = withContext(dispatcher) { override suspend fun initRepository() = withContext(dispatcher) {
settingsRepo.selectedIds.collect { selectedIds -> settingsRepo.selectedIds.collect { selectedIds ->
_satellites.update { localStorage.getEntriesWithIds(selectedIds) } _satellites.update { localStorage.getEntriesWithIds(selectedIds) }
val (_, hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value val settings = settingsRepo.passesSettings.value
calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes) calculatePasses(
time = System.currentTimeMillis(),
hoursAhead = settings.hoursAhead,
minElevation = settings.minElevation,
aosStartMinute = settings.aosStartMinute,
aosEndMinute = settings.aosEndMinute,
invertAosTimeWindow = settings.invertAosTimeWindow,
modes = settings.selectedModes
)
} }
} }
@@ -105,7 +114,15 @@ class SatelliteRepo(
} }
} }
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>) { override suspend fun calculatePasses(
time: Long,
hoursAhead: Int,
minElevation: Double,
aosStartMinute: Int,
aosEndMinute: Int,
invertAosTimeWindow: Boolean,
modes: List<String>
) {
_isCalculating.value = true _isCalculating.value = true
// Normalize to the start of the current minute so that coarse 60-second stepping // Normalize to the start of the current minute so that coarse 60-second stepping
// in getLeoPass always begins from the same phase, producing stable AOS/LOS times // in getLeoPass always begins from the same phase, producing stable AOS/LOS times
@@ -130,7 +147,12 @@ class SatelliteRepo(
val newPasses = ArrayList<OrbitalPass>() val newPasses = ArrayList<OrbitalPass>()
for (list in passLists) { for (list in passLists) {
for (pass in list) { for (pass in list) {
if (pass.losTime > time && pass.aosTime < timeFuture && pass.maxElevation > minElevation) { if (
pass.losTime > time
&& pass.aosTime < timeFuture
&& pass.maxElevation > minElevation
&& (pass.isDeepSpace || isAosInRange(pass.aosTime, aosStartMinute, aosEndMinute, invertAosTimeWindow))
) {
newPasses.add(pass) newPasses.add(pass)
} }
} }
@@ -142,6 +164,23 @@ class SatelliteRepo(
_isCalculating.value = false _isCalculating.value = false
} }
private fun isAosInRange(
aosTime: Long,
aosStartMinute: Int,
aosEndMinute: Int,
invertAosTimeWindow: Boolean
): Boolean {
val offsetMillis = TimeZone.getDefault().getOffset(aosTime).toLong()
val localMillis = Math.floorMod(aosTime + offsetMillis, 24L * 60L * 60L * 1000L)
val aosMinute = (localMillis / 60_000L).toInt()
val inRange = if (aosStartMinute <= aosEndMinute) {
aosMinute in aosStartMinute..aosEndMinute
} else {
aosMinute >= aosStartMinute || aosMinute <= aosEndMinute
}
return if (invertAosTimeWindow) !inRange else inRange
}
private fun OrbitalObject.getPasses(pos: GeoPos, time: Long, hours: Int): List<OrbitalPass> { private fun OrbitalObject.getPasses(pos: GeoPos, time: Long, hours: Int): List<OrbitalPass> {
val passes = mutableListOf<OrbitalPass>() val passes = mutableListOf<OrbitalPass>()
val endDate = time + hours * 60L * 60L * 1000L val endDate = time + hours * 60L * 60L * 1000L
@@ -54,6 +54,9 @@ class SettingsRepo(
private val keyFilterShowDeepSpace = "filterShowDeepSpace" private val keyFilterShowDeepSpace = "filterShowDeepSpace"
private val keyFilterHoursAhead = "filterHoursAhead" private val keyFilterHoursAhead = "filterHoursAhead"
private val keyFilterMinElevation = "filterMinElevation" private val keyFilterMinElevation = "filterMinElevation"
private val keyFilterAosStartMinute = "filterAosStartMinute"
private val keyFilterAosEndMinute = "filterAosEndMinute"
private val keyFilterAosInvert = "filterAosInvert"
private val keyNumberOfRadios = "numberOfRadios" private val keyNumberOfRadios = "numberOfRadios"
private val keyNumberOfSatellites = "numberOfSatellites" private val keyNumberOfSatellites = "numberOfSatellites"
private val keyRotatorAddress = "rotatorAddress" private val keyRotatorAddress = "rotatorAddress"
@@ -82,6 +85,8 @@ class SettingsRepo(
private val keyShouldSeeWarning = "shouldSeeWarning" private val keyShouldSeeWarning = "shouldSeeWarning"
private val keyShouldSeeWhatsNew = "shouldSeeWhatsNew_v$appVersionName" private val keyShouldSeeWhatsNew = "shouldSeeWhatsNew_v$appVersionName"
private val keySstvMode = "sstvMode" private val keySstvMode = "sstvMode"
private val keyLowElevation = "lowElevation"
private val keyHighElevation = "highElevation"
private val keyUseCustomTle = "useCustomTle" private val keyUseCustomTle = "useCustomTle"
private val keyUseCustomTransceivers = "useCustomTransceivers" private val keyUseCustomTransceivers = "useCustomTransceivers"
private val keyTleUrl = "tleUrl" private val keyTleUrl = "tleUrl"
@@ -127,6 +132,9 @@ class SettingsRepo(
putBoolean(keyFilterShowDeepSpace, settings.showDeepSpace) putBoolean(keyFilterShowDeepSpace, settings.showDeepSpace)
putInt(keyFilterHoursAhead, settings.hoursAhead) putInt(keyFilterHoursAhead, settings.hoursAhead)
putLong(keyFilterMinElevation, settings.minElevation.toRawBits()) putLong(keyFilterMinElevation, settings.minElevation.toRawBits())
putInt(keyFilterAosStartMinute, settings.aosStartMinute)
putInt(keyFilterAosEndMinute, settings.aosEndMinute)
putBoolean(keyFilterAosInvert, settings.invertAosTimeWindow)
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma)) putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
_passesSettings.value = settings _passesSettings.value = settings
} }
@@ -135,9 +143,20 @@ class SettingsRepo(
val showDeepSpace = preferences.getBoolean(keyFilterShowDeepSpace, true) val showDeepSpace = preferences.getBoolean(keyFilterShowDeepSpace, true)
val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24) val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24)
val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits())) val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits()))
val aosStartMinute = preferences.getInt(keyFilterAosStartMinute, 0).coerceIn(0, 23 * 60 + 59)
val aosEndMinute = preferences.getInt(keyFilterAosEndMinute, 23 * 60 + 59).coerceIn(0, 23 * 60 + 59)
val invertAosTimeWindow = preferences.getBoolean(keyFilterAosInvert, false)
val selectedModesString = preferences.getString(keySelectedModes, null) val selectedModesString = preferences.getString(keySelectedModes, null)
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList() val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
return PassesSettings(showDeepSpace, hoursAhead, minElevation, selectedModes) return PassesSettings(
showDeepSpace,
hoursAhead,
minElevation,
aosStartMinute,
aosEndMinute,
invertAosTimeWindow,
selectedModes
)
} }
//endregion //endregion
@@ -335,6 +354,8 @@ class SettingsRepo(
putBoolean(keyShouldSeeWarning, new.shouldSeeWarning) putBoolean(keyShouldSeeWarning, new.shouldSeeWarning)
putBoolean(keyShouldSeeWhatsNew, new.shouldSeeWhatsNew) putBoolean(keyShouldSeeWhatsNew, new.shouldSeeWhatsNew)
putString(keySstvMode, new.sstvMode) putString(keySstvMode, new.sstvMode)
putLong(keyLowElevation, new.lowElevation.toRawBits())
putLong(keyHighElevation, new.highElevation.toRawBits())
} }
new new
} }
@@ -349,7 +370,9 @@ class SettingsRepo(
stateOfNightMode = preferences.getBoolean(keyStateOfNightMode, false), stateOfNightMode = preferences.getBoolean(keyStateOfNightMode, false),
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true), shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true), shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto" sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits()))
) )
//endregion //endregion
@@ -27,6 +27,9 @@ data class PassesSettings(
val showDeepSpace: Boolean = true, val showDeepSpace: Boolean = true,
val hoursAhead: Int, val hoursAhead: Int,
val minElevation: Double, val minElevation: Double,
val aosStartMinute: Int = 0,
val aosEndMinute: Int = 23 * 60 + 59,
val invertAosTimeWindow: Boolean = false,
val selectedModes: List<String> val selectedModes: List<String>
) )
@@ -57,7 +60,9 @@ data class OtherSettings(
val stateOfNightMode: Boolean = false, val stateOfNightMode: Boolean = false,
val shouldSeeWarning: Boolean, val shouldSeeWarning: Boolean,
val shouldSeeWhatsNew: Boolean, val shouldSeeWhatsNew: Boolean,
val sstvMode: String = "Auto" val sstvMode: String = "Auto",
val lowElevation: Double = 15.0,
val highElevation: Double = 45.0
) )
data class DataSourcesSettings( data class DataSourcesSettings(
@@ -44,7 +44,15 @@ interface ISatelliteRepo {
suspend fun initRepository() suspend fun initRepository()
/** Recalculate passes with the given filter parameters. */ /** Recalculate passes with the given filter parameters. */
suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>) suspend fun calculatePasses(
time: Long,
hoursAhead: Int,
minElevation: Double,
aosStartMinute: Int,
aosEndMinute: Int,
invertAosTimeWindow: Boolean,
modes: List<String>
)
/** Get the current position of a single satellite. */ /** Get the current position of a single satellite. */
suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos suspend fun getPosition(sat: OrbitalObject, pos: GeoPos, time: Long): OrbitalPos
@@ -39,18 +39,26 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ElevatedButton import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.hideFromAccessibility
@@ -64,10 +72,11 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.rtbishop.look4sat.core.domain.predict.NearEarthObject import com.rtbishop.look4sat.core.domain.predict.NearEarthObject
import com.rtbishop.look4sat.core.domain.predict.OrbitalData import com.rtbishop.look4sat.core.domain.predict.OrbitalData
import com.rtbishop.look4sat.core.domain.predict.OrbitalPass import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
@@ -278,55 +287,87 @@ fun getDefaultPass(): OrbitalPass = OrbitalPass(
fun SharedDialog( fun SharedDialog(
title: String, onCancel: () -> Unit, onAccept: () -> Unit, content: @Composable () -> Unit title: String, onCancel: () -> Unit, onAccept: () -> Unit, content: @Composable () -> Unit
) { ) {
DialogShell(title = title, titleFontSize = 16, onDismissRequest = onCancel) { SharedDialog(title = title, onDismissRequest = onCancel, onCancel = onCancel, onAccept = onAccept) { _ ->
content() content()
Row(modifier = Modifier.padding(start = it, bottom = it, end = it)) {
CardButton(onClick = onCancel, text = stringResource(R.string.btn_cancel))
Spacer(modifier = Modifier.weight(1f))
CardButton(onClick = onAccept, text = stringResource(R.string.btn_accept))
}
} }
} }
@Composable @Composable
fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) { fun SharedDialog(
DialogShell(title = title, titleFontSize = 18, onDismissRequest = {}) { title: String,
Text( onDismissRequest: () -> Unit,
text = text, onCancel: (() -> Unit)? = null,
fontSize = 16.sp, onAccept: (() -> Unit)? = null,
color = MaterialTheme.colorScheme.onSurface, titleFontSize: Int = 16,
modifier = Modifier.padding(horizontal = it) titleTextAlign: TextAlign = if (onCancel != null && onAccept != null) TextAlign.Center else TextAlign.Start,
) content: @Composable (padding: Dp) -> Unit
Row(modifier = Modifier.padding(start = it, bottom = it, end = it)) { ) {
Spacer(modifier = Modifier.weight(1f)) DialogShell(onDismissRequest = onDismissRequest) { padding ->
CardButton(onClick = onDismiss, text = stringResource(R.string.btn_accept)) Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(start = padding, top = padding, end = padding)
) {
if (onCancel != null) {
CardButton(onClick = onCancel, text = stringResource(R.string.btn_cancel))
}
Text(
text = title,
fontSize = titleFontSize.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary,
textAlign = titleTextAlign,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.weight(1f)
.padding(horizontal = if (onCancel != null && onAccept != null) padding else 0.dp)
)
if (onAccept != null) {
CardButton(onClick = onAccept, text = stringResource(R.string.btn_accept))
}
} }
content(padding)
} }
} }
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun DialogShell( private fun DialogShell(
title: String,
titleFontSize: Int,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
content: @Composable (padding: androidx.compose.ui.unit.Dp) -> Unit content: @Composable (padding: Dp) -> Unit
) { ) {
val padding = LocalSpacing.current.large val padding = LocalSpacing.current.large
Dialog(onDismissRequest = onDismissRequest) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
ElevatedCard { val stopSheetFling = remember {
Column( object : NestedScrollConnection {
horizontalAlignment = Alignment.CenterHorizontally, override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
verticalArrangement = Arrangement.spacedBy(padding) return available
) {
Text(
text = title,
fontSize = titleFontSize.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = padding, top = padding, end = padding)
)
content(padding)
} }
override fun onPostScroll(
consumed: Offset,
available: Offset,
source: NestedScrollSource
): Offset = Offset.Zero
}
}
ModalBottomSheet(
onDismissRequest = onDismissRequest,
sheetState = sheetState,
dragHandle = null,
shape = MaterialTheme.shapes.medium,
scrimColor = Color.Black.copy(alpha = 0.64f)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(padding),
modifier = Modifier
.fillMaxWidth()
.nestedScroll(stopSheetFling)
) {
content(padding)
} }
} }
} }
@@ -391,13 +432,26 @@ fun TopBar(
@Composable @Composable
fun elevationColor(elevation: Double): Color { fun elevationColor(elevation: Double): Color {
val thresholds = LocalElevationThresholds.current
return when { return when {
elevation < 15.0 -> Color(0xFFEF5350) // soft red for low elevation elevation < thresholds.low -> ElevationLowColor // soft red for low elevation
elevation < 45.0 -> MaterialTheme.colorScheme.primary // accent yellow for normal elevation < thresholds.high -> MaterialTheme.colorScheme.primary // accent yellow for normal
else -> Color(0xFF66BB6A) // soft green for high elevation else -> ElevationHighColor // soft green for high elevation
} }
} }
/** User-configurable elevation highlight thresholds (in degrees). */
data class ElevationThresholds(val low: Double = 15.0, val high: Double = 45.0)
/** Provided at the app root from settings; defaults keep the original 15°/45° behavior. */
val LocalElevationThresholds = compositionLocalOf { ElevationThresholds() }
/** Soft red used for elevations below the low threshold. */
val ElevationLowColor = Color(0xFFEF5350)
/** Soft green used for elevations above the high threshold. */
val ElevationHighColor = Color(0xFF66BB6A)
@Composable @Composable
fun OutlinedText( fun OutlinedText(
text: String, text: String,
@@ -31,7 +31,10 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Filtrar pases</string> <string name="pass_filter_title">Filtrar pases</string>
<string name="pass_filter_elev">Elevación mínima</string> <string name="pass_filter_elev">Elevación mínima</string>
<string name="pass_filter_hours">Horas por delante</string> <string name="pass_filter_hours">Tiempo por delante</string>
<string name="pass_filter_aos_time">Ventana AOS</string>
<string name="pass_filter_invert_time">Invertir ventana AOS</string>
<string name="pass_filter_deep_space">DeepSpace (período &gt;225min)</string>
<string name="pass_modes_title">Tipo de modulación</string> <string name="pass_modes_title">Tipo de modulación</string>
<string name="pass_elevation">Elevación: %.1f°</string> <string name="pass_elevation">Elevación: %.1f°</string>
<string name="pass_altitude">Altitud: %.0f km</string> <string name="pass_altitude">Altitud: %.0f km</string>
@@ -139,6 +142,12 @@
<string name="prefs_other_switch_update">Habilitar auto actualización de datos</string> <string name="prefs_other_switch_update">Habilitar auto actualización de datos</string>
<string name="prefs_other_switch_sweep">Habilitar animaciones radar</string> <string name="prefs_other_switch_sweep">Habilitar animaciones radar</string>
<string name="prefs_other_switch_sensors">Usar sensores para rotar vista de radar</string> <string name="prefs_other_switch_sensors">Usar sensores para rotar vista de radar</string>
<string name="prefs_other_switch_night_mode">Activar filtro nocturno rojo</string>
<string name="prefs_highlight_title">Resaltado de elevación</string>
<string name="prefs_highlight_low">Baja &lt; %1$d°</string>
<string name="prefs_highlight_mid">Media %1$d-%2$d°</string>
<string name="prefs_highlight_high">Alta &gt; %1$d°</string>
<string name="prefs_outro_title">Me gustaría dar las gracias a:</string> <string name="prefs_outro_title">Me gustaría dar las gracias a:</string>
<string name="prefs_outro_license">La app viene sin garantías de ningún tipo.</string> <string name="prefs_outro_license">La app viene sin garantías de ningún tipo.</string>
@@ -30,7 +30,10 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Фильтровать пролеты</string> <string name="pass_filter_title">Фильтровать пролеты</string>
<string name="pass_filter_elev">Минимальная элевация</string> <string name="pass_filter_elev">Минимальная элевация</string>
<string name="pass_filter_hours">Количество часов</string> <string name="pass_filter_hours">Период расчёта</string>
<string name="pass_filter_aos_time">Окно AOS</string>
<string name="pass_filter_invert_time">Инвертировать окно AOS</string>
<string name="pass_filter_deep_space">DeepSpace (период &gt;225мин)</string>
<string name="pass_modes_title">Выберите тип модуляции</string> <string name="pass_modes_title">Выберите тип модуляции</string>
<string name="pass_elevation">Элевация: %.1f°</string> <string name="pass_elevation">Элевация: %.1f°</string>
<string name="pass_altitude">Высота: %d км</string> <string name="pass_altitude">Высота: %d км</string>
@@ -138,6 +141,12 @@
<string name="prefs_other_switch_update">Обновлять данные автоматически</string> <string name="prefs_other_switch_update">Обновлять данные автоматически</string>
<string name="prefs_other_switch_sweep">Показывать анимацию радара</string> <string name="prefs_other_switch_sweep">Показывать анимацию радара</string>
<string name="prefs_other_switch_sensors">Использовать сенсоры устройства</string> <string name="prefs_other_switch_sensors">Использовать сенсоры устройства</string>
<string name="prefs_other_switch_night_mode">Включить красный ночной фильтр</string>
<string name="prefs_highlight_title">Подсветка высоты</string>
<string name="prefs_highlight_low">Низко &lt; %1$d°</string>
<string name="prefs_highlight_mid">Средне %1$d-%2$d°</string>
<string name="prefs_highlight_high">Высоко &gt; %1$d°</string>
<string name="prefs_outro_title">Я хотел бы сказать спасибо:</string> <string name="prefs_outro_title">Я хотел бы сказать спасибо:</string>
<string name="prefs_outro_license">Это ПО поставляется без гарантий.</string> <string name="prefs_outro_license">Это ПО поставляется без гарантий.</string>
@@ -31,7 +31,10 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">පසුකිරිම් පිරිපහදුව</string> <string name="pass_filter_title">පසුකිරිම් පිරිපහදුව</string>
<string name="pass_filter_elev">අවම උත්තෝලනය</string> <string name="pass_filter_elev">අවම උත්තෝලනය</string>
<string name="pass_filter_hours">ඉදිරි පැය</string> <string name="pass_filter_hours">ඉදිරි කාලය</string>
<string name="pass_filter_aos_time">AOS කවුළුව</string>
<string name="pass_filter_invert_time">AOS කවුළුව ප්‍රතිවිරුද්ධ කරන්න</string>
<string name="pass_filter_deep_space">DeepSpace (කාලය &gt;225min)</string>
<string name="pass_modes_title">මූර්ජන ආකාරය තෝරන්න</string> <string name="pass_modes_title">මූර්ජන ආකාරය තෝරන්න</string>
<string name="pass_elevation">උත්තෝලනය: %.1f°</string> <string name="pass_elevation">උත්තෝලනය: %.1f°</string>
<string name="pass_altitude">උන්නතාශය: %d km</string> <string name="pass_altitude">උන්නතාශය: %d km</string>
@@ -139,6 +142,12 @@
<string name="prefs_other_switch_update">ස්වයං දත්ත යාවත්කාලීනය</string> <string name="prefs_other_switch_update">ස්වයං දත්ත යාවත්කාලීනය</string>
<string name="prefs_other_switch_sweep">radar sweep සජීවිකරණය සබල කරන්න</string> <string name="prefs_other_switch_sweep">radar sweep සජීවිකරණය සබල කරන්න</string>
<string name="prefs_other_switch_sensors">Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න </string> <string name="prefs_other_switch_sensors">Radar දර්ශනය කරකැවීමට සංවේදක භාවිතා කරන්න </string>
<string name="prefs_other_switch_night_mode">රතු රාත්‍රී පෙරහන සබල කරන්න</string>
<string name="prefs_highlight_title">උස් උද්දීපනය</string>
<string name="prefs_highlight_low">අඩු &lt; %1$d°</string>
<string name="prefs_highlight_mid">මධ්‍ය %1$d-%2$d°</string>
<string name="prefs_highlight_high">ඉහළ &gt; %1$d°</string>
<string name="prefs_outro_title">මම ස්තුති කිරීමට කැමති:</string> <string name="prefs_outro_title">මම ස්තුති කිරීමට කැමති:</string>
<string name="prefs_outro_license">මෘදුකාංගය වගකීමක් සමග නොලැබේ</string> <string name="prefs_outro_license">මෘදුකාංගය වගකීමක් සමග නොලැබේ</string>
@@ -31,7 +31,10 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Geçişleri filtrele</string> <string name="pass_filter_title">Geçişleri filtrele</string>
<string name="pass_filter_elev">Minimum yükseklik açısı</string> <string name="pass_filter_elev">Minimum yükseklik açısı</string>
<string name="pass_filter_hours">Gösterilecek saat aralığı</string> <string name="pass_filter_hours">Gösterilecek zaman</string>
<string name="pass_filter_aos_time">AOS aralığı</string>
<string name="pass_filter_invert_time">AOS aralığını tersine çevir</string>
<string name="pass_filter_deep_space">DeepSpace (periyot &gt;225dk)</string>
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string> <string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
<string name="pass_modes_title">Modulasyon türü seçin</string> <string name="pass_modes_title">Modulasyon türü seçin</string>
<string name="pass_satId" translatable="false">%05d</string> <string name="pass_satId" translatable="false">%05d</string>
@@ -182,6 +185,12 @@
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string> <string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string> <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_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_highlight_title">Elevasyon vurgusu</string>
<string name="prefs_highlight_low">Düşük &lt; %1$d°</string>
<string name="prefs_highlight_mid">Orta %1$d-%2$d°</string>
<string name="prefs_highlight_high">Yüksek &gt; %1$d°</string>
<string name="prefs_outro_title">Teşekkürler</string> <string name="prefs_outro_title">Teşekkürler</string>
<string name="prefs_outro_thanks" translatable="false"> <string name="prefs_outro_thanks" translatable="false">
@@ -30,8 +30,11 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Фільтр прольотів</string> <string name="pass_filter_title">Фільтр прольотів</string>
<string name="pass_filter_elev">Мінімальне піднесення</string> <string name="pass_filter_elev">Мінімальне піднесень</string>
<string name="pass_filter_hours">Кількість годин</string> <string name="pass_filter_hours">Час вперед</string>
<string name="pass_filter_aos_time">Вікно AOS</string>
<string name="pass_filter_invert_time">Інвертувати вікно AOS</string>
<string name="pass_filter_deep_space">DeepSpace (період &gt;225хв)</string>
<string name="pass_modes_title">Виберіть тип модуляції</string> <string name="pass_modes_title">Виберіть тип модуляції</string>
<string name="pass_elevation">Піднес.: %.1f°</string> <string name="pass_elevation">Піднес.: %.1f°</string>
<string name="pass_altitude">Висота: %.0f км</string> <string name="pass_altitude">Висота: %.0f км</string>
@@ -66,7 +69,7 @@
<string name="map_prev">Попередній</string> <string name="map_prev">Попередній</string>
<string name="map_next">Наступний</string> <string name="map_next">Наступний</string>
<string name="map_azimuth">Азимут: %.1f°</string> <string name="map_azimuth">Азимут: %.1f°</string>
<string name="map_elevation">Піднесення: %.1f°</string> <string name="map_elevation">Піднесень: %.1f°</string>
<string name="map_altitude">Висота: %.0f км</string> <string name="map_altitude">Висота: %.0f км</string>
<string name="map_distance">Дистанція: %.0f км</string> <string name="map_distance">Дистанція: %.0f км</string>
<string name="map_latitude">Широта: %.1f°</string> <string name="map_latitude">Широта: %.1f°</string>
@@ -139,6 +142,12 @@
<string name="prefs_other_switch_update">Автоматичне оновлення даних</string> <string name="prefs_other_switch_update">Автоматичне оновлення даних</string>
<string name="prefs_other_switch_sweep">Показувати анімацію радара</string> <string name="prefs_other_switch_sweep">Показувати анімацію радара</string>
<string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string> <string name="prefs_other_switch_sensors">Використовувати сенсори пристрою</string>
<string name="prefs_other_switch_night_mode">Увімкнути червоний нічний фільтр</string>
<string name="prefs_highlight_title">Підсвічування висоти</string>
<string name="prefs_highlight_low">Низько &lt; %1$d°</string>
<string name="prefs_highlight_mid">Середньо %1$d-%2$d°</string>
<string name="prefs_highlight_high">Високо &gt; %1$d°</string>
<string name="prefs_outro_title">Я хотів би подякувати:</string> <string name="prefs_outro_title">Я хотів би подякувати:</string>
<string name="prefs_outro_license">Ця програма поставляється без жодних гарантій</string> <string name="prefs_outro_license">Ця програма поставляється без жодних гарантій</string>
@@ -26,7 +26,10 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">筛选过境</string> <string name="pass_filter_title">筛选过境</string>
<string name="pass_filter_elev">仰角值</string> <string name="pass_filter_elev">仰角值</string>
<string name="pass_filter_hours">小时数</string> <string name="pass_filter_hours">提前时间</string>
<string name="pass_filter_aos_time">AOS 时间段</string>
<string name="pass_filter_invert_time">反选 AOS 时间段</string>
<string name="pass_filter_deep_space">DeepSpace(周期 &gt;225分钟)</string>
<string name="pass_modes_title">选择调制类型</string> <string name="pass_modes_title">选择调制类型</string>
<string name="pass_elevation">仰角:%.1f°</string> <string name="pass_elevation">仰角:%.1f°</string>
<string name="pass_altitude">高度:%d km</string> <string name="pass_altitude">高度:%d km</string>
@@ -133,6 +136,11 @@
<string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string> <string name="prefs_other_switch_sensors">使用传感器旋转雷达视图</string>
<string name="prefs_other_switch_night_mode">启用红色夜间模式</string> <string name="prefs_other_switch_night_mode">启用红色夜间模式</string>
<string name="prefs_highlight_title">仰角高亮</string>
<string name="prefs_highlight_low">低 &lt; %1$d°</string>
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
<string name="prefs_highlight_high">高 &gt; %1$d°</string>
<string name="prefs_outro_title">我要感谢:</string> <string name="prefs_outro_title">我要感谢:</string>
<string name="prefs_outro_thanks" translatable="false"> <string name="prefs_outro_thanks" translatable="false">
• Look4Sat 所有用户及贡献者! • Look4Sat 所有用户及贡献者!
@@ -31,7 +31,9 @@
<!-- Passes screen --> <!-- Passes screen -->
<string name="pass_filter_title">Filter passes</string> <string name="pass_filter_title">Filter passes</string>
<string name="pass_filter_elev">Minimal elevation</string> <string name="pass_filter_elev">Minimal elevation</string>
<string name="pass_filter_hours">Hours ahead</string> <string name="pass_filter_hours">Time ahead</string>
<string name="pass_filter_aos_time">AOS window</string>
<string name="pass_filter_invert_time">Invert AOS window</string>
<string name="pass_filter_deep_space">DeepSpace (period >225min)</string> <string name="pass_filter_deep_space">DeepSpace (period >225min)</string>
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string> <string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
<string name="pass_modes_title">Select modulation type</string> <string name="pass_modes_title">Select modulation type</string>
@@ -182,6 +184,11 @@
<string name="prefs_other_switch_sensors">Use sensors to rotate radar view</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_night_mode">Enable red night mode filter</string>
<string name="prefs_highlight_title">Elevation highlight</string>
<string name="prefs_highlight_low">Low &lt; %1$d°</string>
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
<string name="prefs_highlight_high">High &gt; %1$d°</string>
<string name="prefs_outro_title">I would like to say thanks to</string> <string name="prefs_outro_title">I would like to say thanks to</string>
<string name="prefs_outro_thanks" translatable="false"> <string name="prefs_outro_thanks" translatable="false">
* Look4Sat users and contributors! * Look4Sat users and contributors!
@@ -21,8 +21,11 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@@ -31,6 +34,7 @@ import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RangeSlider
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -55,7 +59,10 @@ import com.rtbishop.look4sat.core.presentation.LocalSpacing
import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.MainTheme
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.SharedDialog import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.ElevationHighColor
import com.rtbishop.look4sat.core.presentation.ElevationLowColor
import com.rtbishop.look4sat.core.presentation.elevationColor import com.rtbishop.look4sat.core.presentation.elevationColor
import kotlin.math.roundToInt
private val allModes = listOf( private val allModes = listOf(
"AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3", "AFSK", "AFSK S-Net", "AFSK SALSAT", "AHRPT", "AM", "APT", "BPSK", "BPSK PMT-A3",
@@ -66,33 +73,81 @@ 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 hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 192, 216, 240)
private const val dayMinutes = 24 * 60
private const val minuteStep = 15
private const val endOfDayMinute = dayMinutes - 1
private const val quarterHourSlots = dayMinutes / minuteStep
data class PassFilterParams(
val hours: Int,
val elevation: Double,
val lowElevation: Double,
val highElevation: Double,
val aosStartMinute: Int,
val aosEndMinute: Int,
val invertAosTimeWindow: Boolean,
val showDeepSpace: Boolean
)
@Preview @Preview
@Composable @Composable
private fun PassesDialogPreview() { private fun PassesDialogPreview() {
MainTheme { PassesDialog(24, 16.0, true, {}) { _, _, _ -> } } MainTheme {
PassesFilterDialog(
hours = 24,
elevation = 16.0,
lowElevation = 16.0,
highElevation = 65.0,
aosStartMinute = 0,
aosEndMinute = 23 * 60 + 59,
invertAosTimeWindow = false,
showDeepSpace = true,
cancel = {},
accept = {}
)
}
} }
@Composable @Composable
internal fun PassesDialog( internal fun PassesFilterDialog(
hours: Int, hours: Int,
elevation: Double, elevation: Double,
lowElevation: Double,
highElevation: Double,
aosStartMinute: Int,
aosEndMinute: Int,
invertAosTimeWindow: Boolean,
showDeepSpace: Boolean, showDeepSpace: Boolean,
cancel: () -> Unit, cancel: () -> Unit,
accept: (Int, Double, Boolean) -> Unit accept: (PassFilterParams) -> Unit
) { ) {
val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) } val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) }
val elevationValueNew = remember { mutableDoubleStateOf(elevation) } val elevationValueNew = remember { mutableDoubleStateOf(elevation) }
val highlightBounds = 0f..90f
var highlightRange by remember(lowElevation, highElevation) {
mutableStateOf(lowElevation.toFloat()..highElevation.toFloat())
}
var aosRangeValue by remember {
mutableStateOf(minuteToSlot(aosStartMinute).toFloat()..minuteToSlot(aosEndMinute).toFloat())
}
var invertedAosRange by remember { mutableStateOf(invertAosTimeWindow) }
var deepSpaceEnabled by remember { mutableStateOf(showDeepSpace) } var deepSpaceEnabled by remember { mutableStateOf(showDeepSpace) }
val onAccept = { val onAccept = {
accept(hourSteps[hoursIndex.intValue], elevationValueNew.doubleValue, deepSpaceEnabled).also { cancel() } accept(
PassFilterParams(
hours = hourSteps[hoursIndex.intValue],
elevation = elevationValueNew.doubleValue,
lowElevation = highlightRange.start.roundToInt().toDouble(),
highElevation = highlightRange.endInclusive.roundToInt().toDouble(),
aosStartMinute = slotToMinute(aosRangeValue.start),
aosEndMinute = slotToMinute(aosRangeValue.endInclusive),
invertAosTimeWindow = invertedAosRange,
showDeepSpace = deepSpaceEnabled
)
)
cancel()
} }
SharedDialog(title = stringResource(R.string.pass_filter_title), onCancel = cancel, onAccept = onAccept) { SharedDialog(title = stringResource(R.string.pass_filter_title), onCancel = cancel, onAccept = onAccept) {
ToggleRow(
title = stringResource(R.string.pass_filter_deep_space),
checked = deepSpaceEnabled,
onCheckedChange = { deepSpaceEnabled = it }
)
SliderRow( SliderRow(
title = stringResource(R.string.pass_filter_elev), title = stringResource(R.string.pass_filter_elev),
value = elevationValueNew.doubleValue, value = elevationValueNew.doubleValue,
@@ -104,24 +159,74 @@ internal fun PassesDialog(
SliderRow( SliderRow(
title = stringResource(R.string.pass_filter_hours), title = stringResource(R.string.pass_filter_hours),
value = hoursIndex.intValue.toDouble(), value = hoursIndex.intValue.toDouble(),
displayValue = "${hourSteps[hoursIndex.intValue]}h", displayValue = formatHoursLabel(hourSteps[hoursIndex.intValue]),
valueResId = R.drawable.ic_clock, valueResId = R.drawable.ic_clock,
valueRange = 0f..(hourSteps.size - 1).toFloat(), valueRange = 0f..(hourSteps.size - 1).toFloat(),
steps = hourSteps.size - 2 steps = hourSteps.size - 2
) { hoursIndex.intValue = it.toInt().coerceIn(0, hourSteps.size - 1) } ) { hoursIndex.intValue = it.toInt().coerceIn(0, hourSteps.size - 1) }
ToggleRow(
title = stringResource(R.string.pass_filter_deep_space),
checked = deepSpaceEnabled,
onCheckedChange = { deepSpaceEnabled = it }
)
ElevationColorsRangeSliderRow(
title = stringResource(R.string.prefs_highlight_title),
range = highlightRange,
valueRange = highlightBounds
) { highlightRange = it }
TimeRangeSliderRow(
title = stringResource(R.string.pass_filter_aos_time),
range = aosRangeValue,
displayValue = formatTimeRange(aosRangeValue, invertedAosRange),
valueResId = R.drawable.ic_clock,
valueRange = 0f..quarterHourSlots.toFloat(),
) { aosRangeValue = it }
ToggleRow(
title = stringResource(R.string.pass_filter_invert_time),
checked = invertedAosRange,
onCheckedChange = { invertedAosRange = it }
)
Spacer(modifier = Modifier.height(0.dp))
} }
} }
private fun slotToMinute(value: Float): Int {
val slot = value.roundToInt().coerceIn(0, quarterHourSlots)
return if (slot == quarterHourSlots) endOfDayMinute else slot * minuteStep
}
private fun minuteToSlot(minute: Int): Int {
if (minute >= endOfDayMinute) return quarterHourSlots
return ((minute + minuteStep / 2) / minuteStep).coerceIn(0, quarterHourSlots)
}
private fun formatMinuteOfDay(minute: Int): String {
val hourPart = minute / 60
val minutePart = minute % 60
return "%02d:%02d".format(hourPart, minutePart)
}
private fun formatHoursLabel(hours: Int): String {
if (hours < 24) return "${hours}h"
val days = hours / 24
val remainder = hours % 24
return if (remainder == 0) "${days}d" else "${days}d ${remainder}h"
}
private fun formatTimeRange(
range: ClosedFloatingPointRange<Float>,
inverted: Boolean
): String {
val start = formatMinuteOfDay(slotToMinute(range.start))
val end = formatMinuteOfDay(slotToMinute(range.endInclusive))
return if (inverted) "$end - $start" else "$start - $end"
}
@Composable @Composable
private fun SliderRow( private fun SliderSection(
title: String, title: String,
value: Double, trailingContent: @Composable () -> Unit,
displayValue: String, sliderContent: @Composable () -> Unit
valueResId: Int,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
accentColor: Color = MaterialTheme.colorScheme.primary,
onChange: (Float) -> Unit
) { ) {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -138,6 +243,26 @@ private fun SliderRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.onSurface color = MaterialTheme.colorScheme.onSurface
) )
trailingContent()
}
sliderContent()
}
}
@Composable
private fun SliderRow(
title: String,
value: Double,
displayValue: String,
valueResId: Int,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int = 0,
accentColor: Color = MaterialTheme.colorScheme.primary,
onChange: (Float) -> Unit
) {
SliderSection(
title = title,
trailingContent = {
Icon( Icon(
painter = painterResource(id = valueResId), painter = painterResource(id = valueResId),
contentDescription = null, contentDescription = null,
@@ -151,10 +276,89 @@ private fun SliderRow(
color = accentColor color = accentColor
) )
} }
) {
Slider(value = value.toFloat(), onValueChange = onChange, valueRange = valueRange, steps = steps) Slider(value = value.toFloat(), onValueChange = onChange, valueRange = valueRange, steps = steps)
} }
} }
@Composable
private fun TimeRangeSliderRow(
title: String,
range: ClosedFloatingPointRange<Float>,
displayValue: String,
valueResId: Int,
valueRange: ClosedFloatingPointRange<Float>,
onChange: (ClosedFloatingPointRange<Float>) -> Unit
) {
SliderSection(
title = title,
trailingContent = {
Icon(
painter = painterResource(id = valueResId),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp)
)
Text(
text = displayValue,
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
}
) {
RangeSlider(
value = range,
onValueChange = { onChange(it.start..it.endInclusive) },
valueRange = valueRange,
steps = 0,
modifier = Modifier.fillMaxWidth()
)
}
}
@Composable
private fun ElevationColorsRangeSliderRow(
title: String,
range: ClosedFloatingPointRange<Float>,
valueRange: ClosedFloatingPointRange<Float>,
onChange: (ClosedFloatingPointRange<Float>) -> Unit
) {
val low = range.start.roundToInt()
val high = range.endInclusive.roundToInt()
SliderSection(
title = title,
trailingContent = {
Text(
text = "$low°",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = ElevationLowColor
)
Text(
text = "..",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
Text(
text = "$high°",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
color = ElevationHighColor
)
}
) {
RangeSlider(
value = range,
onValueChange = { onChange(it.start..it.endInclusive) },
valueRange = valueRange,
steps = 0,
modifier = Modifier.fillMaxWidth()
)
}
}
@Preview(showBackground = true) @Preview(showBackground = true)
@Composable @Composable
private fun RadiosDialogPreview() { private fun RadiosDialogPreview() {
@@ -67,11 +67,11 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPass
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.presentation.EmptyListCard import com.rtbishop.look4sat.core.presentation.EmptyListCard
import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.IconCard
import com.rtbishop.look4sat.core.presentation.InfoDialog
import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.MainTheme
import com.rtbishop.look4sat.core.presentation.NextPassRow import com.rtbishop.look4sat.core.presentation.NextPassRow
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.ScreenColumn import com.rtbishop.look4sat.core.presentation.ScreenColumn
import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.SwipeableItem import com.rtbishop.look4sat.core.presentation.SwipeableItem
import com.rtbishop.look4sat.core.presentation.TimerRow import com.rtbishop.look4sat.core.presentation.TimerRow
import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.TopBar
@@ -99,14 +99,31 @@ private fun PassesScreen(
navigateToRadar: (Int, Long) -> Unit navigateToRadar: (Int, Long) -> Unit
) { ) {
if (uiState.isPassesDialogShown) { if (uiState.isPassesDialogShown) {
PassesDialog( PassesFilterDialog(
hours = uiState.hours, hours = uiState.hours,
elevation = uiState.elevation, elevation = uiState.elevation,
lowElevation = uiState.lowElevation,
highElevation = uiState.highElevation,
aosStartMinute = uiState.aosStartMinute,
aosEndMinute = uiState.aosEndMinute,
invertAosTimeWindow = uiState.invertAosTimeWindow,
showDeepSpace = uiState.showDeepSpace, showDeepSpace = uiState.showDeepSpace,
cancel = { onAction(PassesAction.TogglePassesDialog) } cancel = { onAction(PassesAction.TogglePassesDialog) },
) { hours, elevation, showDeepSpace -> accept = { params ->
onAction(PassesAction.FilterPasses(hours, elevation, showDeepSpace)) onAction(
} PassesAction.FilterPasses(
hoursAhead = params.hours,
minElevation = params.elevation,
lowElevation = params.lowElevation,
highElevation = params.highElevation,
aosStartMinute = params.aosStartMinute,
aosEndMinute = params.aosEndMinute,
invertAosTimeWindow = params.invertAosTimeWindow,
showDeepSpace = params.showDeepSpace
)
)
}
)
} }
if (uiState.isRadiosDialogShown) { if (uiState.isRadiosDialogShown) {
RadiosDialog( RadiosDialog(
@@ -117,11 +134,19 @@ private fun PassesScreen(
} }
} }
if (uiState.shouldSeeWhatsNew) { if (uiState.shouldSeeWhatsNew) {
InfoDialog( val dismiss = { onAction(PassesAction.DismissWhatsNew) }
SharedDialog(
title = stringResource(R.string.pass_whatsnew_title), title = stringResource(R.string.pass_whatsnew_title),
text = stringResource(R.string.pass_whatsnew_message) onDismissRequest = dismiss,
) { onAccept = dismiss,
onAction(PassesAction.DismissWhatsNew) titleFontSize = 18
) { padding ->
Text(
text = stringResource(R.string.pass_whatsnew_message),
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = padding)
)
} }
} }
ScreenColumn( ScreenColumn(
@@ -29,6 +29,11 @@ data class PassesState(
val isNextTimeAos: Boolean = true, val isNextTimeAos: Boolean = true,
val hours: Int = 24, val hours: Int = 24,
val elevation: Double = 16.0, val elevation: Double = 16.0,
val lowElevation: Double = 16.0,
val highElevation: Double = 65.0,
val aosStartMinute: Int = 0,
val aosEndMinute: Int = 23 * 60 + 59,
val invertAosTimeWindow: Boolean = false,
val showDeepSpace: Boolean = true, val showDeepSpace: Boolean = true,
val modes: List<String> = emptyList(), val modes: List<String> = emptyList(),
val itemsList: List<OrbitalPass> = emptyList(), val itemsList: List<OrbitalPass> = emptyList(),
@@ -40,7 +45,16 @@ data class PassesState(
sealed interface PassesAction { sealed interface PassesAction {
data object DismissWhatsNew : PassesAction data object DismissWhatsNew : PassesAction
data class FilterPasses(val hoursAhead: Int, val minElevation: Double, val showDeepSpace: Boolean) : PassesAction data class FilterPasses(
val hoursAhead: Int,
val minElevation: Double,
val lowElevation: Double,
val highElevation: Double,
val aosStartMinute: Int,
val aosEndMinute: Int,
val invertAosTimeWindow: Boolean,
val showDeepSpace: Boolean
) : PassesAction
data class FilterRadios(val modes: List<String>) : PassesAction data class FilterRadios(val modes: List<String>) : PassesAction
data object RefreshPasses : PassesAction data object RefreshPasses : PassesAction
data object TogglePassesDialog : PassesAction data object TogglePassesDialog : PassesAction
@@ -54,6 +54,11 @@ class PassesViewModel(
nextPass = defaultPass, nextPass = defaultPass,
hours = settingsRepo.passesSettings.value.hoursAhead, hours = settingsRepo.passesSettings.value.hoursAhead,
elevation = settingsRepo.passesSettings.value.minElevation, elevation = settingsRepo.passesSettings.value.minElevation,
lowElevation = settingsRepo.otherSettings.value.lowElevation,
highElevation = settingsRepo.otherSettings.value.highElevation,
aosStartMinute = settingsRepo.passesSettings.value.aosStartMinute,
aosEndMinute = settingsRepo.passesSettings.value.aosEndMinute,
invertAosTimeWindow = settingsRepo.passesSettings.value.invertAosTimeWindow,
showDeepSpace = settingsRepo.passesSettings.value.showDeepSpace, showDeepSpace = settingsRepo.passesSettings.value.showDeepSpace,
modes = settingsRepo.passesSettings.value.selectedModes, modes = settingsRepo.passesSettings.value.selectedModes,
shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew
@@ -71,7 +76,14 @@ class PassesViewModel(
// React to settings changes: update UTC flag and whatsNew // React to settings changes: update UTC flag and whatsNew
viewModelScope.launch { viewModelScope.launch {
settingsRepo.otherSettings.collectLatest { settings -> settingsRepo.otherSettings.collectLatest { settings ->
_uiState.update { it.copy(isUtc = settings.stateOfUtc, shouldSeeWhatsNew = settings.shouldSeeWhatsNew) } _uiState.update {
it.copy(
isUtc = settings.stateOfUtc,
shouldSeeWhatsNew = settings.shouldSeeWhatsNew,
lowElevation = settings.lowElevation,
highElevation = settings.highElevation
)
}
} }
} }
// Main tick loop: reacts to new passes, then ticks every second // Main tick loop: reacts to new passes, then ticks every second
@@ -107,9 +119,29 @@ class PassesViewModel(
when (action) { when (action) {
PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed() PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed()
is PassesAction.FilterPasses -> is PassesAction.FilterPasses ->
applyFilter(action.hoursAhead, action.minElevation, action.showDeepSpace, _uiState.value.modes) applyFilter(
hoursAhead = action.hoursAhead,
minElevation = action.minElevation,
lowElevation = action.lowElevation,
highElevation = action.highElevation,
aosStartMinute = action.aosStartMinute,
aosEndMinute = action.aosEndMinute,
invertAosTimeWindow = action.invertAosTimeWindow,
showDeepSpace = action.showDeepSpace,
modes = _uiState.value.modes
)
is PassesAction.FilterRadios -> is PassesAction.FilterRadios ->
applyFilter(_uiState.value.hours, _uiState.value.elevation, _uiState.value.showDeepSpace, action.modes) applyFilter(
hoursAhead = _uiState.value.hours,
minElevation = _uiState.value.elevation,
lowElevation = _uiState.value.lowElevation,
highElevation = _uiState.value.highElevation,
aosStartMinute = _uiState.value.aosStartMinute,
aosEndMinute = _uiState.value.aosEndMinute,
invertAosTimeWindow = _uiState.value.invertAosTimeWindow,
showDeepSpace = _uiState.value.showDeepSpace,
modes = action.modes
)
PassesAction.RefreshPasses -> refreshPasses() PassesAction.RefreshPasses -> refreshPasses()
PassesAction.TogglePassesDialog -> PassesAction.TogglePassesDialog ->
_uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) } _uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) }
@@ -198,19 +230,61 @@ class PassesViewModel(
private fun applyFilter( private fun applyFilter(
hoursAhead: Int, hoursAhead: Int,
minElevation: Double, minElevation: Double,
lowElevation: Double,
highElevation: Double,
aosStartMinute: Int,
aosEndMinute: Int,
invertAosTimeWindow: Boolean,
showDeepSpace: Boolean, showDeepSpace: Boolean,
modes: List<String> modes: List<String>
) = viewModelScope.launch { ) = viewModelScope.launch {
settingsRepo.setPassesSettings(PassesSettings(showDeepSpace, hoursAhead, minElevation, modes)) settingsRepo.setPassesSettings(
PassesSettings(
showDeepSpace,
hoursAhead,
minElevation,
aosStartMinute,
aosEndMinute,
invertAosTimeWindow,
modes
)
)
settingsRepo.updateOtherSettings { it.copy(lowElevation = lowElevation, highElevation = highElevation) }
_uiState.update { _uiState.update {
it.copy(hours = hoursAhead, elevation = minElevation, showDeepSpace = showDeepSpace, modes = modes) it.copy(
hours = hoursAhead,
elevation = minElevation,
lowElevation = lowElevation,
highElevation = highElevation,
aosStartMinute = aosStartMinute,
aosEndMinute = aosEndMinute,
invertAosTimeWindow = invertAosTimeWindow,
showDeepSpace = showDeepSpace,
modes = modes
)
} }
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes) satelliteRepo.calculatePasses(
time = System.currentTimeMillis(),
hoursAhead = hoursAhead,
minElevation = minElevation,
aosStartMinute = aosStartMinute,
aosEndMinute = aosEndMinute,
invertAosTimeWindow = invertAosTimeWindow,
modes = modes
)
} }
private fun refreshPasses() = viewModelScope.launch { private fun refreshPasses() = viewModelScope.launch {
val (_, hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value val settings = settingsRepo.passesSettings.value
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes) satelliteRepo.calculatePasses(
time = System.currentTimeMillis(),
hoursAhead = settings.hoursAhead,
minElevation = settings.minElevation,
aosStartMinute = settings.aosStartMinute,
aosEndMinute = settings.aosEndMinute,
invertAosTimeWindow = settings.invertAosTimeWindow,
modes = settings.selectedModes
)
} }
companion object { companion object {
@@ -53,10 +53,10 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.IconCard
import com.rtbishop.look4sat.core.presentation.OutlinedText import com.rtbishop.look4sat.core.presentation.OutlinedText
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.infiniteMarquee import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -95,53 +95,44 @@ internal fun SstvPage(
if (showModeDialog.value) { if (showModeDialog.value) {
val allModes = remember(sstv.supportedModes) { listOf("Auto") + sstv.supportedModes } val allModes = remember(sstv.supportedModes) { listOf("Auto") + sstv.supportedModes }
Dialog(onDismissRequest = { showModeDialog.value = false }) { val dismiss = { showModeDialog.value = false }
ElevatedCard { SharedDialog(
Column( title = "SSTV Mode",
horizontalAlignment = Alignment.CenterHorizontally, onDismissRequest = dismiss,
modifier = Modifier.padding(vertical = 16.dp) onAccept = dismiss
) { ) { _ ->
Text( LazyColumn(
text = "SSTV Mode", modifier = Modifier
fontSize = 16.sp, .fillMaxHeight(0.7f)
fontWeight = FontWeight.Medium, .background(MaterialTheme.colorScheme.background),
color = MaterialTheme.colorScheme.primary, verticalArrangement = Arrangement.spacedBy(1.dp)
modifier = Modifier.padding(bottom = 12.dp) ) {
) items(allModes) { mode ->
LazyColumn( Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.fillMaxHeight(0.5f) .fillMaxWidth()
.background(MaterialTheme.colorScheme.background), .background(MaterialTheme.colorScheme.surface)
verticalArrangement = Arrangement.spacedBy(1.dp) .clickable {
) { onAction(RadarAction.SstvSelectMode(mode))
items(allModes) { mode -> showModeDialog.value = false
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surface)
.clickable {
onAction(RadarAction.SstvSelectMode(mode))
showModeDialog.value = false
}
) {
Text(
text = mode,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
RadioButton(
selected = mode == sstv.selectedMode,
onClick = null,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
)
} }
} ) {
Text(
text = mode,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.weight(1f)
.padding(start = 16.dp)
)
RadioButton(
selected = mode == sstv.selectedMode,
onClick = null,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)
)
} }
} }
} }
@@ -62,10 +62,10 @@ import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
import com.rtbishop.look4sat.core.presentation.CardLoadingIndicator import com.rtbishop.look4sat.core.presentation.CardLoadingIndicator
import com.rtbishop.look4sat.core.presentation.EmptyListCard import com.rtbishop.look4sat.core.presentation.EmptyListCard
import com.rtbishop.look4sat.core.presentation.IconCard import com.rtbishop.look4sat.core.presentation.IconCard
import com.rtbishop.look4sat.core.presentation.InfoDialog
import com.rtbishop.look4sat.core.presentation.MainTheme import com.rtbishop.look4sat.core.presentation.MainTheme
import com.rtbishop.look4sat.core.presentation.PrimaryIconCard import com.rtbishop.look4sat.core.presentation.PrimaryIconCard
import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.SharedDialog
import com.rtbishop.look4sat.core.presentation.TopBar import com.rtbishop.look4sat.core.presentation.TopBar
import com.rtbishop.look4sat.core.presentation.infiniteMarquee import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import com.rtbishop.look4sat.core.presentation.isVerticalLayout import com.rtbishop.look4sat.core.presentation.isVerticalLayout
@@ -95,11 +95,19 @@ private fun SatellitesScreen(
) )
} }
if (uiState.shouldSeeWarning) { if (uiState.shouldSeeWarning) {
InfoDialog( val dismiss = { onAction(SatellitesAction.DismissWarning) }
stringResource(R.string.sat_warning_title), SharedDialog(
stringResource(R.string.sat_warning_message) title = stringResource(R.string.sat_warning_title),
) { onDismissRequest = dismiss,
onAction(SatellitesAction.DismissWarning) onAccept = dismiss,
titleFontSize = 18
) { padding ->
Text(
text = stringResource(R.string.sat_warning_message),
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = padding)
)
} }
} }
@@ -69,13 +69,16 @@ fun PositionDialog(lat: Double, lon: Double, dismiss: () -> Unit, save: (Double,
OutlinedTextField( OutlinedTextField(
value = latValue.value, value = latValue.value,
onValueChange = { latValue.value = it }, onValueChange = { latValue.value = it },
label = { Text(text = stringResource(id = R.string.prefs_station_lat_text)) } label = { Text(text = stringResource(id = R.string.prefs_station_lat_text)) },
modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large),
) )
OutlinedTextField( OutlinedTextField(
value = lonValue.value, value = lonValue.value,
onValueChange = { lonValue.value = it }, onValueChange = { lonValue.value = it },
label = { Text(text = stringResource(id = R.string.prefs_station_lon_text)) } label = { Text(text = stringResource(id = R.string.prefs_station_lon_text)) },
modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large),
) )
Spacer(modifier = Modifier.height(0.dp))
} }
} }
@@ -101,8 +104,10 @@ fun LocatorDialog(qthLocator: String, dismiss: () -> Unit, save: (String) -> Uni
OutlinedTextField( OutlinedTextField(
value = locator.value, value = locator.value,
onValueChange = { locator.value = it }, onValueChange = { locator.value = it },
label = { Text(text = stringResource(id = R.string.prefs_locator_text)) } label = { Text(text = stringResource(id = R.string.prefs_locator_text)) },
modifier = Modifier.fillMaxWidth().padding(horizontal = LocalSpacing.current.large),
) )
Spacer(modifier = Modifier.height(0.dp))
} }
} }
@@ -208,6 +213,7 @@ fun DataSourcesDialog(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTransceivers.value, enabled = isEnabledCustomTransceivers.value,
) )
Spacer(modifier = Modifier.height(12.dp))
} }
} }
} }
@@ -298,6 +304,7 @@ fun NetworkOutputDialog(
onFormatChange = { frequencyFormat.value = it }, onFormatChange = { frequencyFormat.value = it },
formatLabel = stringResource(R.string.prefs_net_frequency_format_hint) formatLabel = stringResource(R.string.prefs_net_frequency_format_hint)
) )
Spacer(modifier = Modifier.height(12.dp))
} }
} }
} }
@@ -391,6 +398,7 @@ fun BluetoothOutputDialog(
onFormatChange = { frequencyFormat.value = it }, onFormatChange = { frequencyFormat.value = it },
formatLabel = stringResource(R.string.prefs_bt_frequency_output_hint) formatLabel = stringResource(R.string.prefs_bt_frequency_output_hint)
) )
Spacer(modifier = Modifier.height(12.dp))
} }
} }
} }
@@ -642,6 +650,7 @@ fun RadioControlDialog(
) )
} }
} }
Spacer(modifier = Modifier.height(6.dp))
} }
} }
} }