fix(ui): stabilize theme and navigation behavior
This commit is contained in:
-2
@@ -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)
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
+46
-78
@@ -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
|
||||||
if (savedSelection != selection) {
|
|
||||||
savedSelection = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
when (detailPresentation(windowSizeClass, selection)) {
|
when (detailPresentation(windowSizeClass, selection)) {
|
||||||
DetailPresentation.PANE -> {
|
DetailPresentation.PANE -> {
|
||||||
|
if (savedSelection != selection) {
|
||||||
|
savedSelection = selection
|
||||||
|
}
|
||||||
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,60 +186,25 @@ fun AuthenticatedNavigationStateHost(
|
|||||||
selectedDevice = selectedDevice,
|
selectedDevice = selectedDevice,
|
||||||
detailPresentation = selectedPresentation,
|
detailPresentation = selectedPresentation,
|
||||||
)
|
)
|
||||||
val canNavigateBack = selectedDevice != null || currentRoute != MainTab.DEVICES.route
|
content(
|
||||||
val predictiveBackEnabled = LocalABoxPredictiveBackEnabled.current &&
|
snapshot,
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
{ tab ->
|
||||||
var backProgress by remember { mutableFloatStateOf(0f) }
|
savedSelection = null
|
||||||
|
if (tab == currentTab) {
|
||||||
Box(
|
navController.popBackStack(tab.route, inclusive = false)
|
||||||
modifier = Modifier.fillMaxSize()
|
} else {
|
||||||
.clip(RoundedCornerShape((28f * backProgress).dp))
|
navController.navigate(tab.route) {
|
||||||
.graphicsLayer {
|
launchSingleTop = true
|
||||||
val scale = 1f - 0.04f * backProgress
|
restoreState = true
|
||||||
scaleX = scale
|
popUpTo(navController.graph.startDestinationId) {
|
||||||
scaleY = scale
|
saveState = true
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
{ deviceName, type ->
|
},
|
||||||
savedSelection = DeviceSelection(profileId, deviceName, type)
|
{ 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+66
-44
@@ -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,22 +116,29 @@ 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(
|
||||||
}
|
state = state,
|
||||||
when (destination) {
|
onOpenTheme = { experienceState.showTheme(returnToHome = true) },
|
||||||
SettingsDestination.HOME -> SettingsHomeScreen(
|
onBack = onExit,
|
||||||
state = state,
|
onAction = onAction,
|
||||||
onOpenTheme = { experienceState.destination = SettingsDestination.THEME.name },
|
)
|
||||||
onBack = onExit,
|
}
|
||||||
onAction = onAction,
|
if (destination == SettingsDestination.THEME) {
|
||||||
)
|
val leaveTheme = {
|
||||||
SettingsDestination.THEME -> ThemeSettingsScreen(
|
if (experienceState.themeBackReturnsHome) experienceState.showHome() else onExit()
|
||||||
state = state,
|
}
|
||||||
onBack = { experienceState.destination = SettingsDestination.HOME.name },
|
ABoxBackLayer(onBack = leaveTheme) {
|
||||||
onAction = onAction,
|
ABoxSurface(modifier = Modifier.fillMaxSize()) {
|
||||||
)
|
ThemeSettingsScreen(
|
||||||
|
state = state,
|
||||||
|
onBack = leaveTheme,
|
||||||
|
onAction = onAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) }
|
|
||||||
|
|||||||
+2
-2
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+18
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -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,
|
||||||
|
|||||||
+91
-9
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+40
-4
@@ -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
|
||||||
|
|||||||
+34
-23
@@ -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 },
|
||||||
|
|||||||
Reference in New Issue
Block a user