ui: slim the More menu and drop dead controls from the CW page

用户反馈三处 UI 问题, 一并处理。

1. 更多菜单风格不符 + 遮盖感重
   - 去掉全屏 scrim 遮罩(0.35 alpha 压暗整页), 改为透明点击层, 点外部仍可关闭
   - Card 限宽 232dp 靠右下角, 从"全宽卡片"变成竖长条
   - 容器色 surfaceContainerHigh -> surfaceContainer, 与导航栏一致; 加 1dp 细边框
   - 动画从全屏 expandVertically + spring 弹跳改为右下角原点 150ms scaleIn
   - Card 加 clickable(enabled=false) 吞掉点击, 避免点卡片空白区误触关闭

2. CW 页左上角退出键点击无效 -> 删除
   根因: CwDecodeScreen(navigateUp: () -> Unit = {}) 是默认空实现, 而
   MainScreen 的 entry<Screen.CwDecode> 调用 CwDecodeScreen() 从未传入
   navigateUp, 所以点击必然无反应。按 AGENTS.md 无死代码原则删除按钮 +
   navigateUp 参数 + cw_back 字符串(五语)。

3. CW 页"正在监听"状态行在停止解码后仍显示 -> 删除
   estimatedPitch 在暂停后保留上次值, 状态行不会消失, 属误导。删除该 Text
   后 estimatedPitch / lastInferenceMs 两个 collector 成为死代码, 一并清理;
   cw_status_listening / cw_status_tone 字符串(五语)同步删除。
   signalStrength(瀑布图) 与 errorMessage(错误提示) 仍在用, 保留。

验证:
- :app:compileDebugKotlin + :feature:cw:compileDebugKotlin => BUILD SUCCESSFUL
- :core:domain:test => 31 个 CW 测试全绿 (7+13+3+8)
This commit is contained in:
mckero committed 2026-08-13 14:48:08 +00:00
1 parent f9fd7a2cfa
commit 5d89190348
8 files changed
+32 -55

No files matched your search

@@ -22,18 +22,14 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
@@ -67,6 +63,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -346,14 +343,18 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
}
}
}
// More-menu popup panel (overlays content above the bottom bar; spring bounce)
// More-menu popup panel (slim strip anchored to the bottom-right corner)
AnimatedVisibility(
visible = moreExpanded,
modifier = Modifier.fillMaxSize(),
enter = expandVertically(
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) + fadeIn(),
exit = shrinkVertically() + fadeOut()
enter = scaleIn(
animationSpec = tween(150),
transformOrigin = TransformOrigin(1f, 1f)
) + fadeIn(animationSpec = tween(150)),
exit = scaleOut(
animationSpec = tween(120),
transformOrigin = TransformOrigin(1f, 1f)
) + fadeOut(animationSpec = tween(120))
) {
MoreMenuPopup(
items = moreNavItems,
@@ -1,13 +1,13 @@
/*
* MoreMenuPopup.kt - bottom-nav "More" second-level menu popup panel (4.5.1).
*
* Overlays the content area (above the bottom bar, right-aligned), vertical menu items (icon+text+arrow),
* current page highlighted; tap the scrim to close, tap an item to navigate. Open/close animation is driven by the caller
* (MainScreen's AnimatedVisibility + spring).
* A slim right-aligned strip above the bottom bar, vertical menu items (icon+text+arrow),
* current page highlighted; tap outside to close, tap an item to navigate. No dimming scrim, so the
* page stays readable. Open/close animation is driven by the caller (MainScreen's AnimatedVisibility).
*/
package com.rtbishop.look4sat
import androidx.compose.foundation.background
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -45,16 +46,22 @@ fun MoreMenuPopup(
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.35f))
// Transparent catcher: taps outside the card dismiss the menu without
// dimming the page behind it.
.clickable(onClick = onDismiss)
) {
Card(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(12.dp),
.padding(12.dp)
.widthIn(max = 232.dp)
// Swallow taps on the card so they do not reach the dismiss
// catcher underneath.
.clickable(enabled = false) {},
shape = RoundedCornerShape(12.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.6f)),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
containerColor = MaterialTheme.colorScheme.surfaceContainer
)
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
@@ -77,7 +77,7 @@ import com.rtbishop.look4sat.core.presentation.R as CoreR
* 400-1200 Hz window and tracks speed on its own, so there is nothing to tune.
*/
@Composable
fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
fun CwDecodeScreen() {
val context = LocalContext.current
val container = remember { (context.applicationContext as IContainerProvider).getMainContainer() }
val decoder = remember { container.provideCwDecoder() }
@@ -95,9 +95,7 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
val decodedText by decoder.decodedText.collectAsState()
val historyText by decoder.historyText.collectAsState()
val estimatedPitch by decoder.estimatedPitch.collectAsState()
val signalStrength by decoder.signalStrength.collectAsState()
val inferenceMs by decoder.lastInferenceMs.collectAsState()
val errorMessage by decoder.errorMessage.collectAsState()
val permissionLauncher = rememberLauncherForActivityResult(
@@ -139,27 +137,13 @@ fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
.padding(horizontal = 4.dp, vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = { isListening = false; navigateUp() }) {
Icon(
painter = painterResource(CoreR.drawable.ic_back),
contentDescription = stringResource(R.string.cw_back)
)
}
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(CoreR.string.nav_cw),
style = MaterialTheme.typography.titleMedium
)
Text(
text = if (estimatedPitch != null) {
stringResource(R.string.cw_status_tone, estimatedPitch!!.toInt(), inferenceMs)
} else {
stringResource(R.string.cw_status_listening)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Text(
text = stringResource(CoreR.string.nav_cw),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
)
IconButton(onClick = { isListening = !isListening }) {
Icon(
painter = painterResource(
@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cw_back">Kembali</string>
<string name="cw_pause">Jeda pendekodean</string>
<string name="cw_resume">Lanjutkan pendekodean</string>
<string name="cw_clear">Hapus teks hasil dekode</string>
<string name="cw_status_listening">Mendengarkan…</string>
<string name="cw_status_tone">%1$d Hz · %2$d ms</string>
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk pendekodean CW</string>
<string name="cw_grant_permission">Berikan izin</string>
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cw_back">Kembali</string>
<string name="cw_pause">Jeda pendekodean</string>
<string name="cw_resume">Lanjutkan pendekodean</string>
<string name="cw_clear">Hapus teks hasil dekode</string>
<string name="cw_status_listening">Mendengarkan…</string>
<string name="cw_status_tone">%1$d Hz · %2$d ms</string>
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk pendekodean CW</string>
<string name="cw_grant_permission">Berikan izin</string>
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cw_back">Geri</string>
<string name="cw_pause">Çözmeyi duraklat</string>
<string name="cw_resume">Çözmeyi sürdür</string>
<string name="cw_clear">Çözülen metni temizle</string>
<string name="cw_status_listening">Dinleniyor…</string>
<string name="cw_status_tone">%1$d Hz · %2$d ms</string>
<string name="cw_mic_permission">CW çözümü için mikrofon izni gerekli</string>
<string name="cw_grant_permission">İzin ver</string>
<string name="cw_open_settings">Uygulama ayarlarını aç</string>
@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cw_back">返回</string>
<string name="cw_pause">暂停解码</string>
<string name="cw_resume">继续解码</string>
<string name="cw_clear">清空解码文本</string>
<string name="cw_status_listening">正在监听…</string>
<string name="cw_status_tone">%1$d Hz · %2$d 毫秒</string>
<string name="cw_mic_permission">CW 解码需要麦克风权限</string>
<string name="cw_grant_permission">授予权限</string>
<string name="cw_open_settings">打开应用设置</string>
@@ -1,11 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="cw_back">Back</string>
<string name="cw_pause">Pause decoding</string>
<string name="cw_resume">Resume decoding</string>
<string name="cw_clear">Clear decoded text</string>
<string name="cw_status_listening">Listening…</string>
<string name="cw_status_tone">%1$d Hz · %2$d ms</string>
<string name="cw_mic_permission">Microphone permission is required for CW decoding</string>
<string name="cw_grant_permission">Grant permission</string>
<string name="cw_open_settings">Open app settings</string>