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
+21
-38
@@ -105,8 +105,7 @@ import com.rtbishop.look4sat.core.presentation.R
|
||||
import com.rtbishop.look4sat.core.presentation.ScreenColumn
|
||||
import com.rtbishop.look4sat.core.presentation.TopBar
|
||||
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
|
||||
import com.rtbishop.look4sat.core.presentation.defaultScreenOrder
|
||||
import com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder
|
||||
import com.rtbishop.look4sat.core.domain.navigation.MenuLayout
|
||||
import com.rtbishop.look4sat.core.presentation.isVerticalLayout
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
@@ -406,7 +405,6 @@ private fun SettingsScreen(
|
||||
screenOrder = uiState.otherSettings.screenOrder,
|
||||
subMenuOrder = uiState.otherSettings.subMenuOrder,
|
||||
onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) },
|
||||
onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) },
|
||||
onResetOrder = { onAction(SettingsAction.ResetMenuOrder) },
|
||||
onUpdateMenu = { main, sub -> onAction(SettingsAction.UpdateMenuOrder(main, sub)) }
|
||||
)
|
||||
@@ -797,7 +795,6 @@ private fun UiSettingsCard(
|
||||
screenOrder: List<String>,
|
||||
subMenuOrder: List<String>,
|
||||
onToggle: (String) -> Unit,
|
||||
onReorder: (List<String>) -> Unit,
|
||||
onResetOrder: () -> Unit,
|
||||
onUpdateMenu: (List<String>, List<String>) -> Unit
|
||||
) {
|
||||
@@ -813,25 +810,18 @@ private fun UiSettingsCard(
|
||||
R.string.nav_map to "Map",
|
||||
R.string.nav_prefs to "Settings"
|
||||
) // name 必须与 Screen.screenId 一致 (R8 安全)
|
||||
// Main menu items: exclude sub-menu items; Settings pinned last
|
||||
val mainItems = remember(screens, screenOrder, subMenuOrder) {
|
||||
val sub = subMenuOrder.ifEmpty { defaultSubMenuOrder }
|
||||
screens.map { it.second }
|
||||
.filter { it !in sub }
|
||||
.sortedBy { name ->
|
||||
screenOrder.indexOf(name).let { idx ->
|
||||
if (idx != -1) idx else defaultScreenOrder.indexOf(name).let {
|
||||
if (it != -1) it else Int.MAX_VALUE
|
||||
}
|
||||
}
|
||||
}
|
||||
.sortedWith(compareBy { if (it == "Settings") 1 else 0 })
|
||||
}
|
||||
// More-menu items
|
||||
val subItems = remember(screens, subMenuOrder) {
|
||||
val sub = subMenuOrder.ifEmpty { defaultSubMenuOrder }
|
||||
sub.filter { s -> screens.any { (_, name) -> name == s } }
|
||||
// Resolved by the same core:domain rule the bottom bar uses, so this list is
|
||||
// exactly what the user will see on the bar.
|
||||
val menuLayout = remember(screens, screenOrder, subMenuOrder, hiddenScreens) {
|
||||
MenuLayout.resolve(
|
||||
allScreenIds = screens.map { it.second },
|
||||
screenOrder = screenOrder,
|
||||
subMenuOrder = subMenuOrder,
|
||||
hiddenScreenIds = hiddenScreens
|
||||
)
|
||||
}
|
||||
val mainItems = menuLayout.mainIds
|
||||
val subItems = menuLayout.moreIds
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
@@ -868,12 +858,12 @@ private fun UiSettingsCard(
|
||||
moveLabel = stringResource(id = R.string.prefs_ui_move_out),
|
||||
moveEnabled = { name -> name != "Settings" },
|
||||
onMove = { name ->
|
||||
onUpdateMenu(
|
||||
mainItems.filter { it != name },
|
||||
(subMenuOrder.ifEmpty { defaultSubMenuOrder } + name).distinct()
|
||||
val moved = MenuLayout.moveToMore(
|
||||
name, screens.map { it.second }, screenOrder, subMenuOrder
|
||||
)
|
||||
onUpdateMenu(moved.screenOrder, moved.subMenuOrder)
|
||||
},
|
||||
onReorder = { main -> onUpdateMenu(main, subMenuOrder) }
|
||||
onReorder = { main -> onUpdateMenu(main, subItems) }
|
||||
)
|
||||
// More-menu area (pages behind "More")
|
||||
Text(
|
||||
@@ -888,19 +878,12 @@ private fun UiSettingsCard(
|
||||
// Buttons always visible; when the main menu hits 5, the last non-Settings item moves to More (swap)
|
||||
moveEnabled = { true },
|
||||
onMove = { name ->
|
||||
val mainWithoutSettings = mainItems.filter { it != "Settings" }
|
||||
val needSwap = mainWithoutSettings.size >= 5
|
||||
val evicted = if (needSwap) mainWithoutSettings.last() else null
|
||||
val newMain = if (needSwap) {
|
||||
(mainWithoutSettings.dropLast(1) + name).distinct()
|
||||
} else {
|
||||
(mainWithoutSettings + name).distinct()
|
||||
}
|
||||
val newSub = (subItems.filter { it != name } +
|
||||
if (evicted != null) listOf(evicted) else emptyList()).distinct()
|
||||
onUpdateMenu(newMain, newSub)
|
||||
val moved = MenuLayout.moveToMain(
|
||||
name, screens.map { it.second }, screenOrder, subMenuOrder
|
||||
)
|
||||
onUpdateMenu(moved.screenOrder, moved.subMenuOrder)
|
||||
},
|
||||
onReorder = { sub -> onUpdateMenu(screenOrder, sub) }
|
||||
onReorder = { sub -> onUpdateMenu(mainItems, sub) }
|
||||
)
|
||||
TextButton(onClick = onResetOrder) {
|
||||
Text(text = stringResource(id = R.string.prefs_ui_order_reset))
|
||||
|
||||
@@ -66,10 +66,6 @@ sealed interface SettingsAction {
|
||||
data class ToggleNightMode(val value: Boolean) : SettingsAction
|
||||
// UI settings: toggle a bottom-nav page's hidden state (Screen simpleName)
|
||||
data class ToggleScreen(val screenName: String) : SettingsAction
|
||||
// UI settings: update page order
|
||||
data class ReorderScreens(val order: List<String>) : SettingsAction
|
||||
// UI settings: reset to default order
|
||||
data object ResetScreenOrder : SettingsAction
|
||||
// UI settings: update main + More menu order (4.5.1 foldable menu)
|
||||
data class UpdateMenuOrder(val mainOrder: List<String>, val subOrder: List<String>) : SettingsAction
|
||||
// UI settings: reset main + More menu order
|
||||
|
||||
-2
@@ -134,8 +134,6 @@ class SettingsViewModel(
|
||||
if (action.screenName in hidden) hidden.remove(action.screenName) else hidden.add(action.screenName)
|
||||
current.copy(hiddenScreens = hidden)
|
||||
}
|
||||
is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) }
|
||||
SettingsAction.ResetScreenOrder -> settingsRepo.updateOtherSettings { it.copy(screenOrder = emptyList()) }
|
||||
is SettingsAction.UpdateMenuOrder -> settingsRepo.updateOtherSettings {
|
||||
it.copy(screenOrder = action.mainOrder, subMenuOrder = action.subOrder)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user