Added AOS/LOS timer to each pass and a SearchBar filter

This commit is contained in:
Arty Bishop committed 2026-10-04 20:19:48 +01:00
1 parent 782b438a07
commit 83ce7229f6
9 files changed
+396 -249

No files matched your search

@@ -39,6 +39,7 @@ import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
@@ -46,21 +47,32 @@ import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfoV2
import androidx.compose.runtime.Composable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.addOutline
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -428,6 +440,55 @@ private fun DialogShell(
}
}
@Composable
fun SearchBar(onQueryChange: (String) -> Unit, modifier: Modifier = Modifier) {
val currentQuery = rememberSaveable { mutableStateOf("") }
val updateQuery = { newValue: String ->
currentQuery.value = newValue
onQueryChange(newValue)
}
ElevatedCard(modifier = modifier.height(48.dp)) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 12.dp)
) {
Icon(painter = painterResource(id = R.drawable.ic_search), contentDescription = null)
BasicTextField(
value = currentQuery.value,
onValueChange = updateQuery,
singleLine = true,
modifier = Modifier
.weight(1f)
.padding(start = 12.dp),
textStyle = TextStyle(
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
),
decorationBox = { innerTextField ->
if (currentQuery.value.isEmpty()) {
Text(
text = stringResource(id = R.string.sat_search_hint),
fontSize = 16.sp,
lineHeight = 20.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
innerTextField()
},
cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurface)
)
IconButton(onClick = { updateQuery("") }) {
val clearCd = stringResource(R.string.sat_search_clear)
Icon(painter = painterResource(id = R.drawable.ic_close), contentDescription = clearCd)
}
}
}
}
@Composable
fun hasEnoughHeight(): Boolean =
currentWindowAdaptiveInfoV2().windowSizeClass.isHeightAtLeastBreakpoint(480)
@@ -580,3 +641,30 @@ fun formatFrequency(frequencyHz: Long): String {
val hz = frequencyHz % 1_000
return String.format(Locale.ENGLISH, "%d.%03d.%03d", mhz, khz, hz)
}
/**
* Strokes the [shape] outline as a track, then reveals it left to right up to the [progress]
* fraction of the width, so the top and bottom edges fill in sync.
*/
@Composable
fun Modifier.progressOutline(
progress: Float,
shape: Shape = RoundedCornerShape(12.dp),
width: Dp = 0.5.dp,
trackColor: Color = ProgressIndicatorDefaults.linearTrackColor,
progressColor: Color = ProgressIndicatorDefaults.linearColor
) = this.drawWithCache {
val widthPx = width.toPx()
val outline = shape.createOutline(Size(size.width - widthPx, size.height - widthPx), layoutDirection, this)
val outlinePath = Path().apply {
addOutline(outline)
translate(Offset(widthPx / 2f, widthPx / 2f))
}
val stroke = Stroke(width = widthPx)
val revealWidth = size.width * progress.coerceIn(0f, 1f)
onDrawWithContent {
drawContent()
drawPath(outlinePath, trackColor, style = stroke)
clipRect(right = revealWidth) { drawPath(outlinePath, progressColor, style = stroke) }
}
}
@@ -39,6 +39,7 @@ fun SwipeableItem(onSwipeRight: () -> Unit, onSwipeLeft: () -> Unit, content: @C
val dismissThresholdPx = with(LocalDensity.current) { 120.dp.toPx() }
val dismissState = rememberSwipeToDismissBoxState { dismissThresholdPx }
val willTrigger by remember { derivedStateOf { dismissState.targetValue != SwipeToDismissBoxValue.Settled } }
val isSwiping by remember { derivedStateOf { dismissState.dismissDirection != SwipeToDismissBoxValue.Settled } }
val hapticFeedback = LocalHapticFeedback.current
LaunchedEffect(willTrigger) {
val feedbackType = if (willTrigger) HapticFeedbackType.LongPress else HapticFeedbackType.SegmentTick
@@ -49,8 +50,10 @@ fun SwipeableItem(onSwipeRight: () -> Unit, onSwipeLeft: () -> Unit, content: @C
enableDismissFromStartToEnd = true,
enableDismissFromEndToStart = true,
backgroundContent = {
val isSwipeRight = dismissState.dismissDirection == SwipeToDismissBoxValue.StartToEnd
SwipeBackground(isSwipeRight, willTrigger, MaterialTheme.colorScheme.primary)
if (isSwiping) {
val isSwipeRight = dismissState.dismissDirection == SwipeToDismissBoxValue.StartToEnd
SwipeBackground(isSwipeRight, willTrigger, MaterialTheme.colorScheme.primary)
}
},
content = { content() },
onDismiss = {
@@ -47,8 +47,13 @@
<string name="pass_deep_space" translatable="false">DeepSpace</string>
<string name="pass_altitude">Altitude: %d km</string>
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
<string name="pass_azimuth" translatable="false">%03d°</string>
<string name="pass_duration_placeholder" translatable="false">--m --s</string>
<string name="pass_timer_aos" translatable="false">AOS %s</string>
<string name="pass_timer_los" translatable="false">LOS %s</string>
<string name="pass_empty_list_message">
Make sure your filters are correct and you have selected satellites</string>
<string name="pass_empty_search_message">No upcoming passes match your search</string>
<string name="pass_error_message">There are no upcoming satellite passes to display</string>
<string name="pass_welcome_title">Welcome to Look4Sat\!</string>
<string name="pass_welcome_message">