feat(app): add authenticated app shell with top bar and bottom navigation (#6)

### 概述 / Summary
实现认证后的主应用壳层(`MainAppShell`),包含胶囊顶栏(`ABoxTopPillBar`)与 Material 3 底部导航栏(`ABoxNavigationBar`),并为团队其他成员预留标准 Composable 插槽。

### 核心变更 / Changes
1. **设计系统 (`:android:core:designsystem`)**:
   - 新增 `ABoxTopPillBar`:展示提供方/空间信息、快捷 `+` 操作与用户头像徽标。
   - 新增 `ABoxUserAvatar`:支持用户名首字符与主题强调轮廓。
   - 新增 `ABoxNavigationBar` & `ABoxNavigationBarItem`:M3 胶囊指示器底部导航栏封装。
2. **应用壳层 (`:android:app`)**:
   - 新增 `MainTab`(`DEVICES`, `SCENES`, `ACTIVITY`),严格对齐需求与分工术语。
   - 新增 `MainAppShell`:承载顶栏、底栏、Tab 平滑切换,并开放 `devicesSlot`、`scenesSlot`、`activitySlot`。
   - 新增 `AccountProviderBottomSheet`:点击头像展示当前会话/提供方信息、支持切换提供方与退出登录。
3. **多语言与测试**:
   - 补充 `values` 与 `values-zh-rCN` 双语资源。
   - 新增 `MainTabTest` 单元测试并通过全量 `check` 与 `test`。

### 分工保护 / Boundaries
- 仅提供顶栏、底栏与主页面容器骨架;
- 设备列表、情景编排与设备控制详情的具体 UI 保持为插槽,供 alZerNest、tzh 和 Aurum 后续无缝填充。

Reviewed-on: #6
Co-authored-by: KremeCN <3265494849@qq.com>
This commit was merged in pull request #6.
This commit is contained in:
2026-08-25 10:49:59 +08:00
committed by KremeCN
parent 06ccfdf952
commit ed98c2be98
16 changed files with 689 additions and 99 deletions
@@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -117,6 +118,8 @@ class AppSessionViewModelTest {
val state = viewModel.uiState.value
assertTrue(state is AppSessionUiState.Authenticated)
val authenticated = state as AppSessionUiState.Authenticated
assertEquals(credentials.username, authenticated.username)
}
private companion object {
@@ -0,0 +1,28 @@
package com.flagship.abox.manager.ui.shell
import com.flagship.abox.manager.R
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Test
class MainTabTest {
@Test
fun `all main tabs have distinct non-empty routes and valid label resources`() {
val tabs = MainTab.values()
assertEquals(3, tabs.size)
val routes = tabs.map { it.route }
assertEquals(routes.distinct().size, tabs.size)
tabs.forEach { tab ->
assertNotNull(tab.selectedIcon)
assertNotNull(tab.unselectedIcon)
assert(tab.route.isNotBlank())
}
assertEquals(R.string.tab_devices, MainTab.DEVICES.labelRes)
assertEquals(R.string.tab_scenes, MainTab.SCENES.labelRes)
assertEquals(R.string.tab_activity, MainTab.ACTIVITY.labelRes)
}
}