From a7b41c95c7f28717685200fac96e4ec81760d6fb Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Mon, 3 Aug 2026 21:38:25 +0800 Subject: [PATCH] fix: improve match header and daily sun times Match page UI: - Always show a meaningful status line in the top bar instead of leaving the second row blank on first entry. - Add a compact status chip for waiting, calculating, result, no-match, and error states. - Remove the duplicate intro card so the first screen starts directly with station inputs. Pass list sun times: - Compute sunrise/sunset from each visible date group's 00:00 in the selected timezone. - Avoid using an arbitrary pass AOS as the rise/set search start, which could jump later-day headers to the following day's events. --- .../look4sat/feature/mutual/MutualScreen.kt | 100 ++++++++++++------ .../feature/mutual/MutualViewModel.kt | 11 +- .../feature/passes/PassesViewModel.kt | 20 +++- 3 files changed, 93 insertions(+), 38 deletions(-) diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt index f359edf3..79ec7bb9 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualScreen.kt @@ -44,6 +44,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Slider +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -57,6 +58,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.rtbishop.look4sat.core.domain.repository.MutualPassData @@ -94,22 +96,29 @@ fun MutualScreen( ) }, bottomInfo = { - if (state.mutualPasses.isNotEmpty()) { - Text( - text = "找到 ${state.mutualPasses.size} 个匹配", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + Text( + text = mutualStatusText(state), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) }, endAction = { - if (state.isCalculating) { - CircularProgressIndicator( - modifier = Modifier - .height(24.dp) - .width(24.dp), - strokeWidth = 2.dp - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + if (state.isCalculating) { + CircularProgressIndicator( + modifier = Modifier + .height(24.dp) + .width(24.dp), + strokeWidth = 2.dp + ) + } + MutualStatusChip(state) } } ) @@ -187,8 +196,6 @@ private fun MutualContent( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp) ) { - MatchIntroCard() - if (isVertical) { StationInputCard( title = "你的位置", @@ -304,25 +311,50 @@ private fun MutualContent( } @Composable -private fun MatchIntroCard(modifier: Modifier = Modifier) { - ElevatedCard(modifier = modifier.fillMaxWidth()) { - Column( - modifier = Modifier.padding(12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = "位置匹配", - style = MaterialTheme.typography.titleSmall, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.primary - ) - Text( - text = "输入双方位置,查找共同可见的卫星过境窗口", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } +private fun MutualStatusChip(state: MutualUiState) { + val colorScheme = MaterialTheme.colorScheme + val containerColor = when { + state.isCalculating -> colorScheme.primaryContainer + state.errorMessage != null -> colorScheme.errorContainer + state.mutualPasses.isNotEmpty() -> colorScheme.primary + state.hasSearched -> colorScheme.surfaceVariant + else -> colorScheme.surfaceVariant } + val contentColor = when { + state.isCalculating -> colorScheme.onPrimaryContainer + state.errorMessage != null -> colorScheme.onErrorContainer + state.mutualPasses.isNotEmpty() -> colorScheme.onPrimary + else -> colorScheme.onSurfaceVariant + } + Surface( + color = containerColor, + contentColor = contentColor, + shape = MaterialTheme.shapes.small + ) { + Text( + text = mutualStatusChipText(state), + style = MaterialTheme.typography.labelSmall, + fontWeight = FontWeight.Medium, + maxLines = 1, + modifier = Modifier.padding(horizontal = 10.dp, vertical = 4.dp) + ) + } +} + +private fun mutualStatusText(state: MutualUiState): String = when { + state.isCalculating -> "正在计算双方共同可见窗口…" + state.errorMessage != null -> "请修正提示后重新查询" + state.mutualPasses.isNotEmpty() -> "找到 ${state.mutualPasses.size} 个匹配 · 点击卡片展开曲线" + state.hasSearched -> "未找到共同可见窗口,调整时间或仰角后重试" + else -> "输入双方位置后点击查询" +} + +private fun mutualStatusChipText(state: MutualUiState): String = when { + state.isCalculating -> "计算中" + state.errorMessage != null -> "有错误" + state.mutualPasses.isNotEmpty() -> "${state.mutualPasses.size}个匹配" + state.hasSearched -> "无匹配" + else -> "待查询" } @Composable diff --git a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt index 378dff5f..0f59b6ce 100644 --- a/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt +++ b/feature/mutual/src/main/java/com/rtbishop/look4sat/feature/mutual/MutualViewModel.kt @@ -49,6 +49,7 @@ data class MutualUiState( val hoursAhead: Int = 24, val mutualPasses: List = emptyList(), val isCalculating: Boolean = false, + val hasSearched: Boolean = false, val selectedPassIndex: Int = -1, val errorMessage: String? = null ) @@ -167,7 +168,15 @@ class MutualViewModel( return } - _uiState.update { it.copy(isCalculating = true, errorMessage = null, mutualPasses = emptyList()) } + _uiState.update { + it.copy( + isCalculating = true, + hasSearched = true, + errorMessage = null, + mutualPasses = emptyList(), + selectedPassIndex = -1 + ) + } viewModelScope.launch { val time = System.currentTimeMillis() diff --git a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt index fbd41078..5bad711e 100644 --- a/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt +++ b/feature/passes/src/main/java/com/rtbishop/look4sat/feature/passes/PassesViewModel.kt @@ -38,6 +38,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Date import java.util.Locale import java.util.TimeZone @@ -162,7 +163,10 @@ class PassesViewModel( return SimpleDateFormat(pattern, Locale.getDefault()).also { it.timeZone = tz } } - // Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace + // Computes sunrise/sunset strings for each unique calendar day in the pass list, plus today for DeepSpace. + // CelestialComputer.findSunRiseSet() returns the next rise/set after the supplied time, so pass + // the calendar day's 00:00 in the same timezone used by the visible date group. Passing a pass's + // AOS time can jump to the following day's sunrise/sunset when that AOS is after the local event. private fun computeSunTimes(passes: List, isUtc: Boolean): Map> { val stationPos = settingsRepo.stationPosition.value val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault() @@ -171,7 +175,7 @@ class PassesViewModel( val result = LinkedHashMap>() // DeepSpace group always shows today's sun times if (passes.any { it.isDeepSpace }) { - val riseSet = CelestialComputer.findSunRiseSet(stationPos, System.currentTimeMillis()) + val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(System.currentTimeMillis(), tz)) val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--" val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--" result["DeepSpace (period >225min)"] = rise to set @@ -180,7 +184,7 @@ class PassesViewModel( if (pass.isDeepSpace) continue val label = sdfDate.format(Date(pass.aosTime)) if (label in result) continue - val riseSet = CelestialComputer.findSunRiseSet(stationPos, pass.aosTime) + val riseSet = CelestialComputer.findSunRiseSet(stationPos, startOfDayMillis(pass.aosTime, tz)) val rise = if (riseSet.riseTimeMillis > 0) sdfTime.format(Date(riseSet.riseTimeMillis)) else "--:--" val set = if (riseSet.setTimeMillis > 0) sdfTime.format(Date(riseSet.setTimeMillis)) else "--:--" result[label] = rise to set @@ -188,6 +192,16 @@ class PassesViewModel( return result } + private fun startOfDayMillis(timeMillis: Long, tz: TimeZone): Long { + return Calendar.getInstance(tz, Locale.getDefault()).apply { + timeInMillis = timeMillis + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + }.timeInMillis + } + private fun groupPasses(passes: List, isUtc: Boolean): Map> { val tz = if (isUtc) TimeZone.getTimeZone("UTC") else TimeZone.getDefault() val sdfDate = dateFormat(tz)