feat(app): add authenticated app shell with top bar and bottom navigation
This commit is contained in:
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -19,9 +20,10 @@ import com.flagship.abox.manager.R
|
||||
import com.flagship.abox.manager.designsystem.ABoxButton
|
||||
import com.flagship.abox.manager.designsystem.ABoxSurface
|
||||
import com.flagship.abox.manager.designsystem.ABoxTheme
|
||||
import com.flagship.abox.manager.ui.shell.MainAppShell
|
||||
|
||||
private const val FOUNDATION_ROUTE = "foundation"
|
||||
private const val PLACEHOLDER_ROUTE = "placeholder"
|
||||
private const val MAIN_SHELL_ROUTE = "main_shell"
|
||||
|
||||
@Composable
|
||||
fun ABoxManagerApp() {
|
||||
@@ -30,15 +32,27 @@ fun ABoxManagerApp() {
|
||||
val navController = rememberNavController()
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = FOUNDATION_ROUTE,
|
||||
startDestination = MAIN_SHELL_ROUTE,
|
||||
) {
|
||||
composable(FOUNDATION_ROUTE) {
|
||||
FoundationScreen(onContinue = { navController.navigate(PLACEHOLDER_ROUTE) })
|
||||
composable(MAIN_SHELL_ROUTE) {
|
||||
MainAppShell(
|
||||
userName = "KremeCN",
|
||||
providerName = "ABox",
|
||||
onSwitchProvider = {
|
||||
navController.navigate(FOUNDATION_ROUTE)
|
||||
},
|
||||
onLogout = {
|
||||
navController.navigate(FOUNDATION_ROUTE)
|
||||
},
|
||||
)
|
||||
}
|
||||
composable(PLACEHOLDER_ROUTE) {
|
||||
MessageScreen(
|
||||
title = stringResource(R.string.placeholder_title),
|
||||
description = stringResource(R.string.placeholder_description),
|
||||
composable(FOUNDATION_ROUTE) {
|
||||
FoundationScreen(
|
||||
onContinue = {
|
||||
navController.navigate(MAIN_SHELL_ROUTE) {
|
||||
popUpTo(FOUNDATION_ROUTE) { inclusive = true }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -55,10 +69,22 @@ private fun FoundationScreen(onContinue: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
MessageScreen(
|
||||
title = stringResource(R.string.foundation_title),
|
||||
description = stringResource(R.string.foundation_description),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.foundation_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.foundation_description),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
ABoxButton(
|
||||
onClick = onContinue,
|
||||
modifier = Modifier
|
||||
@@ -69,26 +95,3 @@ private fun FoundationScreen(onContinue: () -> Unit) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageScreen(
|
||||
title: String,
|
||||
description: String,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package com.flagship.abox.manager.ui.shell
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
||||
import androidx.compose.material.icons.filled.CloudDone
|
||||
import androidx.compose.material.icons.filled.SwapHoriz
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.flagship.abox.manager.R
|
||||
import com.flagship.abox.manager.designsystem.ABoxButton
|
||||
import com.flagship.abox.manager.designsystem.ABoxUserAvatar
|
||||
|
||||
/**
|
||||
* Account and Provider management bottom sheet triggered by clicking the top bar user avatar.
|
||||
* Allows viewing current session info, switching providers, and signing out.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AccountProviderBottomSheet(
|
||||
userName: String?,
|
||||
providerName: String,
|
||||
onDismissRequest: () -> Unit,
|
||||
onSwitchProvider: () -> Unit,
|
||||
onLogout: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
modifier = modifier,
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Header with large avatar & user info
|
||||
ABoxUserAvatar(
|
||||
name = userName,
|
||||
onClick = {},
|
||||
size = 56.dp,
|
||||
)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = userName?.ifBlank { null } ?: stringResource(R.string.account_sheet_title),
|
||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.account_sheet_signed_in_as),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(vertical = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
|
||||
// Provider Info Card
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CloudDone,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_sheet_provider),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = providerName,
|
||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
// Action Buttons
|
||||
OutlinedButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onSwitchProvider()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SwapHoriz,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.account_sheet_switch_provider))
|
||||
}
|
||||
|
||||
ABoxButton(
|
||||
onClick = {
|
||||
onDismissRequest()
|
||||
onLogout()
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.size(8.dp))
|
||||
Text(stringResource(R.string.account_sheet_logout))
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onDismissRequest,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_sheet_close),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.flagship.abox.manager.ui.shell
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.flagship.abox.manager.R
|
||||
import com.flagship.abox.manager.designsystem.ABoxNavigationBar
|
||||
import com.flagship.abox.manager.designsystem.ABoxNavigationBarItem
|
||||
import com.flagship.abox.manager.designsystem.ABoxTopPillBar
|
||||
|
||||
/**
|
||||
* Main authenticated application shell.
|
||||
*
|
||||
* Provides:
|
||||
* 1. A floating/pinned pill top app bar with provider context, '+' action, and avatar account sheet.
|
||||
* 2. Material 3 bottom navigation bar with pill indicator for Devices, Scenes, and Activity tabs.
|
||||
* 3. Clear composable slot boundaries for team member integrations.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun MainAppShell(
|
||||
modifier: Modifier = Modifier,
|
||||
userName: String? = "KremeCN",
|
||||
providerName: String = "ABox",
|
||||
initialTab: MainTab = MainTab.DEVICES,
|
||||
onTabSelected: (MainTab) -> Unit = {},
|
||||
onAddClick: (() -> Unit)? = null,
|
||||
onSwitchProvider: () -> Unit = {},
|
||||
onLogout: () -> Unit = {},
|
||||
devicesSlot: @Composable () -> Unit = { DefaultDevicesSlot() },
|
||||
scenesSlot: @Composable () -> Unit = { DefaultScenesSlot() },
|
||||
activitySlot: @Composable () -> Unit = { DefaultActivitySlot() },
|
||||
) {
|
||||
var activeTab by remember(initialTab) { mutableStateOf(initialTab) }
|
||||
var showAccountSheet by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.statusBarsPadding()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
ABoxTopPillBar(
|
||||
title = stringResource(R.string.top_bar_prompt_format, providerName),
|
||||
userName = userName,
|
||||
onAvatarClick = { showAccountSheet = true },
|
||||
onAddClick = onAddClick,
|
||||
addActionContentDescription = stringResource(R.string.action_add),
|
||||
avatarContentDescription = stringResource(R.string.action_account),
|
||||
)
|
||||
}
|
||||
},
|
||||
bottomBar = {
|
||||
ABoxNavigationBar(
|
||||
modifier = Modifier.navigationBarsPadding(),
|
||||
) {
|
||||
MainTab.values().forEach { tab ->
|
||||
val isSelected = (tab == activeTab)
|
||||
ABoxNavigationBarItem(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
activeTab = tab
|
||||
onTabSelected(tab)
|
||||
},
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = if (isSelected) tab.selectedIcon else tab.unselectedIcon,
|
||||
contentDescription = stringResource(tab.labelRes),
|
||||
)
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(tab.labelRes))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
) { innerPadding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding),
|
||||
) {
|
||||
Crossfade(
|
||||
targetState = activeTab,
|
||||
label = "MainTabCrossfade",
|
||||
) { tab ->
|
||||
when (tab) {
|
||||
MainTab.DEVICES -> devicesSlot()
|
||||
MainTab.SCENES -> scenesSlot()
|
||||
MainTab.ACTIVITY -> activitySlot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAccountSheet) {
|
||||
AccountProviderBottomSheet(
|
||||
userName = userName,
|
||||
providerName = providerName,
|
||||
onDismissRequest = { showAccountSheet = false },
|
||||
onSwitchProvider = onSwitchProvider,
|
||||
onLogout = onLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultDevicesSlot() {
|
||||
SlotPlaceholder(
|
||||
title = stringResource(R.string.devices_placeholder_title),
|
||||
description = stringResource(R.string.devices_placeholder_description),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultScenesSlot() {
|
||||
SlotPlaceholder(
|
||||
title = stringResource(R.string.scenes_placeholder_title),
|
||||
description = stringResource(R.string.scenes_placeholder_description),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultActivitySlot() {
|
||||
SlotPlaceholder(
|
||||
title = stringResource(R.string.activity_placeholder_title),
|
||||
description = stringResource(R.string.activity_placeholder_description),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SlotPlaceholder(
|
||||
title: String,
|
||||
description: String,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.flagship.abox.manager.ui.shell
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AutoAwesome
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.ViewAgenda
|
||||
import androidx.compose.material.icons.outlined.AutoAwesome
|
||||
import androidx.compose.material.icons.outlined.Home
|
||||
import androidx.compose.material.icons.outlined.ViewAgenda
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import com.flagship.abox.manager.R
|
||||
|
||||
/**
|
||||
* Main bottom navigation tabs for the authenticated application shell.
|
||||
*/
|
||||
enum class MainTab(
|
||||
@StringRes val labelRes: Int,
|
||||
val selectedIcon: ImageVector,
|
||||
val unselectedIcon: ImageVector,
|
||||
val route: String,
|
||||
) {
|
||||
DEVICES(
|
||||
labelRes = R.string.tab_devices,
|
||||
selectedIcon = Icons.Filled.Home,
|
||||
unselectedIcon = Icons.Outlined.Home,
|
||||
route = "shell/devices",
|
||||
),
|
||||
SCENES(
|
||||
labelRes = R.string.tab_scenes,
|
||||
selectedIcon = Icons.Filled.AutoAwesome,
|
||||
unselectedIcon = Icons.Outlined.AutoAwesome,
|
||||
route = "shell/scenes",
|
||||
),
|
||||
ACTIVITY(
|
||||
labelRes = R.string.tab_activity,
|
||||
selectedIcon = Icons.Filled.ViewAgenda,
|
||||
unselectedIcon = Icons.Outlined.ViewAgenda,
|
||||
route = "shell/activity",
|
||||
),
|
||||
}
|
||||
@@ -5,4 +5,30 @@
|
||||
<string name="continue_action">继续</string>
|
||||
<string name="placeholder_title">功能模块可以开始开发</string>
|
||||
<string name="placeholder_description">设备、红外和情景页面将由对应模块负责人实现。</string>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<string name="tab_devices">设备</string>
|
||||
<string name="tab_scenes">情景</string>
|
||||
<string name="tab_activity">活动记录</string>
|
||||
|
||||
<!-- Top Bar Actions -->
|
||||
<string name="top_bar_prompt_format">问问“%1$s”…</string>
|
||||
<string name="action_add">添加</string>
|
||||
<string name="action_account">账户与提供方</string>
|
||||
|
||||
<!-- Account & Provider Bottom Sheet -->
|
||||
<string name="account_sheet_title">账户与提供方</string>
|
||||
<string name="account_sheet_signed_in_as">当前登录为</string>
|
||||
<string name="account_sheet_provider">当前提供方</string>
|
||||
<string name="account_sheet_switch_provider">切换提供方</string>
|
||||
<string name="account_sheet_logout">退出登录</string>
|
||||
<string name="account_sheet_close">关闭</string>
|
||||
|
||||
<!-- Screen Placeholders -->
|
||||
<string name="devices_placeholder_title">设备列表</string>
|
||||
<string name="devices_placeholder_description">设备列表与控制卡片插槽(由 alZerNest 模块实现)。</string>
|
||||
<string name="scenes_placeholder_title">情景模式</string>
|
||||
<string name="scenes_placeholder_description">情景动作与执行插槽(由 tzh 模块实现)。</string>
|
||||
<string name="activity_placeholder_title">活动记录</string>
|
||||
<string name="activity_placeholder_description">未记录任何事件。</string>
|
||||
</resources>
|
||||
|
||||
@@ -5,4 +5,30 @@
|
||||
<string name="continue_action">Continue</string>
|
||||
<string name="placeholder_title">Feature development can start</string>
|
||||
<string name="placeholder_description">Device, infrared and scene screens will be added by their module owners.</string>
|
||||
|
||||
<!-- Navigation Tabs -->
|
||||
<string name="tab_devices">Devices</string>
|
||||
<string name="tab_scenes">Scenes</string>
|
||||
<string name="tab_activity">Activity</string>
|
||||
|
||||
<!-- Top Bar Actions -->
|
||||
<string name="top_bar_prompt_format">Ask \"%1$s\"…</string>
|
||||
<string name="action_add">Add</string>
|
||||
<string name="action_account">Account & Provider</string>
|
||||
|
||||
<!-- Account & Provider Bottom Sheet -->
|
||||
<string name="account_sheet_title">Account & Provider</string>
|
||||
<string name="account_sheet_signed_in_as">Signed in as</string>
|
||||
<string name="account_sheet_provider">Active Provider</string>
|
||||
<string name="account_sheet_switch_provider">Switch Provider</string>
|
||||
<string name="account_sheet_logout">Sign Out</string>
|
||||
<string name="account_sheet_close">Close</string>
|
||||
|
||||
<!-- Screen Placeholders -->
|
||||
<string name="devices_placeholder_title">Device List</string>
|
||||
<string name="devices_placeholder_description">Device list and control cards slot for alZerNest.</string>
|
||||
<string name="scenes_placeholder_title">Scene Mode</string>
|
||||
<string name="scenes_placeholder_description">Scene actions and execution slot for tzh.</string>
|
||||
<string name="activity_placeholder_title">Activity Log</string>
|
||||
<string name="activity_placeholder_description">No recorded events yet.</string>
|
||||
</resources>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user