mirror of
https://github.com/rt-bishop/Look4Sat.git
synced 2026-10-02 19:17:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3df358b88c | ||
|
|
7d5bca300d | ||
|
|
fb2cd85b11 | ||
|
|
99158e74fa | ||
|
|
8265b73d75 | ||
|
|
d2b3184084 |
No files matched your search
@@ -25,19 +25,17 @@ import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -58,128 +56,152 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
|
||||
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.feature.map.mapDestination
|
||||
import com.rtbishop.look4sat.feature.passes.passesDestination
|
||||
import com.rtbishop.look4sat.feature.radar.radarDestination
|
||||
import com.rtbishop.look4sat.feature.radiocontrol.radioControlDestination
|
||||
import com.rtbishop.look4sat.feature.satellites.satellitesDestination
|
||||
import com.rtbishop.look4sat.feature.settings.settingsDestination
|
||||
import com.rtbishop.look4sat.feature.map.MapDestination
|
||||
import com.rtbishop.look4sat.feature.passes.PassesDestination
|
||||
import com.rtbishop.look4sat.feature.radar.RadarDestination
|
||||
import com.rtbishop.look4sat.feature.radiocontrol.RadioControlDestination
|
||||
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
|
||||
import com.rtbishop.look4sat.feature.settings.SettingsDestination
|
||||
|
||||
@Composable
|
||||
fun MainScreen(navController: NavHostController = rememberNavController()) {
|
||||
val items = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Map, Screen.Settings)
|
||||
val currentDestination = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||
val startDestination = Screen.Passes.route
|
||||
fun MainScreen() {
|
||||
val backStack = rememberNavBackStack(Screen.Passes)
|
||||
val currentKey = backStack.lastOrNull()
|
||||
val navigateBack: () -> Unit = { backStack.removeAt(backStack.size - 1) }
|
||||
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
|
||||
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar(), Screen.Map, Screen.Settings)
|
||||
|
||||
// Observe radio tracking state for the status bar
|
||||
val context = LocalContext.current
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||
|
||||
NavigationSuiteScaffold(
|
||||
navigationSuiteItems = {
|
||||
items.forEach {
|
||||
navItems.forEach { screen ->
|
||||
val isSelected = when (currentKey) {
|
||||
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(it.iconResId), stringResource(it.titleResId)) },
|
||||
label = { Text(stringResource(it.titleResId)) },
|
||||
selected = currentDestination?.contains(it.route) ?: false,
|
||||
icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) },
|
||||
label = { Text(stringResource(screen.titleResId)) },
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
if (currentDestination?.contains(it.route) ?: false) return@item
|
||||
navController.navigate(it.route) {
|
||||
popUpTo(startDestination) { saveState = false }
|
||||
launchSingleTop = true
|
||||
restoreState = false
|
||||
}
|
||||
})
|
||||
if (isSelected) return@item
|
||||
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
|
||||
if (screen !is Screen.Passes) backStack.add(screen)
|
||||
}
|
||||
)
|
||||
}
|
||||
}, navigationSuiteColors = NavigationSuiteDefaults.colors(
|
||||
},
|
||||
navigationSuiteColors = NavigationSuiteDefaults.colors(
|
||||
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
), layoutType = when {
|
||||
),
|
||||
layoutType = when {
|
||||
!hasEnoughHeight() && hasEnoughWidth() -> NavigationSuiteType.NavigationRail
|
||||
!hasEnoughWidth() -> NavigationSuiteType.ShortNavigationBarCompact
|
||||
else -> NavigationSuiteType.ShortNavigationBarMedium
|
||||
}
|
||||
) {
|
||||
Column {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = startDestination,
|
||||
enterTransition = { fadeIn(animationSpec = tween(350)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(350)) },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
satellitesDestination { navController.navigateUp() }
|
||||
passesDestination { catNum: Int, aosTime: Long ->
|
||||
val radarRoute = "${Screen.Radar.route}?catNum=${catNum}&aosTime=${aosTime}"
|
||||
navController.navigate(radarRoute)
|
||||
}
|
||||
radarDestination(
|
||||
navigateUp = { navController.navigateUp() },
|
||||
navigateToRadioControl = { catNum, aosTime ->
|
||||
val route = "${Screen.RadioControl.route}?catNum=$catNum&aosTime=$aosTime"
|
||||
navController.navigate(route)
|
||||
}
|
||||
)
|
||||
radioControlDestination { navController.navigateUp() }
|
||||
mapDestination()
|
||||
settingsDestination()
|
||||
}
|
||||
|
||||
// Radio tracking status banner (above bottom navigation)
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.clickable {
|
||||
val pass = trackingState.currentPass
|
||||
if (pass != null) {
|
||||
val route = "${Screen.RadioControl.route}?catNum=${pass.catNum}&aosTime=${pass.aosTime}"
|
||||
navController.navigate(route)
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
modifier = Modifier.weight(1f),
|
||||
onBack = navigateBack,
|
||||
transitionSpec = { fadeTransition },
|
||||
popTransitionSpec = { fadeTransition },
|
||||
predictivePopTransitionSpec = { fadeTransition },
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Satellites> {
|
||||
SatellitesDestination(navigateUp = navigateBack)
|
||||
}
|
||||
entry<Screen.Passes> {
|
||||
PassesDestination { catNum, aosTime ->
|
||||
backStack.add(Screen.Radar(catNum, aosTime))
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
Box(
|
||||
entry<Screen.Radar> { route ->
|
||||
RadarDestination(
|
||||
catNum = route.catNum,
|
||||
aosTime = route.aosTime,
|
||||
navigateUp = navigateBack,
|
||||
navigateToRadioControl = { catNum, aosTime ->
|
||||
backStack.add(Screen.RadioControl(catNum, aosTime))
|
||||
}
|
||||
)
|
||||
}
|
||||
entry<Screen.RadioControl> { route ->
|
||||
RadioControlDestination(
|
||||
catNum = route.catNum,
|
||||
aosTime = route.aosTime,
|
||||
navigateUp = navigateBack
|
||||
)
|
||||
}
|
||||
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
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
||||
)
|
||||
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
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.clickable {
|
||||
val pass = trackingState.currentPass
|
||||
if (pass != null) {
|
||||
backStack.add(Screen.RadioControl(pass.catNum, pass.aosTime))
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 12.dp, vertical = 6.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(8.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFF4CAF50).copy(alpha = alpha))
|
||||
)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} // end Column
|
||||
}
|
||||
}
|
||||
+2
@@ -38,6 +38,8 @@ internal class ApplicationPlugin : Plugin<Project> {
|
||||
implementation(project(":feature:satellites"))
|
||||
implementation(project(":feature:settings"))
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.compose.material3.adaptive)
|
||||
implementation(libs.compose.navigation3)
|
||||
androidTestImplementation(libs.bundles.androidTest)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ internal class CoreDataPlugin : Plugin<Project> {
|
||||
implementation(libs.androidx.room)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
ksp(libs.androidx.room.compiler)
|
||||
implementation(libs.other.coroutines)
|
||||
implementation(libs.kotlin.coroutines)
|
||||
implementation(libs.other.okhttp)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -25,10 +25,11 @@ import org.gradle.kotlin.dsl.dependencies
|
||||
internal class CoreDomainPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) = with(target) {
|
||||
applyPlugin(libs.plugins.kotlin.jvm)
|
||||
applyPlugin(libs.plugins.kotlin.serialization)
|
||||
setupKotlin()
|
||||
dependencies {
|
||||
implementation(libs.other.coroutines)
|
||||
implementation(libs.other.json)
|
||||
implementation(libs.kotlin.coroutines)
|
||||
implementation(libs.kotlin.serialization)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
@@ -24,12 +24,15 @@ import org.gradle.kotlin.dsl.dependencies
|
||||
@Suppress("Unused")
|
||||
internal class CorePresentationPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) = with(target) {
|
||||
applyPlugin(libs.plugins.kotlin.serialization)
|
||||
setupAndroidLib()
|
||||
setupCompose()
|
||||
setupKotlin()
|
||||
dependencies {
|
||||
implementation(project(":core:domain"))
|
||||
implementation(libs.androidx.core.splashscreen)
|
||||
implementation(libs.kotlin.serialization)
|
||||
implementation(libs.compose.material3.adaptive)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -74,7 +74,7 @@ internal fun Project.setupAndroidApp() {
|
||||
}
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
localeFilters.addAll(listOf("en", "es", "ru", "si", "uk", "zh"))
|
||||
localeFilters.addAll(listOf("en", "es", "ru", "si", "tr", "uk", "zh"))
|
||||
}
|
||||
packaging { resources { excludes += listOf("META-INF/*") } }
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ plugins {
|
||||
alias(libs.plugins.compose.compiler) apply false
|
||||
alias(libs.plugins.google.ksp) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
}
|
||||
|
||||
tasks.register("clean", Delete::class.java) {
|
||||
|
||||
@@ -25,11 +25,11 @@ data class SatRadio(
|
||||
@PrimaryKey val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlinkLow: Long?,
|
||||
var downlinkHigh: Long?,
|
||||
val downlinkLow: Long?,
|
||||
val downlinkHigh: Long?,
|
||||
val downlinkMode: String?,
|
||||
var uplinkLow: Long?,
|
||||
var uplinkHigh: Long?,
|
||||
val uplinkLow: Long?,
|
||||
val uplinkHigh: Long?,
|
||||
val uplinkMode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?
|
||||
|
||||
+31
-28
@@ -31,6 +31,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@@ -45,6 +46,9 @@ class SatelliteRepo(
|
||||
private val _passes = MutableStateFlow<List<OrbitalPass>>(emptyList())
|
||||
override val passes: StateFlow<List<OrbitalPass>> = _passes
|
||||
|
||||
private val _isCalculating = MutableStateFlow(false)
|
||||
override val isCalculating: StateFlow<Boolean> = _isCalculating
|
||||
|
||||
private val _satellites = MutableStateFlow<List<OrbitalObject>>(emptyList())
|
||||
override val satellites: StateFlow<List<OrbitalObject>> = _satellites
|
||||
|
||||
@@ -53,7 +57,7 @@ class SatelliteRepo(
|
||||
override suspend fun initRepository() = withContext(dispatcher) {
|
||||
settingsRepo.selectedIds.collect { selectedIds ->
|
||||
_satellites.update { localStorage.getEntriesWithIds(selectedIds) }
|
||||
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||
val (_, hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||
calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
|
||||
}
|
||||
}
|
||||
@@ -95,41 +99,40 @@ class SatelliteRepo(
|
||||
}
|
||||
|
||||
override suspend fun calculatePasses(time: Long, hoursAhead: Int, minElevation: Double, modes: List<String>) {
|
||||
_isCalculating.value = true
|
||||
// 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
|
||||
val normalizedTime = time / 60_000L * 60_000L
|
||||
val currentSatellites = _satellites.value
|
||||
if (currentSatellites.isNotEmpty()) {
|
||||
withContext(dispatcher) {
|
||||
val idsWithModes = localStorage.getIdsWithModes(modes)
|
||||
val stationPos = settingsRepo.stationPosition.value
|
||||
val filteredSatellites = if (idsWithModes.isEmpty()) {
|
||||
currentSatellites
|
||||
} else {
|
||||
currentSatellites.filter { it.data.catnum in idsWithModes }
|
||||
}
|
||||
// Compute passes for each satellite in parallel
|
||||
val passLists = coroutineScope {
|
||||
filteredSatellites.map { satellite ->
|
||||
async { satellite.getPasses(stationPos, normalizedTime, hoursAhead) }
|
||||
}.awaitAll()
|
||||
}
|
||||
// Flatten and filter in a single pass
|
||||
val timeFuture = normalizedTime + (hoursAhead * 60L * 60L * 1000L)
|
||||
val newPasses = ArrayList<OrbitalPass>()
|
||||
for (list in passLists) {
|
||||
for (pass in list) {
|
||||
if (pass.losTime > time && pass.aosTime < timeFuture && pass.maxElevation > minElevation) {
|
||||
newPasses.add(pass)
|
||||
}
|
||||
withContext(dispatcher) {
|
||||
val idsWithModes = localStorage.getIdsWithModes(modes)
|
||||
val stationPos = settingsRepo.stationPosition.value
|
||||
val filteredSatellites = if (idsWithModes.isEmpty()) {
|
||||
currentSatellites
|
||||
} else {
|
||||
currentSatellites.filter { it.data.catnum in idsWithModes }
|
||||
}
|
||||
// Compute passes for each satellite in parallel
|
||||
val passLists = coroutineScope {
|
||||
filteredSatellites.map { satellite ->
|
||||
async { satellite.getPasses(stationPos, normalizedTime, hoursAhead) }
|
||||
}.awaitAll()
|
||||
}
|
||||
// Flatten and filter in a single pass
|
||||
val timeFuture = normalizedTime + (hoursAhead * 60L * 60L * 1000L)
|
||||
val newPasses = ArrayList<OrbitalPass>()
|
||||
for (list in passLists) {
|
||||
for (pass in list) {
|
||||
if (pass.losTime > time && pass.aosTime < timeFuture && pass.maxElevation > minElevation) {
|
||||
newPasses.add(pass)
|
||||
}
|
||||
}
|
||||
newPasses.sortBy { it.aosTime }
|
||||
_passes.update { newPasses }
|
||||
}
|
||||
} else {
|
||||
_passes.update { emptyList() }
|
||||
newPasses.sortBy { it.aosTime }
|
||||
delay(1000) // Simulate loading time for better UX
|
||||
_passes.update { newPasses }
|
||||
}
|
||||
_isCalculating.value = false
|
||||
}
|
||||
|
||||
private fun OrbitalObject.getPasses(pos: GeoPos, time: Long, hours: Int): List<OrbitalPass> {
|
||||
|
||||
@@ -51,6 +51,7 @@ class SettingsRepo(
|
||||
private val keyBluetoothFrequencyState = "bluetoothFrequencyState"
|
||||
private val keyBluetoothFrequencyAddress = "bluetoothFrequencyAddress"
|
||||
private val keyBluetoothFrequencyFormat = "bluetoothFrequencyFormat"
|
||||
private val keyFilterShowDeepSpace = "filterShowDeepSpace"
|
||||
private val keyFilterHoursAhead = "filterHoursAhead"
|
||||
private val keyFilterMinElevation = "filterMinElevation"
|
||||
private val keyNumberOfRadios = "numberOfRadios"
|
||||
@@ -121,6 +122,7 @@ class SettingsRepo(
|
||||
override val passesSettings: StateFlow<PassesSettings> = _passesSettings
|
||||
|
||||
override fun setPassesSettings(settings: PassesSettings) = preferences.edit {
|
||||
putBoolean(keyFilterShowDeepSpace, settings.showDeepSpace)
|
||||
putInt(keyFilterHoursAhead, settings.hoursAhead)
|
||||
putLong(keyFilterMinElevation, settings.minElevation.toRawBits())
|
||||
putString(keySelectedModes, settings.selectedModes.joinToString(separatorComma))
|
||||
@@ -128,11 +130,12 @@ class SettingsRepo(
|
||||
}
|
||||
|
||||
private fun getPassesSettings(): PassesSettings {
|
||||
val showDeepSpace = preferences.getBoolean(keyFilterShowDeepSpace, true)
|
||||
val hoursAhead = preferences.getInt(keyFilterHoursAhead, 24)
|
||||
val minElevation = Double.fromBits(preferences.getLong(keyFilterMinElevation, 16.0.toRawBits()))
|
||||
val selectedModesString = preferences.getString(keySelectedModes, null)
|
||||
val selectedModes = selectedModesString?.split(separatorComma)?.sorted() ?: emptyList()
|
||||
return PassesSettings(hoursAhead, minElevation, selectedModes)
|
||||
return PassesSettings(showDeepSpace, hoursAhead, minElevation, selectedModes)
|
||||
}
|
||||
//endregion
|
||||
|
||||
|
||||
@@ -17,16 +17,20 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.domain.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SatRadio(
|
||||
val uuid: String,
|
||||
val info: String,
|
||||
val isAlive: Boolean,
|
||||
var downlinkLow: Long?,
|
||||
var downlinkHigh: Long?,
|
||||
val downlinkMode: String?,
|
||||
var uplinkLow: Long?,
|
||||
var uplinkHigh: Long?,
|
||||
val uplinkMode: String?,
|
||||
val isInverted: Boolean,
|
||||
val catnum: Int?
|
||||
@SerialName("uuid") val uuid: String,
|
||||
@SerialName("description") val info: String,
|
||||
@SerialName("alive") val isAlive: Boolean,
|
||||
@SerialName("downlink_low") val downlinkLow: Long?,
|
||||
@SerialName("downlink_high") val downlinkHigh: Long?,
|
||||
@SerialName("mode") val downlinkMode: String?,
|
||||
@SerialName("uplink_low") val uplinkLow: Long?,
|
||||
@SerialName("uplink_high") val uplinkHigh: Long?,
|
||||
@SerialName("uplink_mode") val uplinkMode: String?,
|
||||
@SerialName("invert") val isInverted: Boolean,
|
||||
@SerialName("norad_cat_id") val catnum: Int?
|
||||
)
|
||||
@@ -24,6 +24,7 @@ data class DatabaseState(
|
||||
)
|
||||
|
||||
data class PassesSettings(
|
||||
val showDeepSpace: Boolean = true,
|
||||
val hoursAhead: Int,
|
||||
val minElevation: Double,
|
||||
val selectedModes: List<String>
|
||||
|
||||
+3
@@ -31,6 +31,9 @@ interface ISatelliteRepo {
|
||||
/** Raw calculated passes (without live progress). Updated on selection/filter change. */
|
||||
val passes: StateFlow<List<OrbitalPass>>
|
||||
|
||||
/** Whether the repo is currently calculating passes. */
|
||||
val isCalculating: StateFlow<Boolean>
|
||||
|
||||
/** Load satellite objects from DB based on the current selection. */
|
||||
suspend fun initRepository()
|
||||
|
||||
|
||||
@@ -21,13 +21,19 @@ import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import java.io.InputStream
|
||||
import kotlin.math.pow
|
||||
|
||||
class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
suspend fun parseCSVStream(stream: InputStream): List<OrbitalData> = withContext(dispatcher) {
|
||||
stream.bufferedReader().useLines { lines ->
|
||||
lines.drop(1).mapNotNull { parseCSV(it.split(",")) }.toList()
|
||||
@@ -43,8 +49,12 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
|
||||
suspend fun parseJSONStream(stream: InputStream): List<SatRadio> = withContext(dispatcher) {
|
||||
runCatching {
|
||||
val jsonArray = JSONArray(stream.bufferedReader().readText())
|
||||
(0 until jsonArray.length()).mapNotNull { parseJSON(jsonArray.getJSONObject(it)) }
|
||||
val root = json.parseToJsonElement(stream.bufferedReader().readText())
|
||||
(root as? JsonArray)?.mapNotNull { element ->
|
||||
runCatching { json.decodeFromJsonElement<SatRadio>(element) }
|
||||
.onFailure { println("JSON parsing exception: $it") }
|
||||
.getOrNull()
|
||||
} ?: emptyList()
|
||||
}.getOrDefault(emptyList())
|
||||
}
|
||||
|
||||
@@ -95,26 +105,6 @@ class DataParser(private val dispatcher: CoroutineDispatcher) {
|
||||
)
|
||||
}.onFailure { println("TLE parsing exception: $it") }.getOrNull()
|
||||
|
||||
private fun parseJSON(json: JSONObject): SatRadio? = runCatching {
|
||||
SatRadio(
|
||||
uuid = json.getString("uuid"),
|
||||
info = json.getString("description"),
|
||||
isAlive = json.getBoolean("alive"),
|
||||
downlinkLow = json.optLongOrNull("downlink_low"),
|
||||
downlinkHigh = json.optLongOrNull("downlink_high"),
|
||||
downlinkMode = json.optStringOrNull("mode"),
|
||||
uplinkLow = json.optLongOrNull("uplink_low"),
|
||||
uplinkHigh = json.optLongOrNull("uplink_high"),
|
||||
uplinkMode = json.optStringOrNull("uplink_mode"),
|
||||
isInverted = json.getBoolean("invert"),
|
||||
catnum = json.optIntOrNull("norad_cat_id")
|
||||
)
|
||||
}.onFailure { println("JSON parsing exception: $it") }.getOrNull()
|
||||
|
||||
private fun JSONObject.optStringOrNull(key: String): String? = if (isNull(key)) null else getString(key)
|
||||
private fun JSONObject.optLongOrNull(key: String): Long? = if (isNull(key)) null else getLong(key)
|
||||
private fun JSONObject.optIntOrNull(key: String): Int? = if (isNull(key)) null else getInt(key)
|
||||
|
||||
private fun getDayOfYear(year: Int, month: Int, dayOfMonth: Int): Int {
|
||||
val daysInMonth = intArrayOf(31, if (isLeapYear(year)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
|
||||
return daysInMonth.take(month - 1).sum() + dayOfMonth
|
||||
|
||||
+2
-2
@@ -2,9 +2,9 @@ package com.rtbishop.look4sat.core.domain
|
||||
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.utility.TransponderMapper
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class TransponderMapperTest {
|
||||
|
||||
|
||||
+40
-7
@@ -1,10 +1,43 @@
|
||||
/*
|
||||
* Look4Sat. Amateur radio satellite tracker and pass predictor.
|
||||
* Copyright (C) 2019-2026 Arty Bishop and contributors.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.rtbishop.look4sat.core.presentation
|
||||
|
||||
sealed class Screen(val route: String, val iconResId: Int, val titleResId: Int) {
|
||||
data object Satellites : Screen("satellites", R.drawable.ic_satellites, R.string.nav_sat)
|
||||
data object Passes : Screen("passes", R.drawable.ic_passes, R.string.nav_pass)
|
||||
data object Radar : Screen("radar", R.drawable.ic_radar, R.string.nav_radar)
|
||||
data object Map : Screen("map", R.drawable.ic_map, R.string.nav_map)
|
||||
data object Settings : Screen("settings", R.drawable.ic_settings, R.string.nav_prefs)
|
||||
data object RadioControl : Screen("radiocontrol", R.drawable.ic_radios, R.string.nav_radiocontrol)
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey {
|
||||
|
||||
@Serializable
|
||||
data object Satellites : Screen(R.drawable.ic_satellites, R.string.nav_sat)
|
||||
|
||||
@Serializable
|
||||
data object Passes : Screen(R.drawable.ic_passes, R.string.nav_pass)
|
||||
|
||||
@Serializable
|
||||
data class Radar(val catNum: Int = 0, val aosTime: Long = 0L) : Screen(R.drawable.ic_radar, R.string.nav_radar)
|
||||
|
||||
@Serializable
|
||||
data class RadioControl(val catNum: Int = 0, val aosTime: Long = 0L) : Screen(0, 0)
|
||||
|
||||
@Serializable
|
||||
data object Map : Screen(R.drawable.ic_map, R.string.nav_map)
|
||||
|
||||
@Serializable
|
||||
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Commmon -->
|
||||
<string name="app_name" translatable="false">Look4Sat</string>
|
||||
<string name="btn_accept">Kabul et</string>
|
||||
<string name="btn_cancel">İptal</string>
|
||||
<string name="empty_list_message">Gösterilecek veri bulunamadı</string>
|
||||
|
||||
<!-- Navigation -->
|
||||
<string name="nav_sat">Uydular</string>
|
||||
<string name="nav_pass">Geçişler</string>
|
||||
<string name="nav_radar">Radar</string>
|
||||
<string name="nav_map">Harita</string>
|
||||
<string name="nav_prefs">Ayarlar</string>
|
||||
|
||||
<!-- Satellites screen -->
|
||||
<string name="sat_type_hint">Uydu türü: %s</string>
|
||||
<string name="sat_type_title">Uydu türü seçin</string>
|
||||
<string name="sat_search_hint">Id - Ad</string>
|
||||
<string name="sat_search_clear">Temizle</string>
|
||||
<string name="sat_clear_all">Tümünü temizle</string>
|
||||
<string name="sat_select_all">Tümünü seç</string>
|
||||
<string name="sat_empty_list_message">"Arama sorgunuzun doğru olduğundan ve veritabanının güncellendiğinden emin olun"</string>
|
||||
<string name="sat_warning_title">Uyarı\!</string>
|
||||
<string name="sat_warning_message">
|
||||
Bu uygulamada 9000\'den fazla uydu listelenmiştir.
|
||||
Hepsini aynı anda takip etmek anlamsızdır.
|
||||
\n\nListeyi yalnızca ilgilendiğiniz uydularla
|
||||
daraltmak için arama ve tür seçiciyi kullanın.</string>
|
||||
|
||||
<!-- Passes screen -->
|
||||
<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_hours">İlerideki saatler</string>
|
||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
||||
<string name="pass_modes_title">Modulasyon türü seçin</string>
|
||||
<string name="pass_satId" translatable="false">%05d</string>
|
||||
<string name="pass_elevation">Yükseklik açısı: %.1f°</string>
|
||||
<string name="pass_deep_space" translatable="false">DeepSpace</string>
|
||||
<string name="pass_altitude">İrtifa: %d km</string>
|
||||
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
|
||||
<string name="pass_empty_list_message">
|
||||
Filtrelerinizin doğru olduğundan ve uydu seçtiğinizden emin olun</string>
|
||||
<string name="pass_error_message">Gösterilecek yaklaşan uydu geçişi bulunmuyor</string>
|
||||
<string name="pass_welcome_title">Look4Sat\'a hoş geldiniz\!</string>
|
||||
<string name="pass_welcome_message">
|
||||
Ayarlar\'dan GPS, Enlem/Boylam veya QTH kullanarak konumunuzu belirlediğinizden emin olun.
|
||||
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* Switched to short-form commands in NetworkReporter, by theojalba
|
||||
\n* Added CAT radio control for full-duplex SAT operation, by jcmerg
|
||||
\n* Added normalized time for passes AOS/LOS, fixed refresh state bug
|
||||
\n* Added multiple performance tweaks, improved passes calculation
|
||||
\n* Fixed UTC pass time problem mentioned in issue #201
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
<string name="radar_back">Geri</string>
|
||||
<string name="radar_notify">Bildir</string>
|
||||
<string name="radar_az_text">Azimut</string>
|
||||
<string name="radar_az_value" translatable="false">%.1f°</string>
|
||||
<string name="radar_el_text">Yükseklik açısı</string>
|
||||
<string name="radar_el_value" translatable="false">%.1f°</string>
|
||||
<string name="radar_alt_text">İrtifa</string>
|
||||
<string name="radar_alt_value" translatable="false">%.0f km</string>
|
||||
<string name="radar_dist_text">Mesafe</string>
|
||||
<string name="radar_dist_value" translatable="false">%.0f km</string>
|
||||
<string name="radar_eclipsed">Tutulumda</string>
|
||||
<string name="radar_downlink">Downlink</string>
|
||||
<string name="radar_uplink">Uplink</string>
|
||||
<string name="radar_link_low" translatable="false">%.4f</string>
|
||||
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
|
||||
<string name="radar_no_link" translatable="false">- - . - -</string>
|
||||
<string name="radar_no_data">Bu uyduda transceiver bulunmuyor</string>
|
||||
<string name="radar_add">Ekle</string>
|
||||
<string name="radar_data">Uydu transceiver\'ları:</string>
|
||||
<string name="radar_inverted">Ters çevrilmiş: %s</string>
|
||||
<string name="radar_mode">Modülasyon: %s</string>
|
||||
<string name="radar_string_no">Hayır</string>
|
||||
<string name="radar_string_yes">Evet</string>
|
||||
<string name="radar_visible">Görünür</string>
|
||||
|
||||
<!-- Map screen -->
|
||||
<string name="map_prev">Önceki</string>
|
||||
<string name="map_next">Sonraki</string>
|
||||
<string name="map_copyright" translatable="false">© OpenStreetMap contributors</string>
|
||||
<string name="map_azimuth">Azimut: %.1f°</string>
|
||||
<string name="map_elevation">Yükseklik açısı: %.1f°</string>
|
||||
<string name="map_altitude">İrtifa: %.0f km</string>
|
||||
<string name="map_distance">Mesafe: %.0f km</string>
|
||||
<string name="map_latitude">Enlem: %.1f°</string>
|
||||
<string name="map_longitude">Boylam: %.1f°</string>
|
||||
<string name="map_qth" translatable="false">QTH: %s</string>
|
||||
<string name="map_phase">Faz: %.1f°</string>
|
||||
<string name="map_eclipsed">Tutulumda</string>
|
||||
<string name="map_period">Periyot: %.0f dk</string>
|
||||
<string name="map_velocity">Hız: %.2f km/s</string>
|
||||
<string name="map_visibility">Görünürlük: %s</string>
|
||||
<string name="map_visible">Görünür</string>
|
||||
|
||||
<!-- Settings screen -->
|
||||
<string name="prefs_app_title" translatable="false">Look4Sat v%s</string>
|
||||
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
|
||||
<string name="prefs_donate_title">Bağış yap</string>
|
||||
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
|
||||
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
|
||||
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
|
||||
<string name="prefs_github_title" translatable="false">GitHub</string>
|
||||
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
|
||||
<string name="prefs_license_title" translatable="false">GPLv3</string>
|
||||
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
|
||||
<string name="prefs_privacy_title">Gizlilik Politikası</string>
|
||||
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
|
||||
|
||||
<string name="prefs_updated_title">Güncellendi: %s</string>
|
||||
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
|
||||
|
||||
<string name="prefs_loc_title">İstasyon konumu</string>
|
||||
<string name="prefs_loc_gps_title">GPS</string>
|
||||
<string name="prefs_loc_gps_error">Konum izinlerinizi kontrol edin</string>
|
||||
<string name="prefs_loc_input_title">Manuel giriş</string>
|
||||
<string name="prefs_loc_input_error">Geçersiz konum girildi</string>
|
||||
<string name="prefs_loc_qth_title">QTH</string>
|
||||
<string name="prefs_loc_qth_error">Geçersiz QTH konumlandırıcısı</string>
|
||||
<string name="prefs_loc_success">Konum başarıyla güncellendi</string>
|
||||
<string name="prefs_station_title">İstasyon konumu ayarları</string>
|
||||
<string name="prefs_station_lat_text">Yer istasyonunuzun enlemini girin</string>
|
||||
<string name="prefs_station_lon_text">Yer istasyonunuzun boylamını girin</string>
|
||||
<string name="prefs_locator_title">QTH konumlandırıcı ayarları</string>
|
||||
<string name="prefs_locator_text">İstasyon konumunu konumlandırıcı ile ayarlayın</string>
|
||||
|
||||
<string name="prefs_data_title">Uydu verisi</string>
|
||||
<string name="prefs_data_entries">Uydular: %s</string>
|
||||
<string name="prefs_data_radios">Transceiver\'lar: %s</string>
|
||||
<string name="prefs_data_update">Güncelle</string>
|
||||
<string name="prefs_data_import">İçe aktar</string>
|
||||
<string name="prefs_data_clear">Temizle</string>
|
||||
<string name="prefs_data_clear_success">Veriler başarıyla temizlendi</string>
|
||||
<string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string>
|
||||
|
||||
<string name="prefs_data_sources_title">Özel veri kaynakları</string>
|
||||
<string name="prefs_data_sources_tle_switch">Özel TLE URL</string>
|
||||
<string name="prefs_data_sources_transceivers_switch">Özel transceiver URL</string>
|
||||
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
|
||||
|
||||
<string name="prefs_data_output_title">Veri aktarımı</string>
|
||||
<string name="prefs_net_output">Ağ</string>
|
||||
<string name="prefs_bt_output">Bluetooth</string>
|
||||
<string name="prefs_cat_output">CAT</string>
|
||||
|
||||
<!-- Radio Control -->
|
||||
<string name="nav_radiocontrol">Radyo Kontrolü</string>
|
||||
<string name="rc_settings_title">CAT Radyo Kontrolü</string>
|
||||
<string name="rc_radio_model">Radyo Modeli</string>
|
||||
<string name="rc_tx_device_hint">TX Radyo BT Adresi</string>
|
||||
<string name="rc_rx_device_hint">RX Radyo BT Adresi</string>
|
||||
<string name="rc_tx_name_hint">TX Radyo Adı</string>
|
||||
<string name="rc_rx_name_hint">RX Radyo Adı</string>
|
||||
<string name="rc_enable_switch">CAT Kontrolünü Etkinleştir</string>
|
||||
|
||||
<string name="prefs_net_title">Ağ veri çıkışı</string>
|
||||
<string name="prefs_net_rotator_switch">Döndürme çıkışını etkinleştir</string>
|
||||
<string name="prefs_net_rotator_address_hint">IP:Port</string>
|
||||
<string name="prefs_net_rotator_format_hint">Biçim</string>
|
||||
<string name="prefs_net_frequency_switch">Frekans çıkışını etkinleştir</string>
|
||||
<string name="prefs_net_frequency_address_hint">IP:Port</string>
|
||||
<string name="prefs_net_frequency_format_hint">Biçim</string>
|
||||
|
||||
<string name="prefs_bt_title">Bluetooth veri çıkışı</string>
|
||||
<string name="prefs_bt_rotator_switch">Döndürme çıkışını etkinleştir</string>
|
||||
<string name="prefs_bt_rotator_device_hint">Cihaz kimliği</string>
|
||||
<string name="prefs_bt_rotator_output_hint">Veri biçimi</string>
|
||||
<string name="prefs_bt_frequency_switch">Frekans çıkışını etkinleştir</string>
|
||||
<string name="prefs_bt_frequency_device_hint">Cihaz kimliği</string>
|
||||
<string name="prefs_bt_frequency_output_hint">Veri biçimi</string>
|
||||
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
|
||||
|
||||
<string name="prefs_other_title">Diğer ayarlar</string>
|
||||
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</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_sensors">Radar görünümünü döndürmek için sensörleri kullan</string>
|
||||
|
||||
<string name="prefs_outro_title">Teşekkür etmek istiyorum</string>
|
||||
<string name="prefs_outro_thanks" translatable="false">
|
||||
• Look4Sat users and contributors!
|
||||
\n• David A. B. Johnson (predict4java)
|
||||
\n• Dave Moten (predict4java)
|
||||
\n• Alexandru Csete (Gpredict)
|
||||
\n• Dr T.S. Kelso (Celestrak)
|
||||
\n• Libre Space Foundation (SatNOGS)</string>
|
||||
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
|
||||
|
||||
</resources>
|
||||
@@ -32,6 +32,7 @@
|
||||
<string name="pass_filter_title">Filter passes</string>
|
||||
<string name="pass_filter_elev">Minimal elevation</string>
|
||||
<string name="pass_filter_hours">Hours ahead</string>
|
||||
<string name="pass_filter_deep_space">DeepSpace (period >225min)</string>
|
||||
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
|
||||
<string name="pass_modes_title">Select modulation type</string>
|
||||
<string name="pass_satId" translatable="false">%05d</string>
|
||||
@@ -48,11 +49,10 @@
|
||||
\n\nPlease update the database at least weekly to get accurate predictions.</string>
|
||||
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
|
||||
<string name="pass_whatsnew_message" translatable="false">
|
||||
* Switched to short-form commands in NetworkReporter, by theojalba
|
||||
\n* Added CAT radio control for full-duplex SAT operation, by jcmerg
|
||||
\n* Added normalized time for passes AOS/LOS, fixed refresh state bug
|
||||
\n* Added multiple performance tweaks, improved passes calculation
|
||||
\n* Fixed UTC pass time problem mentioned in issue #201
|
||||
* Added Turkish translation, by Emre Can Akdaş (TA3ECR)
|
||||
\n* Added DeepSpace passes filter option to the dialog
|
||||
\n* Fixed the radar blip disappearing while eclipsed
|
||||
\n* Fixed (hopefully) the refresh indicator being stuck
|
||||
</string>
|
||||
|
||||
<!-- Radar screen -->
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
* Switched to short-form commands in NetworkReporter (#206) by theojalba
|
||||
* Added CAT radio control for full-duplex SAT operation (#207) by jcmerg
|
||||
* Added normalized time for passes AOS/LOS, fixed refresh state bug
|
||||
* Added multiple performance tweaks, improved passes calculation
|
||||
* Simplified data output code, aligned with HamLib defaults
|
||||
* Fixed UTC pass time problem mentioned in issue #201
|
||||
* Added Turkish translation, by Emre Can Akdaş (TA3ECR)
|
||||
* Added DeepSpace passes filter option to the dialog
|
||||
* Fixed the radar blip disappearing while eclipsed
|
||||
* Fixed (hopefully) the refresh indicator being stuck
|
||||
@@ -1,6 +1,4 @@
|
||||
* Switched to short-form commands in NetworkReporter (#206) by theojalba
|
||||
* Added CAT radio control for full-duplex SAT operation (#207) by jcmerg
|
||||
* Added normalized time for passes AOS/LOS, fixed refresh state bug
|
||||
* Added multiple performance tweaks, improved passes calculation
|
||||
* Simplified data output code, aligned with HamLib defaults
|
||||
* Fixed UTC pass time problem mentioned in issue #201
|
||||
* Added Turkish translation, by Emre Can Akdaş (TA3ECR)
|
||||
* Added DeepSpace passes filter option to the dialog
|
||||
* Fixed the radar blip disappearing while eclipsed
|
||||
* Fixed (hopefully) the refresh indicator being stuck
|
||||
@@ -60,15 +60,12 @@ import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.NextPassRow
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.TimerRow
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
@@ -110,13 +107,15 @@ private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
}
|
||||
private val iconCache = LruCache<String, Drawable>(128)
|
||||
|
||||
fun NavGraphBuilder.mapDestination() {
|
||||
composable(Screen.Map.route) {
|
||||
val viewModel = viewModel(MapViewModel::class.java, factory = MapViewModel.Factory)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val mapView = rememberMapViewWithLifecycle()
|
||||
MapScreen(uiState, viewModel::onAction, mapView)
|
||||
}
|
||||
@Composable
|
||||
fun MapDestination() {
|
||||
val viewModel = viewModel(
|
||||
modelClass = MapViewModel::class.java,
|
||||
factory = MapViewModel.Factory
|
||||
)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val mapView = rememberMapViewWithLifecycle()
|
||||
MapScreen(uiState, viewModel::onAction, mapView)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -32,12 +32,15 @@ import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -65,17 +68,29 @@ private val hourSteps = listOf(1, 2, 4, 8, 12, 24, 48, 72, 96, 120, 144, 168, 19
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PassesDialogPreview() {
|
||||
MainTheme { PassesDialog(24, 16.0, {}) { _, _ -> } }
|
||||
MainTheme { PassesDialog(24, 16.0, true, {}) { _, _, _ -> } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun PassesDialog(hours: Int, elevation: Double, cancel: () -> Unit, accept: (Int, Double) -> Unit) {
|
||||
internal fun PassesDialog(
|
||||
hours: Int,
|
||||
elevation: Double,
|
||||
showDeepSpace: Boolean,
|
||||
cancel: () -> Unit,
|
||||
accept: (Int, Double, Boolean) -> Unit
|
||||
) {
|
||||
val hoursIndex = remember { mutableIntStateOf(hourSteps.indexOfFirst { it >= hours }.coerceAtLeast(0)) }
|
||||
val elevationValueNew = remember { mutableDoubleStateOf(elevation) }
|
||||
var deepSpaceEnabled by remember { mutableStateOf(showDeepSpace) }
|
||||
val onAccept = {
|
||||
accept(hourSteps[hoursIndex.intValue], elevationValueNew.doubleValue).also { cancel() }
|
||||
accept(hourSteps[hoursIndex.intValue], elevationValueNew.doubleValue, deepSpaceEnabled).also { cancel() }
|
||||
}
|
||||
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(
|
||||
title = stringResource(R.string.pass_filter_elev),
|
||||
value = elevationValueNew.doubleValue,
|
||||
@@ -188,3 +203,20 @@ internal fun RadiosDialog(modes: List<String>, cancel: () -> Unit, accept: (List
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = LocalSpacing.current.large)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
fontSize = 16.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
@@ -56,8 +56,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.rtbishop.look4sat.core.domain.predict.DeepSpaceObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.NearEarthObject
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalData
|
||||
@@ -68,7 +66,6 @@ import com.rtbishop.look4sat.core.presentation.InfoDialog
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import com.rtbishop.look4sat.core.presentation.NextPassRow
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||
import com.rtbishop.look4sat.core.presentation.TimerRow
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
@@ -79,15 +76,14 @@ import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
fun NavGraphBuilder.passesDestination(navigateToRadar: (Int, Long) -> Unit) {
|
||||
composable(Screen.Passes.route) {
|
||||
val viewModel = viewModel(
|
||||
modelClass = PassesViewModel::class.java,
|
||||
factory = PassesViewModel.Factory
|
||||
)
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
PassesScreen(uiState, viewModel::onAction, navigateToRadar)
|
||||
}
|
||||
@Composable
|
||||
fun PassesDestination(navigateToRadar: (Int, Long) -> Unit) {
|
||||
val viewModel = viewModel(
|
||||
modelClass = PassesViewModel::class.java,
|
||||
factory = PassesViewModel.Factory
|
||||
)
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
PassesScreen(uiState, viewModel::onAction, navigateToRadar)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -100,9 +96,10 @@ private fun PassesScreen(
|
||||
PassesDialog(
|
||||
hours = uiState.hours,
|
||||
elevation = uiState.elevation,
|
||||
showDeepSpace = uiState.showDeepSpace,
|
||||
cancel = { onAction(PassesAction.TogglePassesDialog) }
|
||||
) { hours, elevation ->
|
||||
onAction(PassesAction.FilterPasses(hours, elevation))
|
||||
) { hours, elevation, showDeepSpace ->
|
||||
onAction(PassesAction.FilterPasses(hours, elevation, showDeepSpace))
|
||||
}
|
||||
}
|
||||
if (uiState.isRadiosDialogShown) {
|
||||
|
||||
@@ -29,6 +29,7 @@ data class PassesState(
|
||||
val isNextTimeAos: Boolean = true,
|
||||
val hours: Int = 24,
|
||||
val elevation: Double = 16.0,
|
||||
val showDeepSpace: Boolean = true,
|
||||
val modes: List<String> = emptyList(),
|
||||
val itemsList: List<OrbitalPass> = emptyList(),
|
||||
val shouldSeeWhatsNew: Boolean = false
|
||||
@@ -36,7 +37,7 @@ data class PassesState(
|
||||
|
||||
sealed interface PassesAction {
|
||||
data object DismissWhatsNew : PassesAction
|
||||
data class FilterPasses(val hoursAhead: Int, val minElevation: Double) : PassesAction
|
||||
data class FilterPasses(val hoursAhead: Int, val minElevation: Double, val showDeepSpace: Boolean) : PassesAction
|
||||
data class FilterRadios(val modes: List<String>) : PassesAction
|
||||
data object RefreshPasses : PassesAction
|
||||
data object TogglePassesDialog : PassesAction
|
||||
|
||||
+29
-30
@@ -34,7 +34,6 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -51,6 +50,7 @@ class PassesViewModel(
|
||||
nextPass = defaultPass,
|
||||
hours = settingsRepo.passesSettings.value.hoursAhead,
|
||||
elevation = settingsRepo.passesSettings.value.minElevation,
|
||||
showDeepSpace = settingsRepo.passesSettings.value.showDeepSpace,
|
||||
modes = settingsRepo.passesSettings.value.selectedModes,
|
||||
shouldSeeWhatsNew = settingsRepo.otherSettings.value.shouldSeeWhatsNew
|
||||
)
|
||||
@@ -58,29 +58,20 @@ class PassesViewModel(
|
||||
val uiState: StateFlow<PassesState> = _uiState
|
||||
|
||||
init {
|
||||
// Show refreshing indicator whenever the selected satellites list changes
|
||||
// Refresh indicator: mirrors the repo's isCalculating state
|
||||
viewModelScope.launch {
|
||||
settingsRepo.selectedIds.collectLatest { selectedIds ->
|
||||
_uiState.update { it.copy(isRefreshing = true) }
|
||||
if (selectedIds.isEmpty()) {
|
||||
// No satellites selected — show indicator briefly, then stop
|
||||
delay(1000)
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
// For non-empty selections, the passes collector below will clear isRefreshing
|
||||
}
|
||||
}
|
||||
// Stop refreshing whenever new passes arrive (from initial load, selection change, or filter)
|
||||
viewModelScope.launch {
|
||||
satelliteRepo.passes.drop(1).collect { _ ->
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
satelliteRepo.isCalculating.collect { calculating ->
|
||||
_uiState.update { it.copy(isRefreshing = calculating) }
|
||||
}
|
||||
}
|
||||
// Local tick loop — computes pass progress and countdown timer every second
|
||||
viewModelScope.launch {
|
||||
while (isActive) {
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val processed = computePassProgress(satelliteRepo.passes.value, timeNow)
|
||||
val showDeepSpace = _uiState.value.showDeepSpace
|
||||
val allPasses = satelliteRepo.passes.value
|
||||
val filtered = if (showDeepSpace) allPasses else allPasses.filter { !it.isDeepSpace }
|
||||
val processed = computePassProgress(filtered, timeNow)
|
||||
val (nextPass, nextTime, isAos) = resolveNextPass(processed, timeNow)
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -108,11 +99,17 @@ class PassesViewModel(
|
||||
fun onAction(action: PassesAction) {
|
||||
when (action) {
|
||||
PassesAction.DismissWhatsNew -> settingsRepo.setWhatsNewDismissed()
|
||||
is PassesAction.FilterPasses -> applyFilter(action.hoursAhead, action.minElevation, _uiState.value.modes)
|
||||
is PassesAction.FilterRadios -> applyFilter(_uiState.value.hours, _uiState.value.elevation, action.modes)
|
||||
is PassesAction.FilterPasses ->
|
||||
applyFilter(action.hoursAhead, action.minElevation, action.showDeepSpace, _uiState.value.modes)
|
||||
|
||||
is PassesAction.FilterRadios ->
|
||||
applyFilter(_uiState.value.hours, _uiState.value.elevation, _uiState.value.showDeepSpace, action.modes)
|
||||
PassesAction.RefreshPasses -> refreshPasses()
|
||||
PassesAction.TogglePassesDialog -> _uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) }
|
||||
PassesAction.ToggleRadiosDialog -> _uiState.update { it.copy(isRadiosDialogShown = !it.isRadiosDialogShown) }
|
||||
PassesAction.TogglePassesDialog ->
|
||||
_uiState.update { it.copy(isPassesDialogShown = !it.isPassesDialogShown) }
|
||||
|
||||
PassesAction.ToggleRadiosDialog ->
|
||||
_uiState.update { it.copy(isRadiosDialogShown = !it.isRadiosDialogShown) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,22 +150,24 @@ class PassesViewModel(
|
||||
return Triple(defaultPass, "00:00:00", true)
|
||||
}
|
||||
|
||||
private fun applyFilter(hoursAhead: Int, minElevation: Double, modes: List<String>) = viewModelScope.launch {
|
||||
_uiState.update { it.copy(isRefreshing = true) }
|
||||
settingsRepo.setPassesSettings(PassesSettings(hoursAhead, minElevation, modes))
|
||||
_uiState.update { it.copy(hours = hoursAhead, elevation = minElevation, modes = modes) }
|
||||
private fun applyFilter(
|
||||
hoursAhead: Int,
|
||||
minElevation: Double,
|
||||
showDeepSpace: Boolean,
|
||||
modes: List<String>
|
||||
) = viewModelScope.launch {
|
||||
settingsRepo.setPassesSettings(PassesSettings(showDeepSpace, hoursAhead, minElevation, modes))
|
||||
_uiState.update {
|
||||
it.copy(hours = hoursAhead, elevation = minElevation, showDeepSpace = showDeepSpace, modes = modes)
|
||||
}
|
||||
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
|
||||
private fun refreshPasses() = viewModelScope.launch {
|
||||
_uiState.update { it.copy(isRefreshing = true) }
|
||||
val (hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||
val (_, hoursAhead, minElevation, modes) = settingsRepo.passesSettings.value
|
||||
satelliteRepo.calculatePasses(System.currentTimeMillis(), hoursAhead, minElevation, modes)
|
||||
_uiState.update { it.copy(isRefreshing = false) }
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -58,9 +59,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
import com.rtbishop.look4sat.core.domain.utility.toDegrees
|
||||
@@ -69,7 +67,6 @@ import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import com.rtbishop.look4sat.core.presentation.NextPassRow
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.TimerRow
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
||||
@@ -77,20 +74,20 @@ import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||
|
||||
fun NavGraphBuilder.radarDestination(
|
||||
@Composable
|
||||
fun RadarDestination(
|
||||
catNum: Int = 0,
|
||||
aosTime: Long = 0L,
|
||||
navigateUp: () -> Unit,
|
||||
navigateToRadioControl: (Int, Long) -> Unit = { _, _ -> }
|
||||
) {
|
||||
val radarRoute = "${Screen.Radar.route}?catNum={catNum}&aosTime={aosTime}"
|
||||
val radarArgs = listOf(
|
||||
navArgument("catNum") { defaultValue = 0 },
|
||||
navArgument("aosTime") { defaultValue = 0L }
|
||||
val viewModel = viewModel(
|
||||
modelClass = RadarViewModel::class.java,
|
||||
key = "$catNum-$aosTime",
|
||||
factory = RadarViewModel.factory(catNum, aosTime)
|
||||
)
|
||||
composable(radarRoute, radarArgs) {
|
||||
val viewModel = viewModel(RadarViewModel::class.java, factory = RadarViewModel.Factory)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
RadarScreen(uiState, viewModel::onAction, navigateUp, navigateToRadioControl)
|
||||
}
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
RadarScreen(uiState, viewModel::onAction, navigateUp, navigateToRadioControl)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -101,7 +98,7 @@ private fun RadarScreen(
|
||||
navigateToRadioControl: (Int, Long) -> Unit
|
||||
) {
|
||||
val upcomingPass = uiState.currentPass ?: getDefaultPass()
|
||||
if (upcomingPass.losTime < System.currentTimeMillis()) navigateUp()
|
||||
LaunchedEffect(uiState.isLos) { if (uiState.isLos) navigateUp() }
|
||||
|
||||
val addToCalendar: () -> Unit = {
|
||||
uiState.currentPass?.let { pass ->
|
||||
@@ -123,14 +120,14 @@ private fun RadarScreen(
|
||||
if (isVertical) {
|
||||
TopBar {
|
||||
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos)
|
||||
IconCard(action = openRadioControl, resId = R.drawable.ic_radios)
|
||||
}
|
||||
TopBar { NextPassRow(pass = upcomingPass, isUtc = uiState.isUtc) }
|
||||
} else {
|
||||
TopBar {
|
||||
IconCard(action = addToCalendar, resId = R.drawable.ic_calendar)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isCurrentTimeAos)
|
||||
TimerRow(timeString = uiState.currentTime, isTimeAos = uiState.isTimeAos)
|
||||
NextPassRow(pass = upcomingPass, modifier = Modifier.weight(1f), isUtc = uiState.isUtc)
|
||||
IconCard(action = openRadioControl, resId = R.drawable.ic_radios)
|
||||
}
|
||||
@@ -149,8 +146,8 @@ private fun RadarScreen(
|
||||
|
||||
@Composable
|
||||
private fun RadarCard(uiState: RadarState, modifier: Modifier = Modifier) {
|
||||
val isEclipsed = uiState.orbitalPos?.eclipsed == true
|
||||
val borderModifier = if (isEclipsed) {
|
||||
val satellitePos = uiState.orbitalPos
|
||||
val borderModifier = if (satellitePos?.aboveHorizon == true && satellitePos.eclipsed) {
|
||||
val infiniteTransition = rememberInfiniteTransition(label = "eclipsedBorder")
|
||||
val borderAlpha by infiniteTransition.animateFloat(
|
||||
initialValue = 1.0f,
|
||||
|
||||
@@ -24,7 +24,8 @@ import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
|
||||
data class RadarState(
|
||||
val currentPass: OrbitalPass? = null,
|
||||
val currentTime: String = "00:00:00",
|
||||
val isCurrentTimeAos: Boolean = true,
|
||||
val isTimeAos: Boolean = true,
|
||||
val isLos: Boolean = false,
|
||||
val isUtc: Boolean = false,
|
||||
val orientationValues: Pair<Float, Float> = 0f to 0f,
|
||||
val orbitalPos: OrbitalPos? = null,
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
@@ -80,21 +78,11 @@ fun RadarViewCompose(
|
||||
animationSpec = infiniteRepeatable(tween(1000)),
|
||||
label = "animScale"
|
||||
)
|
||||
val eclipsedGlowAlpha by animTransition.animateFloat(
|
||||
initialValue = 1.0f,
|
||||
targetValue = 0.0f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 1000, delayMillis = 25, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse
|
||||
),
|
||||
label = "eclipsedGlowAlpha"
|
||||
)
|
||||
val measurer = rememberTextMeasurer()
|
||||
var sweepDegrees by remember { mutableFloatStateOf(0f) }
|
||||
var cachedRadius by remember { mutableFloatStateOf(0f) }
|
||||
var trackPath by remember { mutableStateOf(Path()) }
|
||||
var trackEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
|
||||
val isEclipsed = item.eclipsed
|
||||
|
||||
Canvas(modifier = modifier.aspectRatio(1f)) {
|
||||
val radius = size.minDimension / 2f * 0.95f
|
||||
@@ -110,11 +98,7 @@ fun RadarViewCompose(
|
||||
translate(center.x, center.y) {
|
||||
drawTrack(trackPath, trackEffect, aimColor, trackColor)
|
||||
if (item.elevation > 0) {
|
||||
if (isEclipsed) {
|
||||
drawEclipsedPosition(item, radius, eclipsedGlowAlpha, trackColor)
|
||||
} else {
|
||||
drawPosition(item, radius, animScale, trackColor)
|
||||
}
|
||||
drawPosition(item, radius, animScale, trackColor)
|
||||
}
|
||||
if (shouldUseCompass) drawAim(azimElev.first, azimElev.second, radius, aimColor)
|
||||
}
|
||||
@@ -150,12 +134,7 @@ private fun DrawScope.drawTrack(path: Path, effect: PathEffect, color: Color, ef
|
||||
private fun DrawScope.drawPosition(item: OrbitalPos, radius: Float, posRadius: Float, color: Color) {
|
||||
val pos = sph2Cart(item.azimuth, item.elevation, radius.toDouble())
|
||||
drawCircle(color, 16f, pos)
|
||||
drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, pos)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawEclipsedPosition(item: OrbitalPos, radius: Float, glowAlpha: Float, color: Color) {
|
||||
val pos = sph2Cart(item.azimuth, item.elevation, radius.toDouble())
|
||||
drawCircle(color.copy(alpha = glowAlpha), 16f, pos)
|
||||
if (!item.eclipsed) drawCircle(color.copy(alpha = 1 - (posRadius / 64f)), posRadius, pos)
|
||||
}
|
||||
|
||||
private fun DrawScope.drawAim(azim: Float, elev: Float, radius: Float, color: Color) {
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radar
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
@@ -45,7 +43,8 @@ import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadarViewModel(
|
||||
private val savedStateHandle: SavedStateHandle,
|
||||
private val catNum: Int,
|
||||
private val aosTime: Long,
|
||||
private val bluetoothReporter: IReporter,
|
||||
private val networkReporter: IReporter,
|
||||
private val satelliteRepo: ISatelliteRepo,
|
||||
@@ -85,8 +84,6 @@ class RadarViewModel(
|
||||
}
|
||||
// Resolve which pass we're tracking and start the tick loop
|
||||
viewModelScope.launch {
|
||||
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
|
||||
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
|
||||
val passes = satelliteRepo.passes.value
|
||||
val currentPass = passes.find { it.catNum == catNum && it.aosTime == aosTime }
|
||||
?: passes.firstOrNull()
|
||||
@@ -105,7 +102,8 @@ class RadarViewModel(
|
||||
val timeNow = System.currentTimeMillis()
|
||||
val pos = satelliteRepo.getPosition(satPass.orbitalObject, stationPos, timeNow)
|
||||
val (time, isAos) = computeTimer(satPass.isDeepSpace, satPass.aosTime, satPass.losTime, timeNow)
|
||||
_uiState.update { it.copy(currentTime = time, isCurrentTimeAos = isAos, orbitalPos = pos) }
|
||||
val isLos = !satPass.isDeepSpace && timeNow > satPass.losTime
|
||||
_uiState.update { it.copy(currentTime = time, isTimeAos = isAos, isLos = isLos, orbitalPos = pos) }
|
||||
processRadios(transmitters, satPass.orbitalObject, timeNow)
|
||||
sendPassData(pos)
|
||||
delay(1000)
|
||||
@@ -204,18 +202,19 @@ class RadarViewModel(
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
fun factory(catNum: Int, aosTime: Long): ViewModelProvider.Factory = viewModelFactory {
|
||||
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
|
||||
initializer {
|
||||
val container = (this[applicationKey] as IContainerProvider).getMainContainer()
|
||||
RadarViewModel(
|
||||
createSavedStateHandle(),
|
||||
container.provideBluetoothReporter(),
|
||||
container.provideNetworkReporter(),
|
||||
container.satelliteRepo,
|
||||
container.settingsRepo,
|
||||
container.provideSensorsRepo(),
|
||||
container.provideAddToCalendar()
|
||||
catNum = catNum,
|
||||
aosTime = aosTime,
|
||||
bluetoothReporter = container.provideBluetoothReporter(),
|
||||
networkReporter = container.provideNetworkReporter(),
|
||||
satelliteRepo = container.satelliteRepo,
|
||||
settingsRepo = container.settingsRepo,
|
||||
sensorsRepo = container.provideSensorsRepo(),
|
||||
addToCalendar = container.provideAddToCalendar()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-14
@@ -51,15 +51,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navArgument
|
||||
import com.rtbishop.look4sat.core.domain.model.SatRadio
|
||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||
import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.NextPassRow
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.TimerRow
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.getDefaultPass
|
||||
@@ -67,17 +63,15 @@ import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||
import java.util.Locale
|
||||
|
||||
fun NavGraphBuilder.radioControlDestination(navigateUp: () -> Unit) {
|
||||
val route = "${Screen.RadioControl.route}?catNum={catNum}&aosTime={aosTime}"
|
||||
val args = listOf(
|
||||
navArgument("catNum") { defaultValue = 0 },
|
||||
navArgument("aosTime") { defaultValue = 0L }
|
||||
@Composable
|
||||
fun RadioControlDestination(catNum: Int = 0, aosTime: Long = 0L, navigateUp: () -> Unit) {
|
||||
val viewModel = viewModel(
|
||||
modelClass = RadioControlViewModel::class.java,
|
||||
key = "$catNum-$aosTime",
|
||||
factory = RadioControlViewModel.factory(catNum, aosTime)
|
||||
)
|
||||
composable(route, args) {
|
||||
val viewModel = viewModel(RadioControlViewModel::class.java, factory = RadioControlViewModel.Factory)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
RadioControlScreen(uiState, viewModel::onAction, navigateUp)
|
||||
}
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
RadioControlScreen(uiState, viewModel::onAction, navigateUp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+10
-13
@@ -17,10 +17,8 @@
|
||||
*/
|
||||
package com.rtbishop.look4sat.feature.radiocontrol
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.createSavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
@@ -41,15 +39,16 @@ import kotlinx.coroutines.launch
|
||||
import java.util.Locale
|
||||
|
||||
class RadioControlViewModel(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val catNum: Int,
|
||||
private val aosTime: Long,
|
||||
private val trackingService: IRadioTrackingService,
|
||||
private val satelliteRepo: ISatelliteRepo,
|
||||
private val settingsRepo: ISettingsRepo
|
||||
settingsRepo: ISettingsRepo
|
||||
) : ViewModel() {
|
||||
|
||||
private val stationPos = settingsRepo.stationPosition.value
|
||||
private var currentPass: OrbitalPass? = null
|
||||
private var transponders: List<SatRadio> = emptyList()
|
||||
private val stationPos = settingsRepo.stationPosition.value
|
||||
|
||||
private val _uiState = MutableStateFlow(
|
||||
RadioControlState(
|
||||
@@ -72,9 +71,6 @@ class RadioControlViewModel(
|
||||
val uiState: StateFlow<RadioControlState> = _uiState
|
||||
|
||||
init {
|
||||
val catNum = savedStateHandle.get<Int>("catNum") ?: 0
|
||||
val aosTime = savedStateHandle.get<Long>("aosTime") ?: 0L
|
||||
|
||||
// Resolve pass and load transponders
|
||||
viewModelScope.launch {
|
||||
val passes = satelliteRepo.passes.value
|
||||
@@ -190,15 +186,16 @@ class RadioControlViewModel(
|
||||
return String.format(Locale.ENGLISH, "%d.%03d.%03d", mhz, khz, hz)
|
||||
}
|
||||
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
fun factory(catNum: Int, aosTime: Long): ViewModelProvider.Factory = viewModelFactory {
|
||||
val applicationKey = ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY
|
||||
initializer {
|
||||
val container = (this[applicationKey] as IContainerProvider).getMainContainer()
|
||||
RadioControlViewModel(
|
||||
createSavedStateHandle(),
|
||||
container.radioTrackingService,
|
||||
container.satelliteRepo,
|
||||
container.settingsRepo
|
||||
catNum = catNum,
|
||||
aosTime = aosTime,
|
||||
trackingService = container.radioTrackingService,
|
||||
satelliteRepo = container.satelliteRepo,
|
||||
settingsRepo = container.settingsRepo
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+8
-11
@@ -56,8 +56,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.rtbishop.look4sat.core.domain.model.SatItem
|
||||
import com.rtbishop.look4sat.core.presentation.CardLoadingIndicator
|
||||
import com.rtbishop.look4sat.core.presentation.EmptyListCard
|
||||
@@ -66,20 +64,19 @@ import com.rtbishop.look4sat.core.presentation.InfoDialog
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import com.rtbishop.look4sat.core.presentation.PrimaryIconCard
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import com.rtbishop.look4sat.core.presentation.layoutPadding
|
||||
|
||||
fun NavGraphBuilder.satellitesDestination(navigateUp: () -> Unit) {
|
||||
composable(Screen.Satellites.route) {
|
||||
val viewModel = viewModel(
|
||||
modelClass = SatellitesViewModel::class.java, factory = SatellitesViewModel.Factory
|
||||
)
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
SatellitesScreen(uiState, viewModel::onAction, navigateUp)
|
||||
}
|
||||
@Composable
|
||||
fun SatellitesDestination(navigateUp: () -> Unit) {
|
||||
val viewModel = viewModel(
|
||||
modelClass = SatellitesViewModel::class.java,
|
||||
factory = SatellitesViewModel.Factory
|
||||
)
|
||||
val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
|
||||
SatellitesScreen(uiState, viewModel::onAction, navigateUp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+8
-12
@@ -60,8 +60,6 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import com.rtbishop.look4sat.core.domain.model.OtherSettings
|
||||
import com.rtbishop.look4sat.core.domain.predict.GeoPos
|
||||
import com.rtbishop.look4sat.core.presentation.CardButton
|
||||
@@ -69,7 +67,6 @@ import com.rtbishop.look4sat.core.presentation.IconCard
|
||||
import com.rtbishop.look4sat.core.presentation.MainTheme
|
||||
import com.rtbishop.look4sat.core.presentation.PrimaryIconCard
|
||||
import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.Screen
|
||||
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||
@@ -78,15 +75,14 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
fun NavGraphBuilder.settingsDestination() {
|
||||
composable(Screen.Settings.route) {
|
||||
val viewModel = viewModel(
|
||||
modelClass = SettingsViewModel::class.java,
|
||||
factory = SettingsViewModel.Factory
|
||||
)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
SettingsScreen(uiState, viewModel::onAction)
|
||||
}
|
||||
@Composable
|
||||
fun SettingsDestination() {
|
||||
val viewModel = viewModel(
|
||||
modelClass = SettingsViewModel::class.java,
|
||||
factory = SettingsViewModel.Factory
|
||||
)
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
SettingsScreen(uiState, viewModel::onAction)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
+25
-26
@@ -1,9 +1,9 @@
|
||||
[versions]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionCode = "420"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.2.0"
|
||||
appVersionCode = "422"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
appVersionName = "4.2.2"
|
||||
#noinspection GradleDependency,UnusedVersionCatalogEntry
|
||||
compileSdk = "36"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
minSdk = "24"
|
||||
@@ -12,21 +12,23 @@ jdkVersion = "17"
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
packageName = "com.rtbishop.look4sat"
|
||||
|
||||
android-gradle-plugin = "9.1.0"
|
||||
google-ksp = "2.3.6"
|
||||
kotlin = "2.3.20"
|
||||
android-gradle-plugin = "9.2.0"
|
||||
|
||||
androidx-core-ktx = "1.18.0"
|
||||
androidx-core-splashscreen = "1.2.0"
|
||||
androidx-room = "2.8.4"
|
||||
|
||||
compose-bom = "2026.03.01"
|
||||
compose-bom = "2026.04.01"
|
||||
compose-activity = "1.13.0"
|
||||
compose-lifecycle = "2.10.0"
|
||||
compose-navigation = "2.9.7"
|
||||
compose-navigation3 = "1.1.1"
|
||||
|
||||
google-ksp = "2.3.6"
|
||||
|
||||
kotlin = "2.3.20"
|
||||
kotlin-coroutines = "1.10.2"
|
||||
kotlin-serialization = "1.11.0"
|
||||
|
||||
other-coroutines = "1.10.2"
|
||||
other-json = "20251224"
|
||||
other-okhttp = "5.3.2"
|
||||
other-osmdroid = "6.1.20"
|
||||
|
||||
@@ -50,32 +52,32 @@ androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "
|
||||
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
compose-animation = { group = "androidx.compose.animation", name = "animation" }
|
||||
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
compose-material3-navigation = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compose-material3-adaptive = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite" }
|
||||
compose-runtime = { group = "androidx.compose.runtime", name = "runtime" }
|
||||
compose-tooling = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
|
||||
|
||||
compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-activity" }
|
||||
compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "compose-lifecycle" }
|
||||
compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" }
|
||||
compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "compose-lifecycle" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
compose-navigation3 = { module = "androidx.navigation3:navigation3-ui", version.ref = "compose-navigation3" }
|
||||
compose-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "compose-lifecycle" }
|
||||
|
||||
compose-debug-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
compose-debug-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
|
||||
|
||||
kotlin-gradlePlugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
|
||||
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlin-serialization" }
|
||||
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
other-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "other-coroutines" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
other-json = { module = "org.json:json", version.ref = "other-json" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
other-okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "other-okhttp" }
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
other-osmdroid = { module = "org.osmdroid:osmdroid-android", version.ref = "other-osmdroid" }
|
||||
|
||||
test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "other-coroutines" }
|
||||
test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlin-coroutines" }
|
||||
test-junit4 = { module = "junit:junit", version.ref = "test-junit4" }
|
||||
|
||||
androidTest-junit = { module = "androidx.test.ext:junit", version.ref = "androidTest-junit" }
|
||||
@@ -87,6 +89,7 @@ android-library = { id = "com.android.library", version.ref = "android-gradle-pl
|
||||
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
google-ksp = { id = "com.google.devtools.ksp", version.ref = "google-ksp" }
|
||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||
# plugins defined by this project
|
||||
convention-applicationPlugin = { id = "com.rtbishop.look4sat.convention.applicationPlugin" }
|
||||
convention-coreDataPlugin = { id = "com.rtbishop.look4sat.convention.coreDataPlugin" }
|
||||
@@ -96,11 +99,7 @@ convention-featurePlugin = { id = "com.rtbishop.look4sat.convention.featurePlugi
|
||||
|
||||
[bundles]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
composeAll = [
|
||||
"compose-animation", "compose-runtime", "compose-tooling",
|
||||
"compose-activity", "compose-lifecycle", "compose-material3",
|
||||
"compose-material3-navigation", "compose-navigation", "compose-viewmodel"
|
||||
]
|
||||
composeAll = ["compose-runtime", "compose-tooling", "compose-activity", "compose-material3", "compose-viewmodel"]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
composeDebug = ["compose-debug-manifest", "compose-debug-tooling"]
|
||||
#noinspection UnusedVersionCatalogEntry
|
||||
|
||||
Reference in new issue
Block a user