fix(nav): resolve the menu layout in core:domain so page order actually applies
用户报告"把 AMSAT 从更多菜单移到主菜单没生效"。排查后发现这是两个互相
掩盖的 bug, 其中一个会让用户永久无法进入设置页。
## Bug 1 (致命): 移任意页进主菜单 -> 设置入口从所有菜单消失
对"主菜单上限 5"的定义两处不一致:
- SettingsScreen.kt:892 判断 mainItems.filter { it != "Settings" }.size >= 5,
上限是【不含 Settings 的 5 个】, 于是认为 [Satellites,Passes,Radar,Map]
还有空位, 直接追加 -> 共 6 项
- MainScreen.kt:165 的 .take(5) 上限是【含 Settings 的前 5 个】, 而 Settings
排在最后 -> 被截断丢弃
结果 Settings 既不在底栏也不在更多菜单(它不在 subMenuOrder 里), 用户再也
进不去设置页, 无法自行改回, 只能清数据或重装。
## Bug 2 (用户报告): AMSAT / WavelogLog 移到主菜单被静默撤销
MainScreen.kt:161-163 给老用户补新页面的迁移逻辑【无条件执行】:
.let { list -> if ("AMSAT" in list) list else list + "AMSAT" }
用户把 AMSAT 移出子菜单后 subMenuOrder 里没有它, 这段又加回去, 第 165 行
filter { it.screenId !in subOrder } 于是永远过滤掉 AMSAT。
副作用: 这个 bug 恰好把主菜单拉回 5 项内, 反而掩盖了 Bug 1 —— 所以用户只
看到"AMSAT 移不过去", 没触发"设置锁死"。
## Bug 3: 溢出页面凭空消失而非落入更多菜单
.take(5) 直接丢弃超出的页面, 它们既不在底栏也不在更多菜单。
## Bug 4: 设置页展示顺序与底栏实际顺序不一致 (WYSIWYG 失效)
两处各自实现排序: 设置页不做 take(5)、用 sortedWith 把 Settings 钉最后;
MainScreen 做 take(5)、Settings 位置由 screenOrder 决定。
## Bug 5: 更多菜单里 AMSAT / Roaming 当前页不高亮
MoreMenuPopup.kt:69-79 的 when (currentKey) 缺 AmSat 与 Roaming 分支,
MainScreen.kt:188-198 缺 Roaming 分支, 靠 else -> false 兜底, 新增页面时
不会有编译错误提醒。Screen 子类都是 data object, 直接用 currentKey == screen
即可, 新增页面自动生效。
## Bug 6: 设置页主菜单区不过滤 hiddenScreens
MainScreen 会过滤隐藏页, 设置页不过滤, 于是隐藏的页面仍占据设置页的位置且
"移出主菜单"按钮可点, 与底栏实际情况不符。
## 改动
新增 core/domain/navigation/MenuLayout.kt (纯 Kotlin, 为 KMP 就绪) 作为菜单
布局的唯一入口:
- resolve(): 持久化偏好 -> 底栏 + 更多菜单。先给 Settings 预留名额再截断,
溢出页面落入更多菜单而非丢弃; 两个列表都没提到的页面按默认归位(升级不丢页)
- moveToMain() / moveToMore(): 移动语义集中一处, 拒绝把 Settings 移出
MainScreen 与 SettingsScreen 改为共用它, 删除双份实现与无条件迁移逻辑。
## 死代码清理 (每项已 grep 全仓库确认零引用)
- SettingsAction.ResetScreenOrder: 无任何派发点, 仅定义与 when 分支
- UiSettingsCard 的 onReorder 参数: 函数体内两个 DragOrderList 的 onReorder
实参都调 onUpdateMenu, 从未调用该参数
- SettingsAction.ReorderScreens: 唯一引用是上面那个死参数
- MainScreen 的 navigateToRadar 参数: 函数体内唯一出现是注释掉的一行
- Navigation.kt 的 defaultScreenOrder / defaultSubMenuOrder: 迁移后零引用,
默认值已在 MenuLayout 内
保留 entry<RadarDestination> 分支不动 —— RadarDestination NavKey 被
PassDetailsMatcher deeplink 使用。
## 验证
- MenuLayoutTest 13 个新测试全绿, 每个对应上面一个 bug 场景
- :core:domain:test 全量 100 个测试 0 失败 0 错误
(DataParser 19 / Doppler 17 / Qth 8 / Transponder 7 / CwCtc 7 /
CwDeepBuffer 13 / CwGolden 3 / CwSpectrogram 8 / MenuLayout 13 /
WaveLogApi 5)
- :core:domain:compileKotlin + :core:presentation + :app +
:feature:settings compileDebugKotlin => BUILD SUCCESSFUL
- 用 Python 复刻新规则重跑当初失败的全部场景: 5 个页面逐一移入主菜单,
设置入口全部保住、页面无丢失; 隐藏页 + 移动组合无页面丢失; 幂等性通过
This commit is contained in:
1 parent
8adf861ed7
commit
6fc2f560b7
8 files changed
+344
-96
No files matched your search
@@ -80,6 +80,7 @@ import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import com.rtbishop.look4sat.core.domain.repository.IContainerProvider
|
||||
import com.rtbishop.look4sat.core.domain.repository.MutualPassData
|
||||
import com.rtbishop.look4sat.core.domain.navigation.MenuLayout
|
||||
import com.rtbishop.look4sat.core.presentation.DeeplinkResolver
|
||||
import com.rtbishop.look4sat.core.presentation.ElevationThresholds
|
||||
import com.rtbishop.look4sat.core.presentation.LocalElevationThresholds
|
||||
@@ -124,7 +125,7 @@ fun NavRoot(deeplink: String? = null) {
|
||||
rememberViewModelStoreNavEntryDecorator() // Required for ViewModel scoping per entry
|
||||
),
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Passes> { MainScreen(navigateToRadar = { rootBackStack.add(RadarDestination) }) }
|
||||
entry<Screen.Passes> { MainScreen() }
|
||||
entry<RadarDestination> {
|
||||
Scaffold { innerPadding ->
|
||||
RadarDestination(navigateUp = navigateBack)
|
||||
@@ -136,7 +137,7 @@ fun NavRoot(deeplink: String? = null) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
fun MainScreen() {
|
||||
val backStack = rememberNavBackStack(Screen.Passes)
|
||||
val currentKey = backStack.lastOrNull()
|
||||
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
|
||||
@@ -145,27 +146,27 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
val container = (context.applicationContext as IContainerProvider).getMainContainer()
|
||||
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
|
||||
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
|
||||
// UI settings: sort by screenOrder (empty = default order), then filter by hiddenScreens (Settings always kept)
|
||||
val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.WavelogLog, Screen.AmSat, Screen.Map, Screen.Settings)
|
||||
.sortedBy { screen ->
|
||||
// Unknown pages (e.g. CwDecode not in old persisted order): use default-order position (Roaming<->Map), then fall back to last
|
||||
val idx = otherSettings.screenOrder.indexOf(screen.screenId)
|
||||
if (idx != -1) idx
|
||||
else com.rtbishop.look4sat.core.presentation.defaultScreenOrder.indexOf(screen.screenId).let {
|
||||
if (it != -1) it else Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
.filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings }
|
||||
// 4.5.1 foldable menu: main menu (5 bottom-bar slots) + More menu (overflow page)
|
||||
// Legacy migration: persisted subMenuOrder lacks new pages (WavelogLog) -> append to the sub-menu tail
|
||||
val subOrder = (otherSettings.subMenuOrder.ifEmpty { com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder })
|
||||
.let { list -> if ("WavelogLog" in list) list else list + "WavelogLog" }
|
||||
.let { list -> if ("AMSAT" in list) list else list + "AMSAT" }
|
||||
val mainNavItems = remember(allNavItems, subOrder) {
|
||||
allNavItems.filter { it.screenId !in subOrder }.take(5)
|
||||
// Menu layout is resolved in core:domain so the bar and the settings editor
|
||||
// cannot disagree, and so Settings can never be pushed out of both menus.
|
||||
val allNavItems = listOf(
|
||||
Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming,
|
||||
Screen.CwDecode, Screen.WavelogLog, Screen.AmSat, Screen.Map, Screen.Settings
|
||||
)
|
||||
val menuLayout = remember(
|
||||
otherSettings.screenOrder, otherSettings.subMenuOrder, otherSettings.hiddenScreens
|
||||
) {
|
||||
MenuLayout.resolve(
|
||||
allScreenIds = allNavItems.map { it.screenId },
|
||||
screenOrder = otherSettings.screenOrder,
|
||||
subMenuOrder = otherSettings.subMenuOrder,
|
||||
hiddenScreenIds = otherSettings.hiddenScreens
|
||||
)
|
||||
}
|
||||
val moreNavItems = remember(allNavItems, subOrder) {
|
||||
subOrder.mapNotNull { id -> allNavItems.find { it.screenId == id } }
|
||||
val mainNavItems = remember(menuLayout) {
|
||||
menuLayout.mainIds.mapNotNull { id -> allNavItems.find { it.screenId == id } }
|
||||
}
|
||||
val moreNavItems = remember(menuLayout) {
|
||||
menuLayout.moreIds.mapNotNull { id -> allNavItems.find { it.screenId == id } }
|
||||
}
|
||||
var moreExpanded by remember { mutableStateOf(false) }
|
||||
// Intercept Back while the More menu is open: close the menu first
|
||||
@@ -185,18 +186,9 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
NavigationSuiteScaffold(
|
||||
navigationSuiteItems = {
|
||||
mainNavItems.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.Mutual -> screen is Screen.Mutual
|
||||
is Screen.CwDecode -> screen is Screen.CwDecode
|
||||
is Screen.WavelogLog -> screen is Screen.WavelogLog
|
||||
is Screen.AmSat -> screen is Screen.AmSat
|
||||
is Screen.Map -> screen is Screen.Map
|
||||
is Screen.Settings -> screen is Screen.Settings
|
||||
else -> false
|
||||
}
|
||||
// Screen subclasses are data objects, so identity is enough and
|
||||
// newly added pages highlight without touching this call site.
|
||||
val isSelected = currentKey == screen
|
||||
item(
|
||||
icon = { Icon(painterResource(screen.iconResId), stringResource(screen.titleResId)) },
|
||||
label = { Text(stringResource(screen.titleResId)) },
|
||||
@@ -257,7 +249,6 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
|
||||
container.setMutualPassData(MutualPassData())
|
||||
container.satelliteRepo.selectPass(catNum, aosTime)
|
||||
backStack.add(Screen.Radar)
|
||||
// navigateToRadar()
|
||||
}
|
||||
}
|
||||
entry<Screen.Radar> {
|
||||
|
||||
@@ -66,17 +66,10 @@ fun MoreMenuPopup(
|
||||
) {
|
||||
Column(modifier = Modifier.padding(vertical = 4.dp)) {
|
||||
items.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.Mutual -> screen is Screen.Mutual
|
||||
is Screen.CwDecode -> screen is Screen.CwDecode
|
||||
is Screen.WavelogLog -> screen is Screen.WavelogLog
|
||||
is Screen.Map -> screen is Screen.Map
|
||||
is Screen.Settings -> screen is Screen.Settings
|
||||
else -> false
|
||||
}
|
||||
// Screen subclasses are data objects, so identity is enough. The
|
||||
// old per-type when was missing AmSat and Roaming, leaving those
|
||||
// pages unhighlighted while open.
|
||||
val isSelected = currentKey == screen
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
|
||||
Reference in new issue
Block a user