Optimized common composables and satellite selection

This commit is contained in:
Arty Bishop committed 2026-04-02 08:28:37 +01:00
1 parent 706c912b13
commit 45eb6915c2
2 files changed
+136 -150

No files matched your search

@@ -40,11 +40,23 @@ class SelectionRepo(
private val currentItems = MutableStateFlow<List<SatItem>>(emptyList()) private val currentItems = MutableStateFlow<List<SatItem>>(emptyList())
private val currentTypes = MutableStateFlow(settingsRepo.selectedTypes.value) private val currentTypes = MutableStateFlow(settingsRepo.selectedTypes.value)
private val currentQuery = MutableStateFlow("") private val currentQuery = MutableStateFlow("")
// Resolve type IDs once when types change, then filter items reactively.
// The HashSet gives O(1) catnum lookups instead of O(n) with a List.
private val itemsWithTypes = currentTypes.flatMapLatest { types: List<String> -> private val itemsWithTypes = currentTypes.flatMapLatest { types: List<String> ->
currentItems.map { items -> items.filterByTypes(types) } val catnumSet: Set<Int>? = if (types.isEmpty()) {
null // null = no filtering
} else {
val ids = settingsRepo.getSatelliteTypesIds(types)
if (ids.isEmpty()) null else ids.toHashSet()
}
currentItems.map { items ->
if (catnumSet == null) items else items.filter { it.catnum in catnumSet }
}
} }
private val itemsWithQuery = currentQuery.flatMapLatest { query -> private val itemsWithQuery = currentQuery.flatMapLatest { query ->
itemsWithTypes.map { items -> items.filterByQuery(query) } itemsWithTypes.map { items -> filterByQuery(items, query) }
} }
override fun getCurrentTypes() = currentTypes.value override fun getCurrentTypes() = currentTypes.value
@@ -54,7 +66,7 @@ class SelectionRepo(
} }
override suspend fun getEntriesFlow() = withContext(dispatcher) { override suspend fun getEntriesFlow() = withContext(dispatcher) {
val selectedIds = settingsRepo.selectedIds.value val selectedIds = settingsRepo.selectedIds.value.toHashSet()
currentItems.value = localSource.getEntriesList().map { item -> currentItems.value = localSource.getEntriesList().map { item ->
item.copy(isSelected = item.catnum in selectedIds) item.copy(isSelected = item.catnum in selectedIds)
} }
@@ -66,17 +78,19 @@ class SelectionRepo(
settingsRepo.setSelectedTypes(types) settingsRepo.setSelectedTypes(types)
} }
override suspend fun setQuery(query: String) = withContext(dispatcher) { override suspend fun setQuery(query: String) {
currentQuery.value = query currentQuery.value = query
} }
override suspend fun setSelection(selectAll: Boolean) = withContext(dispatcher) { override suspend fun setSelection(selectAll: Boolean) = withContext(dispatcher) {
setSelection(itemsWithQuery.first().map { item -> item.catnum }, selectAll) val visibleIds = itemsWithQuery.first().mapTo(HashSet()) { it.catnum }
setSelection(visibleIds, selectAll)
} }
override suspend fun setSelection(ids: List<Int>, isTicked: Boolean) = withContext(dispatcher) { override suspend fun setSelection(ids: List<Int>, isTicked: Boolean) = withContext(dispatcher) {
val idSet = ids.toHashSet()
currentItems.value = currentItems.value.map { item -> currentItems.value = currentItems.value.map { item ->
if (item.catnum in ids) item.copy(isSelected = isTicked) else item if (item.catnum in idSet) item.copy(isSelected = isTicked) else item
} }
} }
@@ -85,19 +99,24 @@ class SelectionRepo(
settingsRepo.setSelectedIds(currentSelection) settingsRepo.setSelectedIds(currentSelection)
} }
private suspend fun List<SatItem>.filterByTypes(types: List<String>) = withContext(dispatcher) { /**
if (types.isEmpty()) return@withContext this@filterByTypes * Bulk selection using a pre-built Set for O(1) lookups.
val catnums = settingsRepo.getSatelliteTypesIds(types) */
if (catnums.isEmpty()) return@withContext this@filterByTypes private suspend fun setSelection(idSet: Set<Int>, isTicked: Boolean) = withContext(dispatcher) {
return@withContext this@filterByTypes.filter { item -> item.catnum in catnums } currentItems.value = currentItems.value.map { item ->
} if (item.catnum in idSet) item.copy(isSelected = isTicked) else item
private suspend fun List<SatItem>.filterByQuery(query: String) = withContext(dispatcher) {
if (query.isBlank()) return@withContext this@filterByQuery
return@withContext try {
this@filterByQuery.filter { it.catnum == query.toInt() }
} catch (_: Exception) {
this@filterByQuery.filter { item -> item.name.lowercase().contains(query.lowercase()) }
} }
} }
/**
* Filters items by query. Uses toIntOrNull() instead of exception-based flow,
* and lowercases the query once up front instead of per-item.
*/
private fun filterByQuery(items: List<SatItem>, query: String): List<SatItem> {
if (query.isBlank()) return items
val catnum = query.toIntOrNull()
if (catnum != null) return items.filter { it.catnum == catnum }
val lowerQuery = query.lowercase()
return items.filter { it.name.lowercase().contains(lowerQuery) }
}
} }
@@ -76,19 +76,23 @@ private fun TopBarPreview() = MainTheme {
} }
@Composable @Composable
fun TopBar(content: @Composable (RowScope.() -> Unit)) { fun TopBar(content: @Composable RowScope.() -> Unit) {
Row( Row(
modifier = Modifier.height(48.dp), modifier = Modifier.height(48.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically,
) { content() } content = content
)
} }
@Composable @Composable
fun RowScope.TimerRow(timeString: String, isTimeAos: Boolean) { fun RowScope.TimerRow(timeString: String, isTimeAos: Boolean) {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
val aosColor = if (isTimeAos) colorScheme.primary else colorScheme.onSurface val (aosColor, losColor) = if (isTimeAos) {
val losColor = if (!isTimeAos) colorScheme.primary else colorScheme.onSurface colorScheme.primary to colorScheme.onSurface
} else {
colorScheme.onSurface to colorScheme.primary
}
ElevatedCard(modifier = Modifier.weight(1f)) { ElevatedCard(modifier = Modifier.weight(1f)) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -100,7 +104,7 @@ fun RowScope.TimerRow(timeString: String, isTimeAos: Boolean) {
text = timeString, text = timeString,
fontSize = 32.sp, fontSize = 32.sp,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary color = colorScheme.primary
) )
Text(text = "LOS", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = losColor) Text(text = "LOS", fontSize = 16.sp, fontWeight = FontWeight.Medium, color = losColor)
} }
@@ -124,13 +128,11 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier) {
verticalArrangement = Arrangement.spacedBy((-2).dp), verticalArrangement = Arrangement.spacedBy((-2).dp),
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
// .background(color = MaterialTheme.colorScheme.background)
.padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp) .padding(start = 6.dp, top = 1.dp, end = 6.dp, bottom = 0.dp)
) { ) {
val passSatId = stringResource(id = R.string.pass_satId, pass.catNum)
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Text( Text(
text = "$passSatId - ", text = "${stringResource(R.string.pass_satId, pass.catNum)} - ",
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Text( Text(
@@ -144,7 +146,7 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier) {
overflow = TextOverflow.Ellipsis overflow = TextOverflow.Ellipsis
) )
Icon( Icon(
painter = painterResource(id = R.drawable.ic_elevation), painter = painterResource(R.drawable.ic_elevation),
contentDescription = null, contentDescription = null,
tint = MaterialTheme.colorScheme.primary, tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
@@ -160,47 +162,30 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier) {
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Row( Text(
modifier = Modifier.weight(1f), text = sdfTime.format(Date(pass.aosTime)),
horizontalArrangement = Arrangement.Start, fontSize = 15.sp,
verticalAlignment = Alignment.CenterVertically modifier = Modifier.weight(1f)
) { )
Text(
text = sdfTime.format(Date(pass.aosTime)),
fontSize = 15.sp,
modifier = Modifier.weight(1f)
)
}
Row( Row(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_altitude), painter = painterResource(R.drawable.ic_altitude),
contentDescription = null, contentDescription = null,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
Text( Text(text = "${pass.altitude} km", fontSize = 15.sp)
text = "${pass.altitude} km",
fontSize = 15.sp
)
}
Row(
modifier = Modifier.weight(1f),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(
id = R.string.pass_aosLos,
pass.aosAzimuth.toInt(),
pass.losAzimuth.toInt()
),
fontSize = 15.sp
)
} }
Text(
text = stringResource(R.string.pass_aosLos, pass.aosAzimuth.toInt(), pass.losAzimuth.toInt()),
fontSize = 15.sp,
textAlign = TextAlign.End,
modifier = Modifier.weight(1f)
)
} }
} }
} }
@@ -209,34 +194,41 @@ fun RowScope.NextPassRow(pass: OrbitalPass, modifier: Modifier = Modifier) {
@Composable @Composable
fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) { fun CardButton(onClick: () -> Unit, text: String, modifier: Modifier = Modifier) {
ElevatedButton( ElevatedButton(
onClick = { onClick() }, colors = ButtonDefaults.buttonColors( onClick = onClick,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant, containerColor = MaterialTheme.colorScheme.surfaceVariant,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant contentColor = MaterialTheme.colorScheme.onSurfaceVariant
), shape = MaterialTheme.shapes.small, modifier = modifier, ),
shape = MaterialTheme.shapes.small,
modifier = modifier,
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp) contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp)
) { Text(text = text, fontSize = 16.sp, textAlign = TextAlign.Center) } ) { Text(text = text, fontSize = 16.sp, textAlign = TextAlign.Center) }
} }
@Composable @Composable
fun IconCard(action: () -> Unit, resId: Int, modifier: Modifier = Modifier) { fun IconCard(action: () -> Unit, resId: Int, modifier: Modifier = Modifier) {
val clickableMod = Modifier.clickable { action() }
ElevatedCard(modifier = Modifier.size(48.dp)) { ElevatedCard(modifier = Modifier.size(48.dp)) {
Box(modifier = clickableMod.fillMaxSize(), contentAlignment = Alignment.Center) { Box(
Icon(painter = painterResource(resId), contentDescription = null, modifier = modifier) modifier = Modifier
} .clickable(onClick = action)
.fillMaxSize(),
contentAlignment = Alignment.Center
) { Icon(painter = painterResource(resId), contentDescription = null, modifier = modifier) }
} }
} }
@Composable @Composable
fun PrimaryIconCard(modifier: Modifier = Modifier, resId: Int, onClick: () -> Unit) { fun PrimaryIconCard(modifier: Modifier = Modifier, resId: Int, onClick: () -> Unit) {
val clickableMod = modifier.clickable { onClick() }
ElevatedCard( ElevatedCard(
modifier = Modifier.size(102.dp, 48.dp), modifier = Modifier.size(102.dp, 48.dp),
colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.primary) colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.primary)
) { ) {
Box(modifier = clickableMod.fillMaxSize(), contentAlignment = Alignment.Center) { Box(
Icon(painter = painterResource(id = resId), contentDescription = null) modifier = modifier
} .clickable(onClick = onClick)
.fillMaxSize(),
contentAlignment = Alignment.Center
) { Icon(painter = painterResource(resId), contentDescription = null) }
} }
} }
@@ -262,56 +254,56 @@ fun EmptyListCard(message: String) {
} }
} }
fun getDefaultPass(): OrbitalPass { private val defaultOrbitalData = OrbitalData(
val orbitalData = OrbitalData( name = """ ¯\_(ツ)_/¯ ⚠️""",
""" ¯\_(ツ)_/¯ ⚠️""", epoch = 0.0, meanmo = 0.0, eccn = 0.0, incl = 0.0,
0.0, raan = 0.0, argper = 0.0, meanan = 0.0, catnum = 0, bstar = 0.0
0.0, )
0.0,
0.0, fun getDefaultPass(): OrbitalPass = OrbitalPass(
0.0, aosTime = 0L, aosAzimuth = 0.0, losTime = Long.MAX_VALUE, losAzimuth = 0.0,
0.0, altitude = 0, maxElevation = 0.0, orbitalObject = NearEarthObject(defaultOrbitalData), progress = 0f
0.0, )
0,
0.0
)
val satellite = NearEarthObject(orbitalData)
return OrbitalPass(0L, 0.0, Long.MAX_VALUE, 0.0, 0, 0.0, satellite, 0f)
}
@Composable @Composable
fun SharedDialog( fun SharedDialog(
title: String, onCancel: () -> Unit, onAccept: () -> Unit, content: @Composable () -> Unit title: String, onCancel: () -> Unit, onAccept: () -> Unit, content: @Composable () -> Unit
) { ) {
val padding = LocalSpacing.current.large DialogShell(title = title, titleFontSize = 16, onDismissRequest = onCancel) {
Dialog(onDismissRequest = { onCancel() }) { content()
ElevatedCard { Row(modifier = Modifier.padding(start = it, bottom = it, end = it)) {
Column( CardButton(onClick = onCancel, text = stringResource(R.string.btn_cancel))
horizontalAlignment = Alignment.CenterHorizontally, Spacer(modifier = Modifier.weight(1f))
verticalArrangement = Arrangement.spacedBy(padding) CardButton(onClick = onAccept, text = stringResource(R.string.btn_accept))
) {
Text(
text = title,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = padding, top = padding, end = padding)
)
content()
Row(modifier = Modifier.padding(start = padding, bottom = padding, end = padding)) {
CardButton(onClick = onCancel, text = stringResource(id = R.string.btn_cancel))
Spacer(modifier = Modifier.weight(1f))
CardButton(onClick = onAccept, text = stringResource(id = R.string.btn_accept))
}
}
} }
} }
} }
@Composable @Composable
fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) { fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) {
DialogShell(title = title, titleFontSize = 18, onDismissRequest = {}) {
Text(
text = text,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = it)
)
Row(modifier = Modifier.padding(start = it, bottom = it, end = it)) {
Spacer(modifier = Modifier.weight(1f))
CardButton(onClick = onDismiss, text = stringResource(R.string.btn_accept))
}
}
}
@Composable
private fun DialogShell(
title: String,
titleFontSize: Int,
onDismissRequest: () -> Unit,
content: @Composable (padding: androidx.compose.ui.unit.Dp) -> Unit
) {
val padding = LocalSpacing.current.large val padding = LocalSpacing.current.large
Dialog(onDismissRequest = {}) { Dialog(onDismissRequest = onDismissRequest) {
ElevatedCard { ElevatedCard {
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
@@ -319,54 +311,36 @@ fun InfoDialog(title: String, text: String, onDismiss: () -> Unit) {
) { ) {
Text( Text(
text = title, text = title,
fontSize = 18.sp, fontSize = titleFontSize.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = padding, top = padding, end = padding) modifier = Modifier.padding(start = padding, top = padding, end = padding)
) )
Text( content(padding)
text = text,
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = padding)
)
Row(modifier = Modifier.padding(start = padding, bottom = padding, end = padding)) {
Spacer(modifier = Modifier.weight(1f))
CardButton(
onClick = onDismiss,
text = stringResource(id = R.string.btn_accept)
)
}
} }
} }
} }
} }
@Composable @Composable
fun hasEnoughHeight(): Boolean { fun hasEnoughHeight(): Boolean =
return currentWindowAdaptiveInfo().windowSizeClass.isHeightAtLeastBreakpoint(480) currentWindowAdaptiveInfo().windowSizeClass.isHeightAtLeastBreakpoint(480)
}
@Composable @Composable
fun hasEnoughWidth(): Boolean { fun hasEnoughWidth(): Boolean =
return currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(600) currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(600)
}
@Composable @Composable
fun isVerticalLayout(): Boolean { fun isVerticalLayout(): Boolean = !hasEnoughWidth()
return currentWindowAdaptiveInfo().windowSizeClass.isWidthAtLeastBreakpoint(600).not()
}
@Composable @Composable
fun Modifier.infiniteMarquee(): Modifier { fun Modifier.infiniteMarquee(): Modifier =
return basicMarquee(iterations = Int.MAX_VALUE, spacing = MarqueeSpacing(16.dp)) basicMarquee(iterations = Int.MAX_VALUE, spacing = MarqueeSpacing(16.dp))
}
@Composable @Composable
fun Modifier.layoutPadding(): Modifier { fun Modifier.layoutPadding(): Modifier {
val statusBarMod = this.statusBarsPadding()
val spacing = LocalSpacing.current.extraSmall val spacing = LocalSpacing.current.extraSmall
return statusBarMod.padding(start = spacing, top = 0.dp, end = spacing, bottom = spacing) return statusBarsPadding().padding(start = spacing, top = 0.dp, end = spacing, bottom = spacing)
} }
@Composable @Composable
@@ -375,15 +349,15 @@ fun ScreenColumn(
floatingBar: @Composable () -> Unit = {}, floatingBar: @Composable () -> Unit = {},
content: @Composable (Boolean) -> Unit = {} content: @Composable (Boolean) -> Unit = {}
) { ) {
val isVerticalLayout = isVerticalLayout() val isVertical = isVerticalLayout()
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
Box(modifier = Modifier.layoutPadding(), contentAlignment = Alignment.BottomCenter) { Box(modifier = Modifier.layoutPadding(), contentAlignment = Alignment.BottomCenter) {
Column(verticalArrangement = Arrangement.spacedBy(LocalSpacing.current.extraSmall)) { Column(verticalArrangement = Arrangement.spacedBy(LocalSpacing.current.extraSmall)) {
topBar(isVerticalLayout) topBar(isVertical)
Surface( Surface(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surfaceContainer color = MaterialTheme.colorScheme.surfaceContainer
) { content(isVerticalLayout) } ) { content(isVertical) }
} }
floatingBar() floatingBar()
} }
@@ -394,21 +368,14 @@ fun ScreenColumn(
fun TopBar( fun TopBar(
isVerticalLayout: Boolean, isVerticalLayout: Boolean,
startAction: @Composable () -> Unit, startAction: @Composable () -> Unit,
topInfo: @Composable (RowScope.() -> Unit), topInfo: @Composable RowScope.() -> Unit,
bottomInfo: @Composable (RowScope.() -> Unit), bottomInfo: @Composable RowScope.() -> Unit,
endAction: @Composable () -> Unit endAction: @Composable () -> Unit
) { ) {
val topBarRow = @Composable { content: @Composable RowScope.() -> Unit ->
Row(
modifier = Modifier.height(48.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) { content() }
}
if (isVerticalLayout) { if (isVerticalLayout) {
topBarRow { startAction().also { topInfo() }.also { endAction() } } TopBar { startAction(); topInfo(); endAction() }
topBarRow { bottomInfo() } TopBar { bottomInfo() }
} else { } else {
topBarRow { startAction().also { topInfo() }.also { bottomInfo() }.also { endAction() } } TopBar { startAction(); topInfo(); bottomInfo(); endAction() }
} }
} }