mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 03:15:37 +00:00
Implemented AOS window and elevation highlight filters
This commit is contained in:
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
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-4
@@ -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(
|
||||||
|
|||||||
+9
-1
@@ -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
|
||||||
|
|||||||
+92
-38
@@ -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 >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 < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">Media %1$d-%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">Alta > %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 (период >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">Низко < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">Средне %1$d-%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">Высоко > %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 (කාලය >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">අඩු < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">මධ්ය %1$d-%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">ඉහළ > %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 >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 < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">Orta %1$d-%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">Yüksek > %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 (період >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">Низько < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">Середньо %1$d-%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">Високо > %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(周期 >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">低 < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">高 > %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 < %1$d°</string>
|
||||||
|
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
|
||||||
|
<string name="prefs_highlight_high">High > %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!
|
||||||
|
|||||||
+222
-18
@@ -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
|
||||||
|
|||||||
+82
-8
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-6
@@ -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)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-3
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user