fix(ui): stabilize theme and navigation behavior

This commit is contained in:
2026-08-27 23:20:39 +08:00
parent bdeddf46e2
commit a5f31486b4
14 changed files with 322 additions and 180 deletions
@@ -57,7 +57,6 @@ class SettingsScreenTest {
"settings-theme-mode", "settings-theme-mode",
"settings-color-style", "settings-color-style",
"settings-color-standard", "settings-color-standard",
"settings-predictive-back",
"settings-interface-scale", "settings-interface-scale",
) )
composeRule.onAllNodesWithTag("settings-renderer-selector").assertCountEquals(0) composeRule.onAllNodesWithTag("settings-renderer-selector").assertCountEquals(0)
@@ -74,7 +73,6 @@ class SettingsScreenTest {
"settings-blur", "settings-blur",
"settings-floating-navigation", "settings-floating-navigation",
"settings-liquid-glass", "settings-liquid-glass",
"settings-predictive-back",
"settings-interface-scale", "settings-interface-scale",
) )
composeRule.onAllNodesWithTag("settings-renderer-selector").assertCountEquals(0) composeRule.onAllNodesWithTag("settings-renderer-selector").assertCountEquals(0)
+1 -1
View File
@@ -3,7 +3,7 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".ABoxManagerApplication" android:name=".ABoxManagerApplication"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="false"
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -20,6 +19,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
import com.flagship.abox.manager.designsystem.ABoxSurface import com.flagship.abox.manager.designsystem.ABoxSurface
import com.flagship.abox.manager.designsystem.ABoxTheme import com.flagship.abox.manager.designsystem.ABoxTheme
import com.flagship.abox.manager.designsystem.ABoxBackLayer
import com.flagship.abox.manager.domain.ProviderProfile import com.flagship.abox.manager.domain.ProviderProfile
import com.flagship.abox.manager.domain.Username import com.flagship.abox.manager.domain.Username
import com.flagship.abox.manager.ui.login.LoginRoute import com.flagship.abox.manager.ui.login.LoginRoute
@@ -108,13 +108,7 @@ private fun AuthenticatedNavHost(
onShowAccountDialogChange: (Boolean) -> Unit, onShowAccountDialogChange: (Boolean) -> Unit,
onShowSettingsChange: (Boolean) -> Unit, onShowSettingsChange: (Boolean) -> Unit,
) { ) {
if (showSettings) { Box(Modifier.fillMaxSize()) {
BackHandler { onShowSettingsChange(false) }
SettingsRoute(
experienceState = settingsExperienceState,
onExit = { onShowSettingsChange(false) },
)
} else {
AuthenticatedNavigation( AuthenticatedNavigation(
username = username, username = username,
profile = profile, profile = profile,
@@ -122,6 +116,17 @@ private fun AuthenticatedNavHost(
onProviderClick = { onShowAccountDialogChange(true) }, onProviderClick = { onShowAccountDialogChange(true) },
onAvatarClick = { onShowAccountDialogChange(true) }, onAvatarClick = { onShowAccountDialogChange(true) },
) )
if (showSettings) {
ABoxBackLayer(
onBack = { onShowSettingsChange(false) },
enabled = settingsExperienceState.isHome,
) {
SettingsRoute(
experienceState = settingsExperienceState,
onExit = { onShowSettingsChange(false) },
)
}
}
} }
if (showAccountDialog) { if (showAccountDialog) {
@@ -131,6 +136,7 @@ private fun AuthenticatedNavHost(
onDismissRequest = { onShowAccountDialogChange(false) }, onDismissRequest = { onShowAccountDialogChange(false) },
onOpenSettings = { onOpenSettings = {
onShowAccountDialogChange(false) onShowAccountDialogChange(false)
settingsExperienceState.showTheme(returnToHome = false)
onShowSettingsChange(true) onShowSettingsChange(true)
}, },
onSwitchProvider = { onSwitchProvider = {
@@ -1,15 +1,11 @@
package com.flagship.abox.manager.ui.navigation package com.flagship.abox.manager.ui.navigation
import android.net.Uri import android.net.Uri
import androidx.activity.compose.BackHandler
import androidx.activity.compose.PredictiveBackHandler
import android.os.Build
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -17,14 +13,11 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
@@ -60,9 +53,6 @@ import com.flagship.abox.manager.ui.shell.MainAppShell
import com.flagship.abox.manager.ui.shell.MainTab import com.flagship.abox.manager.ui.shell.MainTab
import com.flagship.abox.manager.ui.shell.detailPresentation import com.flagship.abox.manager.ui.shell.detailPresentation
import com.flagship.abox.manager.ui.shell.widthClass import com.flagship.abox.manager.ui.shell.widthClass
import com.flagship.abox.manager.designsystem.LocalABoxPredictiveBackEnabled
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.collect
import com.flagship.abox.manager.ui.socket.SocketScreen import com.flagship.abox.manager.ui.socket.SocketScreen
import com.flagship.abox.manager.ui.socket.SocketUiEvent import com.flagship.abox.manager.ui.socket.SocketUiEvent
import com.flagship.abox.manager.ui.socket.SocketViewModel import com.flagship.abox.manager.ui.socket.SocketViewModel
@@ -169,12 +159,11 @@ fun AuthenticatedNavigationStateHost(
} }
val selection = selectedDevice ?: return@LaunchedEffect val selection = selectedDevice ?: return@LaunchedEffect
when (detailPresentation(windowSizeClass, selection)) {
DetailPresentation.PANE -> {
if (savedSelection != selection) { if (savedSelection != selection) {
savedSelection = selection savedSelection = selection
} }
when (detailPresentation(windowSizeClass, selection)) {
DetailPresentation.PANE -> {
if (currentRoute != MainTab.DEVICES.route) { if (currentRoute != MainTab.DEVICES.route) {
navController.popBackStack(MainTab.DEVICES.route, inclusive = false) navController.popBackStack(MainTab.DEVICES.route, inclusive = false)
} }
@@ -184,6 +173,9 @@ fun AuthenticatedNavigationStateHost(
navController.navigate(deviceRoute(selection)) { navController.navigate(deviceRoute(selection)) {
launchSingleTop = true launchSingleTop = true
} }
savedSelection = null
} else if (savedSelection != null) {
savedSelection = null
} }
} }
} }
@@ -194,21 +186,6 @@ fun AuthenticatedNavigationStateHost(
selectedDevice = selectedDevice, selectedDevice = selectedDevice,
detailPresentation = selectedPresentation, detailPresentation = selectedPresentation,
) )
val canNavigateBack = selectedDevice != null || currentRoute != MainTab.DEVICES.route
val predictiveBackEnabled = LocalABoxPredictiveBackEnabled.current &&
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
var backProgress by remember { mutableFloatStateOf(0f) }
Box(
modifier = Modifier.fillMaxSize()
.clip(RoundedCornerShape((28f * backProgress).dp))
.graphicsLayer {
val scale = 1f - 0.04f * backProgress
scaleX = scale
scaleY = scale
translationX = 28.dp.toPx() * backProgress
},
) {
content( content(
snapshot, snapshot,
{ tab -> { tab ->
@@ -225,29 +202,9 @@ fun AuthenticatedNavigationStateHost(
} }
} }
}, },
{ deviceName, type -> { deviceName, type -> savedSelection = DeviceSelection(profileId, deviceName, type) },
savedSelection = DeviceSelection(profileId, deviceName, type)
},
{ savedSelection = null }, { savedSelection = null },
) )
}
PredictiveBackHandler(enabled = canNavigateBack && predictiveBackEnabled) { events ->
try {
events.collect { event -> backProgress = event.progress }
savedSelection = null
navController.popCurrentDestinationUnlessRoot()
} catch (cancelled: CancellationException) {
throw cancelled
} finally {
backProgress = 0f
}
}
BackHandler(enabled = canNavigateBack && !predictiveBackEnabled) {
savedSelection = null
navController.popCurrentDestinationUnlessRoot()
}
} }
@Composable @Composable
@@ -355,6 +312,23 @@ private fun AuthenticatedNavHost(
onLogout: () -> Unit, onLogout: () -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
) { ) {
val activeBackStackEntry by navController.currentBackStackEntryAsState()
val activeRoute = activeBackStackEntry?.actualRoute()
var backNavigationInFlight by remember { mutableStateOf(false) }
LaunchedEffect(activeRoute) {
backNavigationInFlight = false
}
val navigateBackFromDetail: () -> Unit = {
val liveRoute = navController.currentBackStackEntry?.actualRoute()
val isMainDestination = MainTab.entries.any { it.route == liveRoute }
if (!backNavigationInFlight && !isMainDestination) {
backNavigationInFlight = true
if (!navController.popBackStack()) {
backNavigationInFlight = false
}
}
}
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = MainTab.DEVICES.route, startDestination = MainTab.DEVICES.route,
@@ -427,14 +401,14 @@ private fun AuthenticatedNavHost(
deviceDetailGraph( deviceDetailGraph(
onBackClick = { onBackClick = {
onClearDeviceSelection() onClearDeviceSelection()
navController.popBackStack() navigateBackFromDetail()
}, },
) )
sceneEditorDestination( sceneEditorDestination(
onBack = navController::popBackStack, onBack = navigateBackFromDetail,
onSaved = { navController.popBackStack(MainTab.SCENES.route, inclusive = false) }, onSaved = { navController.popBackStack(MainTab.SCENES.route, inclusive = false) },
) )
sceneExecutionDestination(onBack = navController::popBackStack) sceneExecutionDestination(onBack = navigateBackFromDetail)
composable( composable(
route = Routes.INFRARED_HUB, route = Routes.INFRARED_HUB,
arguments = listOf( arguments = listOf(
@@ -445,7 +419,7 @@ private fun AuthenticatedNavHost(
InfraredHubRoute( InfraredHubRoute(
onBack = { onBack = {
onClearDeviceSelection() onClearDeviceSelection()
navController.popBackStack() navigateBackFromDetail()
}, },
onOpenRemote = { remoteId -> onOpenRemote = { remoteId ->
navController.navigate( navController.navigate(
@@ -467,7 +441,7 @@ private fun AuthenticatedNavHost(
), ),
) { ) {
RemotePanelRoute( RemotePanelRoute(
onBack = navController::popBackStack, onBack = navigateBackFromDetail,
onSessionInvalid = onLogout, onSessionInvalid = onLogout,
) )
} }
@@ -479,7 +453,7 @@ private fun AuthenticatedNavHost(
), ),
) { ) {
InfraredControlRoute( InfraredControlRoute(
onBack = navController::popBackStack, onBack = navigateBackFromDetail,
onSessionInvalid = onLogout, onSessionInvalid = onLogout,
) )
} }
@@ -561,9 +535,3 @@ private fun NavBackStackEntry.actualRoute(): String? {
} }
return route return route
} }
private fun NavHostController.popCurrentDestinationUnlessRoot() {
if (currentBackStackEntry?.actualRoute() != MainTab.DEVICES.route) {
popBackStack()
}
}
@@ -1,6 +1,5 @@
package com.flagship.abox.manager.ui.preferences package com.flagship.abox.manager.ui.preferences
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -58,6 +57,9 @@ import com.flagship.abox.manager.designsystem.LocalABoxShapes
import com.flagship.abox.manager.designsystem.icon.ABoxIcon import com.flagship.abox.manager.designsystem.icon.ABoxIcon
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
import com.flagship.abox.manager.preferences.AccentPreference import com.flagship.abox.manager.preferences.AccentPreference
import com.flagship.abox.manager.designsystem.tokens.ABoxAccentPalette
import com.flagship.abox.manager.designsystem.ABoxBackLayer
import com.flagship.abox.manager.designsystem.ABoxSurface
import com.flagship.abox.manager.preferences.AppThemeMode import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.DesignRenderer import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.preferences.InterfaceScale import com.flagship.abox.manager.preferences.InterfaceScale
@@ -70,11 +72,27 @@ private enum class SettingsDestination { HOME, THEME }
@Stable @Stable
class SettingsExperienceState(initialDestination: String = SettingsDestination.HOME.name) { class SettingsExperienceState(initialDestination: String = SettingsDestination.HOME.name) {
var destination: String by mutableStateOf(initialDestination) var destination: String by mutableStateOf(initialDestination)
var themeBackReturnsHome: Boolean by mutableStateOf(true)
val isHome: Boolean get() = destination == SettingsDestination.HOME.name
fun showHome() {
destination = SettingsDestination.HOME.name
}
fun showTheme(returnToHome: Boolean = true) {
themeBackReturnsHome = returnToHome
destination = SettingsDestination.THEME.name
}
companion object { companion object {
val Saver: Saver<SettingsExperienceState, String> = Saver( val Saver: Saver<SettingsExperienceState, String> = Saver(
save = { it.destination }, save = { "${it.destination}|${it.themeBackReturnsHome}" },
restore = { SettingsExperienceState(it) }, restore = { saved ->
val parts = saved.split('|', limit = 2)
SettingsExperienceState(parts.first()).apply {
themeBackReturnsHome = parts.getOrNull(1)?.toBooleanStrictOrNull() ?: true
}
},
) )
} }
} }
@@ -98,23 +116,30 @@ fun SettingsExperience(
) { ) {
val destination = runCatching { SettingsDestination.valueOf(experienceState.destination) } val destination = runCatching { SettingsDestination.valueOf(experienceState.destination) }
.getOrDefault(SettingsDestination.HOME) .getOrDefault(SettingsDestination.HOME)
BackHandler { Box(Modifier.fillMaxSize()) {
if (destination == SettingsDestination.THEME) experienceState.destination = SettingsDestination.HOME.name ABoxSurface(modifier = Modifier.fillMaxSize()) {
else onExit() SettingsHomeScreen(
}
when (destination) {
SettingsDestination.HOME -> SettingsHomeScreen(
state = state, state = state,
onOpenTheme = { experienceState.destination = SettingsDestination.THEME.name }, onOpenTheme = { experienceState.showTheme(returnToHome = true) },
onBack = onExit, onBack = onExit,
onAction = onAction, onAction = onAction,
) )
SettingsDestination.THEME -> ThemeSettingsScreen( }
if (destination == SettingsDestination.THEME) {
val leaveTheme = {
if (experienceState.themeBackReturnsHome) experienceState.showHome() else onExit()
}
ABoxBackLayer(onBack = leaveTheme) {
ABoxSurface(modifier = Modifier.fillMaxSize()) {
ThemeSettingsScreen(
state = state, state = state,
onBack = { experienceState.destination = SettingsDestination.HOME.name }, onBack = leaveTheme,
onAction = onAction, onAction = onAction,
) )
} }
}
}
}
} }
@Composable @Composable
@@ -218,6 +243,7 @@ private fun ThemeSettingsScreen(
@Composable @Composable
private fun ColumnScope.MaterialThemeOptions(state: SettingsUiState, onAction: (SettingsUiAction) -> Unit) { private fun ColumnScope.MaterialThemeOptions(state: SettingsUiState, onAction: (SettingsUiAction) -> Unit) {
val manualColorsEnabled = !state.preferences.dynamicColorEnabled || Build.VERSION.SDK_INT < Build.VERSION_CODES.S
SettingsGroup(modifier = Modifier.testTag("settings-material-colors")) { SettingsGroup(modifier = Modifier.testTag("settings-material-colors")) {
SettingsSwitchRow( SettingsSwitchRow(
icon = ABoxIconKey.THEME, icon = ABoxIconKey.THEME,
@@ -238,6 +264,7 @@ private fun ColumnScope.MaterialThemeOptions(state: SettingsUiState, onAction: (
values = ColorStyle.entries, values = ColorStyle.entries,
label = { colorStyleLabel(it) }, label = { colorStyleLabel(it) },
onSelected = { onAction(SettingsUiAction.SetColorStyle(it)) }, onSelected = { onAction(SettingsUiAction.SetColorStyle(it)) },
enabled = manualColorsEnabled,
) )
EnumSettingRow( EnumSettingRow(
modifier = Modifier.testTag("settings-color-standard"), modifier = Modifier.testTag("settings-color-standard"),
@@ -247,8 +274,8 @@ private fun ColumnScope.MaterialThemeOptions(state: SettingsUiState, onAction: (
values = ColorStandard.entries, values = ColorStandard.entries,
label = { colorStandardLabel(it) }, label = { colorStandardLabel(it) },
onSelected = { onAction(SettingsUiAction.SetColorStandard(it)) }, onSelected = { onAction(SettingsUiAction.SetColorStandard(it)) },
enabled = manualColorsEnabled,
) )
PredictiveBackSetting(state, onAction)
SettingsGroup(modifier = Modifier.testTag("settings-interface-scale")) { ScaleSelector(state, onAction) } SettingsGroup(modifier = Modifier.testTag("settings-interface-scale")) { ScaleSelector(state, onAction) }
} }
@@ -303,26 +330,9 @@ private fun ColumnScope.MiuixThemeOptions(state: SettingsUiState, onAction: (Set
}, },
) { onAction(SettingsUiAction.SetLiquidGlass(it)) } ) { onAction(SettingsUiAction.SetLiquidGlass(it)) }
} }
PredictiveBackSetting(state, onAction)
SettingsGroup(modifier = Modifier.testTag("settings-interface-scale")) { ScaleSelector(state, onAction) } SettingsGroup(modifier = Modifier.testTag("settings-interface-scale")) { ScaleSelector(state, onAction) }
} }
@Composable
private fun PredictiveBackSetting(state: SettingsUiState, onAction: (SettingsUiAction) -> Unit) {
val supported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
SettingsGroup(modifier = Modifier.testTag("settings-predictive-back")) {
SettingsSwitchRow(
icon = ABoxIconKey.BACK,
title = stringResource(R.string.settings_predictive_back),
description = stringResource(R.string.settings_predictive_back_description),
checked = state.preferences.predictiveBackEnabled,
tag = "settings-predictive-back-switch",
enabled = supported,
unavailableDescription = if (supported) null else stringResource(R.string.settings_requires_android_13),
) { onAction(SettingsUiAction.SetPredictiveBack(it)) }
}
}
@Composable @Composable
private fun SettingsPage( private fun SettingsPage(
title: String, title: String,
@@ -448,15 +458,17 @@ private fun <T> EnumSettingRow(
values: List<T>, values: List<T>,
label: @Composable (T) -> String, label: @Composable (T) -> String,
onSelected: (T) -> Unit, onSelected: (T) -> Unit,
enabled: Boolean = true,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
SettingsGroup(modifier = modifier) { SettingsGroup(modifier = modifier) {
Box(Modifier.fillMaxWidth()) { Box(Modifier.fillMaxWidth().alpha(if (enabled) 1f else 0.42f)) {
SettingsLinkRow( SettingsLinkRow(
icon = icon, icon = icon,
title = title, title = title,
description = stringResource(R.string.settings_tap_to_choose), description = stringResource(R.string.settings_tap_to_choose),
value = value, value = value,
enabled = enabled,
onClick = { expanded = true }, onClick = { expanded = true },
) )
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
@@ -478,18 +490,29 @@ private fun <T> EnumSettingRow(
@Composable @Composable
private fun AccentSelector(state: SettingsUiState, onAction: (SettingsUiAction) -> Unit) { private fun AccentSelector(state: SettingsUiState, onAction: (SettingsUiAction) -> Unit) {
val dark = when (state.preferences.themeMode) {
AppThemeMode.DARK -> true
AppThemeMode.LIGHT -> false
AppThemeMode.SYSTEM -> androidx.compose.foundation.isSystemInDarkTheme()
}
val manualColorsEnabled = !state.preferences.dynamicColorEnabled || Build.VERSION.SDK_INT < Build.VERSION_CODES.S
Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) { Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(stringResource(R.string.settings_accent_label), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold) Text(stringResource(R.string.settings_accent_label), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
Row( Row(
modifier = Modifier.fillMaxWidth().alpha(if (state.preferences.dynamicColorEnabled) 0.42f else 1f), modifier = Modifier.fillMaxWidth().alpha(if (manualColorsEnabled) 1f else 0.42f),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
AccentPreference.entries.forEach { accent -> AccentPreference.entries.forEach { accent ->
val color = accentColor(accent) val color = ABoxAccentPalette.preview(
accent = accent,
style = state.preferences.colorStyle,
standard = state.preferences.colorStandard,
dark = dark,
)
Box( Box(
modifier = Modifier.size(44.dp).background(color, CircleShape) modifier = Modifier.size(44.dp).background(color, CircleShape)
.then(if (state.preferences.accentPreference == accent) Modifier.border(3.dp, MaterialTheme.colorScheme.onSurface, CircleShape) else Modifier) .then(if (state.preferences.accentPreference == accent) Modifier.border(3.dp, MaterialTheme.colorScheme.onSurface, CircleShape) else Modifier)
.clickable(enabled = !state.preferences.dynamicColorEnabled) { onAction(SettingsUiAction.SetAccent(accent)) }, .clickable(enabled = manualColorsEnabled) { onAction(SettingsUiAction.SetAccent(accent)) },
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
if (state.preferences.accentPreference == accent) Icon(Icons.Outlined.Check, null, tint = Color.White) if (state.preferences.accentPreference == accent) Icon(Icons.Outlined.Check, null, tint = Color.White)
@@ -518,8 +541,8 @@ private fun ScaleSelector(state: SettingsUiState, onAction: (SettingsUiAction) -
} }
@Composable @Composable
private fun SettingsLinkRow(icon: ABoxIconKey, title: String, description: String, value: String?, modifier: Modifier = Modifier, onClick: () -> Unit) { private fun SettingsLinkRow(icon: ABoxIconKey, title: String, description: String, value: String?, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) {
Row(modifier.fillMaxWidth().clickable(onClick = onClick).padding(14.dp), verticalAlignment = Alignment.CenterVertically) { Row(modifier.fillMaxWidth().clickable(enabled = enabled, onClick = onClick).padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
SettingsRowHeader(icon, title, description, Modifier.weight(1f)) SettingsRowHeader(icon, title, description, Modifier.weight(1f))
if (value != null) Text(value, color = MaterialTheme.colorScheme.onSurfaceVariant) if (value != null) Text(value, color = MaterialTheme.colorScheme.onSurfaceVariant)
Icon(Icons.Outlined.ChevronRight, null, tint = MaterialTheme.colorScheme.onSurfaceVariant) Icon(Icons.Outlined.ChevronRight, null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
@@ -570,4 +593,3 @@ private fun SettingsRowHeader(icon: ABoxIconKey, title: String, description: Str
@Composable private fun themeLabel(mode: AppThemeMode) = stringResource(when (mode) { AppThemeMode.SYSTEM -> R.string.settings_theme_system; AppThemeMode.LIGHT -> R.string.settings_theme_light; AppThemeMode.DARK -> R.string.settings_theme_dark }) @Composable private fun themeLabel(mode: AppThemeMode) = stringResource(when (mode) { AppThemeMode.SYSTEM -> R.string.settings_theme_system; AppThemeMode.LIGHT -> R.string.settings_theme_light; AppThemeMode.DARK -> R.string.settings_theme_dark })
@Composable private fun colorStyleLabel(value: ColorStyle) = stringResource(when (value) { ColorStyle.TONAL_SPOT -> R.string.settings_color_style_tonal; ColorStyle.EXPRESSIVE -> R.string.settings_color_style_expressive; ColorStyle.FIDELITY -> R.string.settings_color_style_fidelity }) @Composable private fun colorStyleLabel(value: ColorStyle) = stringResource(when (value) { ColorStyle.TONAL_SPOT -> R.string.settings_color_style_tonal; ColorStyle.EXPRESSIVE -> R.string.settings_color_style_expressive; ColorStyle.FIDELITY -> R.string.settings_color_style_fidelity })
@Composable private fun colorStandardLabel(value: ColorStandard) = stringResource(when (value) { ColorStandard.SPEC_2021 -> R.string.settings_color_standard_2021; ColorStandard.SPEC_2025 -> R.string.settings_color_standard_2025 }) @Composable private fun colorStandardLabel(value: ColorStandard) = stringResource(when (value) { ColorStandard.SPEC_2021 -> R.string.settings_color_standard_2021; ColorStandard.SPEC_2025 -> R.string.settings_color_standard_2025 })
private fun accentColor(accent: AccentPreference) = when (accent) { AccentPreference.BLUE -> Color(0xFF4F6FA9); AccentPreference.GREEN -> Color(0xFF2F7D57); AccentPreference.AMBER -> Color(0xFFA66700); AccentPreference.ROSE -> Color(0xFFA94C68); AccentPreference.VIOLET -> Color(0xFF7659A8) }
@@ -14,7 +14,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Logout import androidx.compose.material.icons.automirrored.outlined.Logout
import androidx.compose.material.icons.outlined.CloudDone import androidx.compose.material.icons.outlined.CloudDone
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@@ -107,7 +107,7 @@ fun AccountProviderDialog(
} }
AccountSheetAction( AccountSheetAction(
icon = Icons.Outlined.Settings, icon = Icons.Outlined.Palette,
label = stringResource(R.string.account_interface_settings), label = stringResource(R.string.account_interface_settings),
description = stringResource(R.string.account_interface_settings_desc), description = stringResource(R.string.account_interface_settings_desc),
tag = "account_dialog_open_settings", tag = "account_dialog_open_settings",
@@ -265,7 +265,7 @@
<string name="settings_style_section">样式</string> <string name="settings_style_section">样式</string>
<string name="settings_renderer_label">渲染器</string> <string name="settings_renderer_label">渲染器</string>
<string name="settings_renderer_description">选择应用使用的界面渲染方式</string> <string name="settings_renderer_description">选择应用使用的界面渲染方式</string>
<string name="settings_renderer_material3">Material 3</string> <string name="settings_renderer_material3">Material</string>
<string name="settings_renderer_miuix">Miuix</string> <string name="settings_renderer_miuix">Miuix</string>
<string name="settings_theme_section">主题</string> <string name="settings_theme_section">主题</string>
<string name="settings_theme_label">主题模式</string> <string name="settings_theme_label">主题模式</string>
@@ -291,7 +291,7 @@
<string name="settings_theme_entry">主题</string> <string name="settings_theme_entry">主题</string>
<string name="settings_theme_entry_description">配色、明暗、视觉效果与缩放</string> <string name="settings_theme_entry_description">配色、明暗、视觉效果与缩放</string>
<string name="settings_theme_detail_title">主题</string> <string name="settings_theme_detail_title">主题</string>
<string name="settings_renderer_primary">Material 3</string> <string name="settings_renderer_primary">Material</string>
<string name="settings_renderer_secondary">Miuix</string> <string name="settings_renderer_secondary">Miuix</string>
<string name="settings_navigation_floating_description">让底部导航独立悬浮在页面内容上方</string> <string name="settings_navigation_floating_description">让底部导航独立悬浮在页面内容上方</string>
<string name="settings_accent_label">强调色</string> <string name="settings_accent_label">强调色</string>
@@ -344,8 +344,8 @@
<string name="account_current">当前账户</string> <string name="account_current">当前账户</string>
<string name="account_greeting">%1$s,您好</string> <string name="account_greeting">%1$s,您好</string>
<string name="account_connection_method">连接方式</string> <string name="account_connection_method">连接方式</string>
<string name="account_interface_settings">界面设置</string> <string name="account_interface_settings">主题</string>
<string name="account_interface_settings_desc">主题、渲染风格与交互偏好</string> <string name="account_interface_settings_desc">配色、明暗、视觉效果与缩放</string>
<string name="account_logout">退出当前会话</string> <string name="account_logout">退出当前会话</string>
<string name="account_logout_desc">结束当前连接并返回登录流程</string> <string name="account_logout_desc">结束当前连接并返回登录流程</string>
<string name="account_logout_confirm">确定退出当前会话吗?</string> <string name="account_logout_confirm">确定退出当前会话吗?</string>
+4 -4
View File
@@ -265,7 +265,7 @@
<string name="settings_style_section">Style</string> <string name="settings_style_section">Style</string>
<string name="settings_renderer_label">Renderer</string> <string name="settings_renderer_label">Renderer</string>
<string name="settings_renderer_description">Choose the interface renderer used by the app</string> <string name="settings_renderer_description">Choose the interface renderer used by the app</string>
<string name="settings_renderer_material3">Material 3</string> <string name="settings_renderer_material3">Material</string>
<string name="settings_renderer_miuix">Miuix</string> <string name="settings_renderer_miuix">Miuix</string>
<string name="settings_theme_section">Theme</string> <string name="settings_theme_section">Theme</string>
<string name="settings_theme_label">Theme mode</string> <string name="settings_theme_label">Theme mode</string>
@@ -291,7 +291,7 @@
<string name="settings_theme_entry">Theme</string> <string name="settings_theme_entry">Theme</string>
<string name="settings_theme_entry_description">Colors, brightness, effects and scale</string> <string name="settings_theme_entry_description">Colors, brightness, effects and scale</string>
<string name="settings_theme_detail_title">Theme</string> <string name="settings_theme_detail_title">Theme</string>
<string name="settings_renderer_primary">Material 3</string> <string name="settings_renderer_primary">Material</string>
<string name="settings_renderer_secondary">Miuix</string> <string name="settings_renderer_secondary">Miuix</string>
<string name="settings_navigation_floating_description">Use a detached navigation surface above page content</string> <string name="settings_navigation_floating_description">Use a detached navigation surface above page content</string>
<string name="settings_accent_label">Accent color</string> <string name="settings_accent_label">Accent color</string>
@@ -344,8 +344,8 @@
<string name="account_current">Current account</string> <string name="account_current">Current account</string>
<string name="account_greeting">Hello, %1$s</string> <string name="account_greeting">Hello, %1$s</string>
<string name="account_connection_method">Connection method</string> <string name="account_connection_method">Connection method</string>
<string name="account_interface_settings">Interface settings</string> <string name="account_interface_settings">Theme</string>
<string name="account_interface_settings_desc">Theme, renderer and interaction preferences</string> <string name="account_interface_settings_desc">Colors, brightness, effects and scale</string>
<string name="account_logout">Sign out</string> <string name="account_logout">Sign out</string>
<string name="account_logout_desc">End the current connection and return to sign-in</string> <string name="account_logout_desc">End the current connection and return to sign-in</string>
<string name="account_logout_confirm">Sign out of the current session?</string> <string name="account_logout_confirm">Sign out of the current session?</string>
@@ -38,6 +38,7 @@ dependencies {
implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3) implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended) implementation(libs.compose.material.icons.extended)
implementation(libs.androidx.activity.compose)
implementation(libs.miuix.ui.android) implementation(libs.miuix.ui.android)
debugImplementation(libs.compose.ui.tooling) debugImplementation(libs.compose.ui.tooling)
screenshotTestImplementation(libs.compose.screenshot.test) screenshotTestImplementation(libs.compose.screenshot.test)
@@ -1,5 +1,7 @@
package com.flagship.abox.manager.designsystem package com.flagship.abox.manager.designsystem
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -45,6 +47,9 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@@ -677,3 +682,16 @@ fun RowScope.ABoxNavigationBarItem(
), ),
) )
} }
@Composable
fun ABoxBackLayer(
onBack: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable () -> Unit,
) {
BackHandler(enabled = enabled, onBack = onBack)
Box(modifier = modifier.fillMaxSize()) {
content()
}
}
@@ -101,7 +101,7 @@ internal fun ABoxThemeContent(
LocalABoxBlurEnabled provides resolution.blurEnabled, LocalABoxBlurEnabled provides resolution.blurEnabled,
LocalABoxNavigationPresentation provides preferences.navigationPresentation, LocalABoxNavigationPresentation provides preferences.navigationPresentation,
LocalABoxLiquidGlassEnabled provides preferences.liquidGlassEnabled, LocalABoxLiquidGlassEnabled provides preferences.liquidGlassEnabled,
LocalABoxPredictiveBackEnabled provides preferences.predictiveBackEnabled, LocalABoxPredictiveBackEnabled provides false,
) { ) {
renderer.Render( renderer.Render(
resolution = resolution, resolution = resolution,
@@ -2,10 +2,13 @@ package com.flagship.abox.manager.designsystem.tokens
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.flagship.abox.manager.preferences.AppThemeMode import com.flagship.abox.manager.preferences.AppThemeMode
import com.flagship.abox.manager.preferences.AccentPreference import com.flagship.abox.manager.preferences.AccentPreference
import com.flagship.abox.manager.preferences.ColorStandard
import com.flagship.abox.manager.preferences.ColorStyle
import com.flagship.abox.manager.preferences.DesignRenderer import com.flagship.abox.manager.preferences.DesignRenderer
import com.flagship.abox.manager.preferences.UiPreferences import com.flagship.abox.manager.preferences.UiPreferences
@@ -122,7 +125,13 @@ object ABoxThemeResolver {
val colorTokens = if (preferences.dynamicColorEnabled && sdkInt >= 31 && dynamicColorTokens != null) { val colorTokens = if (preferences.dynamicColorEnabled && sdkInt >= 31 && dynamicColorTokens != null) {
dynamicColorTokens dynamicColorTokens
} else { } else {
brandTokens.withAccent(preferences.accentPreference, isDarkTheme) ABoxAccentPalette.resolve(
base = brandTokens,
accent = preferences.accentPreference,
style = preferences.colorStyle,
standard = preferences.colorStandard,
dark = isDarkTheme,
)
} }
val shapeTokens = when (preferences.renderer) { val shapeTokens = when (preferences.renderer) {
DesignRenderer.MATERIAL3 -> ABoxThemeDefaults.material3Shapes DesignRenderer.MATERIAL3 -> ABoxThemeDefaults.material3Shapes
@@ -139,13 +148,86 @@ object ABoxThemeResolver {
} }
} }
private fun ABoxColorTokens.withAccent(accent: AccentPreference, dark: Boolean): ABoxColorTokens { object ABoxAccentPalette {
val selected = when (accent) { fun preview(
AccentPreference.BLUE -> if (dark) Color(0xFF9CCAFF) else Color(0xFF385F9D) accent: AccentPreference,
AccentPreference.GREEN -> if (dark) Color(0xFF7EDFA7) else Color(0xFF247A4B) style: ColorStyle,
AccentPreference.AMBER -> if (dark) Color(0xFFFFC46B) else Color(0xFF8A5800) standard: ColorStandard,
AccentPreference.ROSE -> if (dark) Color(0xFFFFB1C1) else Color(0xFFA43C59) dark: Boolean,
AccentPreference.VIOLET -> if (dark) Color(0xFFD4BBFF) else Color(0xFF6D52A2) ): Color = resolve(
base = if (dark) ABoxThemeDefaults.darkColors else ABoxThemeDefaults.lightColors,
accent = accent,
style = style,
standard = standard,
dark = dark,
).primary
fun resolve(
base: ABoxColorTokens,
accent: AccentPreference,
style: ColorStyle,
standard: ColorStandard,
dark: Boolean,
): ABoxColorTokens {
val source = source(accent, dark)
val primary = when (style) {
ColorStyle.TONAL_SPOT -> lerp(source, base.surface, if (dark) 0.08f else 0.12f)
ColorStyle.EXPRESSIVE -> expressive(accent, dark)
ColorStyle.FIDELITY -> source
}
val deviceSensor = when (style) {
ColorStyle.EXPRESSIVE -> expressiveSensor(accent, dark)
ColorStyle.FIDELITY -> lerp(source, base.success, 0.38f)
ColorStyle.TONAL_SPOT -> base.deviceSensor
}
val deviceInfrared = when (style) {
ColorStyle.EXPRESSIVE -> expressiveInfrared(accent, dark)
ColorStyle.FIDELITY -> lerp(source, base.deviceInfrared, 0.46f)
ColorStyle.TONAL_SPOT -> base.deviceInfrared
}
val styled = base.copy(
primary = primary,
deviceSocket = primary,
deviceSensor = deviceSensor,
deviceInfrared = deviceInfrared,
)
return when (standard) {
ColorStandard.SPEC_2021 -> styled
ColorStandard.SPEC_2025 -> styled.copy(
surface = if (dark) Color(0xFF0C1114) else Color(0xFFFAF9FC),
surfaceContainer = if (dark) Color(0xFF242C30) else Color(0xFFE9E8EE),
outline = if (dark) Color(0xFFA7B2B7) else Color(0xFF596267),
primary = lerp(primary, if (dark) Color.White else Color.Black, 0.08f),
deviceSocket = lerp(primary, if (dark) Color.White else Color.Black, 0.08f),
)
}
}
private fun source(accent: AccentPreference, dark: Boolean): Color = when (accent) {
AccentPreference.BLUE -> if (dark) Color(0xFF9CCAFF) else Color(0xFF4F6FA9)
AccentPreference.GREEN -> if (dark) Color(0xFF7EDFA7) else Color(0xFF2F7D57)
AccentPreference.AMBER -> if (dark) Color(0xFFFFC46B) else Color(0xFFA66700)
AccentPreference.ROSE -> if (dark) Color(0xFFFFB1C1) else Color(0xFFA94C68)
AccentPreference.VIOLET -> if (dark) Color(0xFFD4BBFF) else Color(0xFF7659A8)
}
private fun expressive(accent: AccentPreference, dark: Boolean): Color = when (accent) {
AccentPreference.BLUE -> if (dark) Color(0xFF72D6FF) else Color(0xFF006B91)
AccentPreference.GREEN -> if (dark) Color(0xFF69E6B1) else Color(0xFF007A54)
AccentPreference.AMBER -> if (dark) Color(0xFFFFC24B) else Color(0xFF9A5C00)
AccentPreference.ROSE -> if (dark) Color(0xFFFFA8C4) else Color(0xFFB12F62)
AccentPreference.VIOLET -> if (dark) Color(0xFFC9B5FF) else Color(0xFF7048B8)
}
private fun expressiveSensor(accent: AccentPreference, dark: Boolean): Color = when (accent) {
AccentPreference.BLUE, AccentPreference.VIOLET -> if (dark) Color(0xFF78DFA2) else Color(0xFF26764A)
AccentPreference.GREEN, AccentPreference.AMBER -> if (dark) Color(0xFF79D6F3) else Color(0xFF006985)
AccentPreference.ROSE -> if (dark) Color(0xFFFFC45F) else Color(0xFF8B5B00)
}
private fun expressiveInfrared(accent: AccentPreference, dark: Boolean): Color = when (accent) {
AccentPreference.BLUE, AccentPreference.GREEN -> if (dark) Color(0xFFD1B8FF) else Color(0xFF7453A5)
AccentPreference.AMBER, AccentPreference.ROSE -> if (dark) Color(0xFF9FCBFF) else Color(0xFF38639A)
AccentPreference.VIOLET -> if (dark) Color(0xFFFFB0C7) else Color(0xFFA43E62)
} }
return copy(primary = selected, deviceSocket = selected)
} }
@@ -26,8 +26,26 @@ class ABoxThemeResolverTest {
) )
assertFalse(light.isDarkTheme) assertFalse(light.isDarkTheme)
assertEquals(ABoxThemeDefaults.lightColors, light.colorTokens) assertEquals(
assertEquals(ABoxThemeDefaults.darkColors, dark.colorTokens) ABoxAccentPalette.resolve(
ABoxThemeDefaults.lightColors,
UiPreferences().accentPreference,
UiPreferences().colorStyle,
UiPreferences().colorStandard,
false,
),
light.colorTokens,
)
assertEquals(
ABoxAccentPalette.resolve(
ABoxThemeDefaults.darkColors,
UiPreferences().accentPreference,
UiPreferences().colorStyle,
UiPreferences().colorStandard,
true,
),
dark.colorTokens,
)
} }
@Test @Test
@@ -42,7 +60,16 @@ class ABoxThemeResolverTest {
dynamicColorTokens = dynamicTokens, dynamicColorTokens = dynamicTokens,
) )
assertEquals(ABoxThemeDefaults.lightColors, resolution.colorTokens) assertEquals(
ABoxAccentPalette.resolve(
ABoxThemeDefaults.lightColors,
UiPreferences().accentPreference,
UiPreferences().colorStyle,
UiPreferences().colorStandard,
false,
),
resolution.colorTokens,
)
} }
@Test @Test
@@ -57,7 +84,16 @@ class ABoxThemeResolverTest {
dynamicColorTokens = dynamicTokens, dynamicColorTokens = dynamicTokens,
) )
assertEquals(ABoxThemeDefaults.darkColors, resolution.colorTokens) assertEquals(
ABoxAccentPalette.resolve(
ABoxThemeDefaults.darkColors,
UiPreferences().accentPreference,
UiPreferences().colorStyle,
UiPreferences().colorStandard,
true,
),
resolution.colorTokens,
)
} }
@Test @Test
@@ -50,7 +50,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SwapVert import androidx.compose.material.icons.outlined.SwapVert
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -71,6 +70,7 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.designsystem.ABoxSurface import com.flagship.abox.manager.designsystem.ABoxSurface
import com.flagship.abox.manager.designsystem.ABoxBackLayer
import com.flagship.abox.manager.designsystem.ABoxIconButton import com.flagship.abox.manager.designsystem.ABoxIconButton
import com.flagship.abox.manager.designsystem.ABoxDeviceDetailScaffold import com.flagship.abox.manager.designsystem.ABoxDeviceDetailScaffold
import com.flagship.abox.manager.designsystem.icon.ABoxIcon import com.flagship.abox.manager.designsystem.icon.ABoxIcon
@@ -157,8 +157,6 @@ internal fun InfraredHubScreen(
var showCreateDialog by remember { mutableStateOf(false) } var showCreateDialog by remember { mutableStateOf(false) }
var showManagement by remember { mutableStateOf(false) } var showManagement by remember { mutableStateOf(false) }
BackHandler(enabled = showManagement) { showManagement = false }
LaunchedEffect(state.message) { LaunchedEffect(state.message) {
state.message?.let { msg -> state.message?.let { msg ->
when (msg) { when (msg) {
@@ -188,18 +186,16 @@ internal fun InfraredHubScreen(
ABoxDeviceDetailScaffold( ABoxDeviceDetailScaffold(
title = state.savedDeviceName.value, title = state.savedDeviceName.value,
subtitle = if (showManagement) stringResource(R.string.infrared_settings_subtitle) else stringResource(R.string.infrared_center_subtitle), subtitle = stringResource(R.string.infrared_center_subtitle),
icon = ABoxIconKey.INFRARED, icon = ABoxIconKey.INFRARED,
onBack = if (showManagement) ({ showManagement = false }) else onBack, onBack = onBack,
action = { action = {
if (!showManagement) {
ABoxIconButton( ABoxIconButton(
key = ABoxIconKey.EDIT, key = ABoxIconKey.EDIT,
contentDescription = stringResource(R.string.infrared_manage_action), contentDescription = stringResource(R.string.infrared_manage_action),
onClick = { showManagement = true }, onClick = { showManagement = true },
modifier = Modifier.testTag("hub_settings_button"), modifier = Modifier.testTag("hub_settings_button"),
) )
}
}, },
) { innerPadding -> ) { innerPadding ->
ABoxSurface( ABoxSurface(
@@ -207,16 +203,7 @@ internal fun InfraredHubScreen(
.fillMaxSize() .fillMaxSize()
.padding(innerPadding), .padding(innerPadding),
) { ) {
if (showManagement) { Column(
InfraredManagementScreen(
state = state,
onAddRemote = { showCreateDialog = true },
onDeleteRemote = onDeleteRemote,
onSaveDeviceName = onSaveDeviceName,
onMoveRemote = onMoveRemote,
onUpdateRemote = onUpdateRemote,
)
} else Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 16.dp, vertical = 10.dp), .padding(horizontal = 16.dp, vertical = 10.dp),
@@ -299,6 +286,30 @@ internal fun InfraredHubScreen(
} }
} }
if (showManagement) {
ABoxBackLayer(onBack = { showManagement = false }) {
ABoxDeviceDetailScaffold(
title = state.savedDeviceName.value,
subtitle = stringResource(R.string.infrared_settings_subtitle),
icon = ABoxIconKey.INFRARED,
onBack = { showManagement = false },
) { innerPadding ->
ABoxSurface(
modifier = Modifier.fillMaxSize().padding(innerPadding),
) {
InfraredManagementScreen(
state = state,
onAddRemote = { showCreateDialog = true },
onDeleteRemote = onDeleteRemote,
onSaveDeviceName = onSaveDeviceName,
onMoveRemote = onMoveRemote,
onUpdateRemote = onUpdateRemote,
)
}
}
}
}
if (showCreateDialog) { if (showCreateDialog) {
CreateVirtualRemoteDialog( CreateVirtualRemoteDialog(
onDismiss = { showCreateDialog = false }, onDismiss = { showCreateDialog = false },