diff --git a/android/app/src/androidTest/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreenTest.kt b/android/app/src/androidTest/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreenTest.kt
index 114d3eb..8a5d630 100644
--- a/android/app/src/androidTest/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreenTest.kt
@@ -57,7 +57,6 @@ class SettingsScreenTest {
"settings-theme-mode",
"settings-color-style",
"settings-color-standard",
- "settings-predictive-back",
"settings-interface-scale",
)
composeRule.onAllNodesWithTag("settings-renderer-selector").assertCountEquals(0)
@@ -74,7 +73,6 @@ class SettingsScreenTest {
"settings-blur",
"settings-floating-navigation",
"settings-liquid-glass",
- "settings-predictive-back",
"settings-interface-scale",
)
composeRule.onAllNodesWithTag("settings-renderer-selector").assertCountEquals(0)
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index c934de3..508eb01 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,7 +3,7 @@
Unit,
onShowSettingsChange: (Boolean) -> Unit,
) {
- if (showSettings) {
- BackHandler { onShowSettingsChange(false) }
- SettingsRoute(
- experienceState = settingsExperienceState,
- onExit = { onShowSettingsChange(false) },
- )
- } else {
+ Box(Modifier.fillMaxSize()) {
AuthenticatedNavigation(
username = username,
profile = profile,
@@ -122,6 +116,17 @@ private fun AuthenticatedNavHost(
onProviderClick = { onShowAccountDialogChange(true) },
onAvatarClick = { onShowAccountDialogChange(true) },
)
+ if (showSettings) {
+ ABoxBackLayer(
+ onBack = { onShowSettingsChange(false) },
+ enabled = settingsExperienceState.isHome,
+ ) {
+ SettingsRoute(
+ experienceState = settingsExperienceState,
+ onExit = { onShowSettingsChange(false) },
+ )
+ }
+ }
}
if (showAccountDialog) {
@@ -131,6 +136,7 @@ private fun AuthenticatedNavHost(
onDismissRequest = { onShowAccountDialogChange(false) },
onOpenSettings = {
onShowAccountDialogChange(false)
+ settingsExperienceState.showTheme(returnToHome = false)
onShowSettingsChange(true)
},
onSwitchProvider = {
diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/navigation/AuthenticatedNavigation.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/navigation/AuthenticatedNavigation.kt
index 2f6d5db..c32b8f3 100644
--- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/navigation/AuthenticatedNavigation.kt
+++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/navigation/AuthenticatedNavigation.kt
@@ -1,15 +1,11 @@
package com.flagship.abox.manager.ui.navigation
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -17,14 +13,11 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.unit.dp
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.detailPresentation
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.SocketUiEvent
import com.flagship.abox.manager.ui.socket.SocketViewModel
@@ -169,12 +159,11 @@ fun AuthenticatedNavigationStateHost(
}
val selection = selectedDevice ?: return@LaunchedEffect
- if (savedSelection != selection) {
- savedSelection = selection
- }
-
when (detailPresentation(windowSizeClass, selection)) {
DetailPresentation.PANE -> {
+ if (savedSelection != selection) {
+ savedSelection = selection
+ }
if (currentRoute != MainTab.DEVICES.route) {
navController.popBackStack(MainTab.DEVICES.route, inclusive = false)
}
@@ -184,6 +173,9 @@ fun AuthenticatedNavigationStateHost(
navController.navigate(deviceRoute(selection)) {
launchSingleTop = true
}
+ savedSelection = null
+ } else if (savedSelection != null) {
+ savedSelection = null
}
}
}
@@ -194,60 +186,25 @@ fun AuthenticatedNavigationStateHost(
selectedDevice = selectedDevice,
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(
- snapshot,
- { tab ->
- savedSelection = null
- if (tab == currentTab) {
- navController.popBackStack(tab.route, inclusive = false)
- } else {
- navController.navigate(tab.route) {
- launchSingleTop = true
- restoreState = true
- popUpTo(navController.graph.startDestinationId) {
- saveState = true
- }
+ content(
+ snapshot,
+ { tab ->
+ savedSelection = null
+ if (tab == currentTab) {
+ navController.popBackStack(tab.route, inclusive = false)
+ } else {
+ navController.navigate(tab.route) {
+ launchSingleTop = true
+ restoreState = true
+ popUpTo(navController.graph.startDestinationId) {
+ saveState = true
}
}
- },
- { deviceName, type ->
- savedSelection = DeviceSelection(profileId, deviceName, type)
- },
- { 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()
- }
+ }
+ },
+ { deviceName, type -> savedSelection = DeviceSelection(profileId, deviceName, type) },
+ { savedSelection = null },
+ )
}
@Composable
@@ -355,6 +312,23 @@ private fun AuthenticatedNavHost(
onLogout: () -> 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(
navController = navController,
startDestination = MainTab.DEVICES.route,
@@ -427,14 +401,14 @@ private fun AuthenticatedNavHost(
deviceDetailGraph(
onBackClick = {
onClearDeviceSelection()
- navController.popBackStack()
+ navigateBackFromDetail()
},
)
sceneEditorDestination(
- onBack = navController::popBackStack,
+ onBack = navigateBackFromDetail,
onSaved = { navController.popBackStack(MainTab.SCENES.route, inclusive = false) },
)
- sceneExecutionDestination(onBack = navController::popBackStack)
+ sceneExecutionDestination(onBack = navigateBackFromDetail)
composable(
route = Routes.INFRARED_HUB,
arguments = listOf(
@@ -445,7 +419,7 @@ private fun AuthenticatedNavHost(
InfraredHubRoute(
onBack = {
onClearDeviceSelection()
- navController.popBackStack()
+ navigateBackFromDetail()
},
onOpenRemote = { remoteId ->
navController.navigate(
@@ -467,7 +441,7 @@ private fun AuthenticatedNavHost(
),
) {
RemotePanelRoute(
- onBack = navController::popBackStack,
+ onBack = navigateBackFromDetail,
onSessionInvalid = onLogout,
)
}
@@ -479,7 +453,7 @@ private fun AuthenticatedNavHost(
),
) {
InfraredControlRoute(
- onBack = navController::popBackStack,
+ onBack = navigateBackFromDetail,
onSessionInvalid = onLogout,
)
}
@@ -561,9 +535,3 @@ private fun NavBackStackEntry.actualRoute(): String? {
}
return route
}
-
-private fun NavHostController.popCurrentDestinationUnlessRoot() {
- if (currentBackStackEntry?.actualRoute() != MainTab.DEVICES.route) {
- popBackStack()
- }
-}
diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreen.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreen.kt
index be914b8..415439d 100644
--- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreen.kt
+++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/preferences/SettingsScreen.kt
@@ -1,6 +1,5 @@
package com.flagship.abox.manager.ui.preferences
-import androidx.activity.compose.BackHandler
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.ABoxIconKey
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.DesignRenderer
import com.flagship.abox.manager.preferences.InterfaceScale
@@ -70,11 +72,27 @@ private enum class SettingsDestination { HOME, THEME }
@Stable
class SettingsExperienceState(initialDestination: String = SettingsDestination.HOME.name) {
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 {
val Saver: Saver = Saver(
- save = { it.destination },
- restore = { SettingsExperienceState(it) },
+ save = { "${it.destination}|${it.themeBackReturnsHome}" },
+ restore = { saved ->
+ val parts = saved.split('|', limit = 2)
+ SettingsExperienceState(parts.first()).apply {
+ themeBackReturnsHome = parts.getOrNull(1)?.toBooleanStrictOrNull() ?: true
+ }
+ },
)
}
}
@@ -98,22 +116,29 @@ fun SettingsExperience(
) {
val destination = runCatching { SettingsDestination.valueOf(experienceState.destination) }
.getOrDefault(SettingsDestination.HOME)
- BackHandler {
- if (destination == SettingsDestination.THEME) experienceState.destination = SettingsDestination.HOME.name
- else onExit()
- }
- when (destination) {
- SettingsDestination.HOME -> SettingsHomeScreen(
- state = state,
- onOpenTheme = { experienceState.destination = SettingsDestination.THEME.name },
- onBack = onExit,
- onAction = onAction,
- )
- SettingsDestination.THEME -> ThemeSettingsScreen(
- state = state,
- onBack = { experienceState.destination = SettingsDestination.HOME.name },
- onAction = onAction,
- )
+ Box(Modifier.fillMaxSize()) {
+ ABoxSurface(modifier = Modifier.fillMaxSize()) {
+ SettingsHomeScreen(
+ state = state,
+ onOpenTheme = { experienceState.showTheme(returnToHome = true) },
+ onBack = onExit,
+ onAction = onAction,
+ )
+ }
+ if (destination == SettingsDestination.THEME) {
+ val leaveTheme = {
+ if (experienceState.themeBackReturnsHome) experienceState.showHome() else onExit()
+ }
+ ABoxBackLayer(onBack = leaveTheme) {
+ ABoxSurface(modifier = Modifier.fillMaxSize()) {
+ ThemeSettingsScreen(
+ state = state,
+ onBack = leaveTheme,
+ onAction = onAction,
+ )
+ }
+ }
+ }
}
}
@@ -218,6 +243,7 @@ private fun ThemeSettingsScreen(
@Composable
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")) {
SettingsSwitchRow(
icon = ABoxIconKey.THEME,
@@ -238,6 +264,7 @@ private fun ColumnScope.MaterialThemeOptions(state: SettingsUiState, onAction: (
values = ColorStyle.entries,
label = { colorStyleLabel(it) },
onSelected = { onAction(SettingsUiAction.SetColorStyle(it)) },
+ enabled = manualColorsEnabled,
)
EnumSettingRow(
modifier = Modifier.testTag("settings-color-standard"),
@@ -247,8 +274,8 @@ private fun ColumnScope.MaterialThemeOptions(state: SettingsUiState, onAction: (
values = ColorStandard.entries,
label = { colorStandardLabel(it) },
onSelected = { onAction(SettingsUiAction.SetColorStandard(it)) },
+ enabled = manualColorsEnabled,
)
- PredictiveBackSetting(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)) }
}
- PredictiveBackSetting(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
private fun SettingsPage(
title: String,
@@ -448,15 +458,17 @@ private fun EnumSettingRow(
values: List,
label: @Composable (T) -> String,
onSelected: (T) -> Unit,
+ enabled: Boolean = true,
) {
var expanded by remember { mutableStateOf(false) }
SettingsGroup(modifier = modifier) {
- Box(Modifier.fillMaxWidth()) {
+ Box(Modifier.fillMaxWidth().alpha(if (enabled) 1f else 0.42f)) {
SettingsLinkRow(
icon = icon,
title = title,
description = stringResource(R.string.settings_tap_to_choose),
value = value,
+ enabled = enabled,
onClick = { expanded = true },
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
@@ -478,18 +490,29 @@ private fun EnumSettingRow(
@Composable
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)) {
Text(stringResource(R.string.settings_accent_label), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
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,
) {
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(
modifier = Modifier.size(44.dp).background(color, CircleShape)
.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,
) {
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
-private fun SettingsLinkRow(icon: ABoxIconKey, title: String, description: String, value: String?, modifier: Modifier = Modifier, onClick: () -> Unit) {
- Row(modifier.fillMaxWidth().clickable(onClick = onClick).padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
+private fun SettingsLinkRow(icon: ABoxIconKey, title: String, description: String, value: String?, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) {
+ Row(modifier.fillMaxWidth().clickable(enabled = enabled, onClick = onClick).padding(14.dp), verticalAlignment = Alignment.CenterVertically) {
SettingsRowHeader(icon, title, description, Modifier.weight(1f))
if (value != null) Text(value, color = 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 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 })
-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) }
diff --git a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/shell/AccountProviderDialog.kt b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/shell/AccountProviderDialog.kt
index 45de938..d792c1f 100644
--- a/android/app/src/main/kotlin/com/flagship/abox/manager/ui/shell/AccountProviderDialog.kt
+++ b/android/app/src/main/kotlin/com/flagship/abox/manager/ui/shell/AccountProviderDialog.kt
@@ -14,7 +14,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Logout
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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
@@ -107,7 +107,7 @@ fun AccountProviderDialog(
}
AccountSheetAction(
- icon = Icons.Outlined.Settings,
+ icon = Icons.Outlined.Palette,
label = stringResource(R.string.account_interface_settings),
description = stringResource(R.string.account_interface_settings_desc),
tag = "account_dialog_open_settings",
diff --git a/android/app/src/main/res/values-zh-rCN/strings.xml b/android/app/src/main/res/values-zh-rCN/strings.xml
index 22d18d4..adcff17 100644
--- a/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -265,7 +265,7 @@
样式
渲染器
选择应用使用的界面渲染方式
- Material 3
+ Material
Miuix
主题
主题模式
@@ -291,7 +291,7 @@
主题
配色、明暗、视觉效果与缩放
主题
- Material 3
+ Material
Miuix
让底部导航独立悬浮在页面内容上方
强调色
@@ -344,8 +344,8 @@
当前账户
%1$s,您好
连接方式
- 界面设置
- 主题、渲染风格与交互偏好
+ 主题
+ 配色、明暗、视觉效果与缩放
退出当前会话
结束当前连接并返回登录流程
确定退出当前会话吗?
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index a1216ed..677acab 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -265,7 +265,7 @@
Style
Renderer
Choose the interface renderer used by the app
- Material 3
+ Material
Miuix
Theme
Theme mode
@@ -291,7 +291,7 @@
Theme
Colors, brightness, effects and scale
Theme
- Material 3
+ Material
Miuix
Use a detached navigation surface above page content
Accent color
@@ -344,8 +344,8 @@
Current account
Hello, %1$s
Connection method
- Interface settings
- Theme, renderer and interaction preferences
+ Theme
+ Colors, brightness, effects and scale
Sign out
End the current connection and return to sign-in
Sign out of the current session?
diff --git a/android/core/designsystem/build.gradle.kts b/android/core/designsystem/build.gradle.kts
index 7b06b9b..8f3340d 100644
--- a/android/core/designsystem/build.gradle.kts
+++ b/android/core/designsystem/build.gradle.kts
@@ -38,6 +38,7 @@ dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.extended)
+ implementation(libs.androidx.activity.compose)
implementation(libs.miuix.ui.android)
debugImplementation(libs.compose.ui.tooling)
screenshotTestImplementation(libs.compose.screenshot.test)
diff --git a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt
index 9b38645..f8dda1d 100644
--- a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt
+++ b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Components.kt
@@ -1,5 +1,7 @@
package com.flagship.abox.manager.designsystem
+import androidx.activity.compose.BackHandler
+
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@@ -45,6 +47,9 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.Modifier
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()
+ }
+}
diff --git a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt
index 0a1b683..e8e9518 100644
--- a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt
+++ b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/Theme.kt
@@ -101,7 +101,7 @@ internal fun ABoxThemeContent(
LocalABoxBlurEnabled provides resolution.blurEnabled,
LocalABoxNavigationPresentation provides preferences.navigationPresentation,
LocalABoxLiquidGlassEnabled provides preferences.liquidGlassEnabled,
- LocalABoxPredictiveBackEnabled provides preferences.predictiveBackEnabled,
+ LocalABoxPredictiveBackEnabled provides false,
) {
renderer.Render(
resolution = resolution,
diff --git a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxTokens.kt b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxTokens.kt
index a21e57f..8b7d73c 100644
--- a/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxTokens.kt
+++ b/android/core/designsystem/src/main/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxTokens.kt
@@ -2,10 +2,13 @@ package com.flagship.abox.manager.designsystem.tokens
import androidx.compose.runtime.Immutable
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 com.flagship.abox.manager.preferences.AppThemeMode
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.UiPreferences
@@ -122,7 +125,13 @@ object ABoxThemeResolver {
val colorTokens = if (preferences.dynamicColorEnabled && sdkInt >= 31 && dynamicColorTokens != null) {
dynamicColorTokens
} 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) {
DesignRenderer.MATERIAL3 -> ABoxThemeDefaults.material3Shapes
@@ -139,13 +148,86 @@ object ABoxThemeResolver {
}
}
-private fun ABoxColorTokens.withAccent(accent: AccentPreference, dark: Boolean): ABoxColorTokens {
- val selected = when (accent) {
- AccentPreference.BLUE -> if (dark) Color(0xFF9CCAFF) else Color(0xFF385F9D)
- AccentPreference.GREEN -> if (dark) Color(0xFF7EDFA7) else Color(0xFF247A4B)
- AccentPreference.AMBER -> if (dark) Color(0xFFFFC46B) else Color(0xFF8A5800)
- AccentPreference.ROSE -> if (dark) Color(0xFFFFB1C1) else Color(0xFFA43C59)
- AccentPreference.VIOLET -> if (dark) Color(0xFFD4BBFF) else Color(0xFF6D52A2)
+object ABoxAccentPalette {
+ fun preview(
+ accent: AccentPreference,
+ style: ColorStyle,
+ standard: ColorStandard,
+ dark: Boolean,
+ ): 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)
}
diff --git a/android/core/designsystem/src/test/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxThemeResolverTest.kt b/android/core/designsystem/src/test/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxThemeResolverTest.kt
index e0a23ce..53ee0d6 100644
--- a/android/core/designsystem/src/test/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxThemeResolverTest.kt
+++ b/android/core/designsystem/src/test/kotlin/com/flagship/abox/manager/designsystem/tokens/ABoxThemeResolverTest.kt
@@ -26,8 +26,26 @@ class ABoxThemeResolverTest {
)
assertFalse(light.isDarkTheme)
- assertEquals(ABoxThemeDefaults.lightColors, light.colorTokens)
- assertEquals(ABoxThemeDefaults.darkColors, dark.colorTokens)
+ assertEquals(
+ 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
@@ -42,7 +60,16 @@ class ABoxThemeResolverTest {
dynamicColorTokens = dynamicTokens,
)
- assertEquals(ABoxThemeDefaults.lightColors, resolution.colorTokens)
+ assertEquals(
+ ABoxAccentPalette.resolve(
+ ABoxThemeDefaults.lightColors,
+ UiPreferences().accentPreference,
+ UiPreferences().colorStyle,
+ UiPreferences().colorStandard,
+ false,
+ ),
+ resolution.colorTokens,
+ )
}
@Test
@@ -57,7 +84,16 @@ class ABoxThemeResolverTest {
dynamicColorTokens = dynamicTokens,
)
- assertEquals(ABoxThemeDefaults.darkColors, resolution.colorTokens)
+ assertEquals(
+ ABoxAccentPalette.resolve(
+ ABoxThemeDefaults.darkColors,
+ UiPreferences().accentPreference,
+ UiPreferences().colorStyle,
+ UiPreferences().colorStandard,
+ true,
+ ),
+ resolution.colorTokens,
+ )
}
@Test
diff --git a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt
index ef9d92f..71a9957 100644
--- a/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt
+++ b/android/feature/infrared/src/main/kotlin/com/flagship/abox/manager/infrared/hub/InfraredHubScreen.kt
@@ -50,7 +50,6 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.SwapVert
-import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -71,6 +70,7 @@ import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.ABoxDeviceDetailScaffold
import com.flagship.abox.manager.designsystem.icon.ABoxIcon
@@ -157,8 +157,6 @@ internal fun InfraredHubScreen(
var showCreateDialog by remember { mutableStateOf(false) }
var showManagement by remember { mutableStateOf(false) }
- BackHandler(enabled = showManagement) { showManagement = false }
-
LaunchedEffect(state.message) {
state.message?.let { msg ->
when (msg) {
@@ -188,18 +186,16 @@ internal fun InfraredHubScreen(
ABoxDeviceDetailScaffold(
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,
- onBack = if (showManagement) ({ showManagement = false }) else onBack,
+ onBack = onBack,
action = {
- if (!showManagement) {
- ABoxIconButton(
- key = ABoxIconKey.EDIT,
- contentDescription = stringResource(R.string.infrared_manage_action),
- onClick = { showManagement = true },
- modifier = Modifier.testTag("hub_settings_button"),
- )
- }
+ ABoxIconButton(
+ key = ABoxIconKey.EDIT,
+ contentDescription = stringResource(R.string.infrared_manage_action),
+ onClick = { showManagement = true },
+ modifier = Modifier.testTag("hub_settings_button"),
+ )
},
) { innerPadding ->
ABoxSurface(
@@ -207,16 +203,7 @@ internal fun InfraredHubScreen(
.fillMaxSize()
.padding(innerPadding),
) {
- if (showManagement) {
- InfraredManagementScreen(
- state = state,
- onAddRemote = { showCreateDialog = true },
- onDeleteRemote = onDeleteRemote,
- onSaveDeviceName = onSaveDeviceName,
- onMoveRemote = onMoveRemote,
- onUpdateRemote = onUpdateRemote,
- )
- } else Column(
+ Column(
modifier = Modifier
.fillMaxSize()
.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) {
CreateVirtualRemoteDialog(
onDismiss = { showCreateDialog = false },