feat: redesign account and provider dialog
This commit is contained in:
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.flagship.abox.manager.ui.shell
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithTag
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performClick
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxTheme
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AccountProviderDialogTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun dialogExposesAccountActionsAndClose() {
|
||||||
|
var closed = 0
|
||||||
|
composeRule.setContent {
|
||||||
|
ABoxTheme {
|
||||||
|
AccountProviderDialog(
|
||||||
|
userName = "user",
|
||||||
|
providerName = "ABox",
|
||||||
|
onDismissRequest = { closed++ },
|
||||||
|
onOpenSettings = {},
|
||||||
|
onSwitchProvider = {},
|
||||||
|
onEndSession = {},
|
||||||
|
onForgetCredentials = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRule.onNodeWithText("账户与提供方").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithTag("account_dialog_close").performClick()
|
||||||
|
|
||||||
|
assertEquals(1, closed)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun destructiveActionsRequireSeparateConfirmation() {
|
||||||
|
var ended = 0
|
||||||
|
var forgotten = 0
|
||||||
|
composeRule.setContent {
|
||||||
|
ABoxTheme {
|
||||||
|
AccountProviderDialog(
|
||||||
|
userName = "user",
|
||||||
|
providerName = "ABox",
|
||||||
|
onDismissRequest = {},
|
||||||
|
onOpenSettings = {},
|
||||||
|
onSwitchProvider = {},
|
||||||
|
onEndSession = { ended++ },
|
||||||
|
onForgetCredentials = { forgotten++ },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRule.onNodeWithTag("account_dialog_end_session").performClick()
|
||||||
|
composeRule.onNodeWithText("退出当前会话").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithTag("account_dialog_confirm").performClick()
|
||||||
|
assertEquals(1, ended)
|
||||||
|
assertEquals(0, forgotten)
|
||||||
|
|
||||||
|
composeRule.onNodeWithTag("account_dialog_forget_credentials").performClick()
|
||||||
|
composeRule.onNodeWithText("清除保存的凭据").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithTag("account_dialog_confirm").performClick()
|
||||||
|
assertEquals(1, forgotten)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ 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.compose.material3.ExperimentalMaterial3Api
|
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
|
||||||
@@ -24,12 +24,14 @@ import com.flagship.abox.manager.domain.Username
|
|||||||
import com.flagship.abox.manager.ui.login.LoginRoute
|
import com.flagship.abox.manager.ui.login.LoginRoute
|
||||||
import com.flagship.abox.manager.ui.login.LoginViewModel
|
import com.flagship.abox.manager.ui.login.LoginViewModel
|
||||||
import com.flagship.abox.manager.ui.navigation.AuthenticatedNavigation
|
import com.flagship.abox.manager.ui.navigation.AuthenticatedNavigation
|
||||||
|
import com.flagship.abox.manager.ui.preferences.SettingsRoute
|
||||||
import com.flagship.abox.manager.ui.preferences.SettingsViewModel
|
import com.flagship.abox.manager.ui.preferences.SettingsViewModel
|
||||||
import com.flagship.abox.manager.ui.provider.ProviderSelectionRoute
|
import com.flagship.abox.manager.ui.provider.ProviderSelectionRoute
|
||||||
import com.flagship.abox.manager.ui.session.AppSessionUiState
|
import com.flagship.abox.manager.ui.session.AppSessionUiState
|
||||||
import com.flagship.abox.manager.ui.session.AppSessionViewModel
|
import com.flagship.abox.manager.ui.session.AppSessionViewModel
|
||||||
import com.flagship.abox.manager.ui.session.SessionActionsViewModel
|
import com.flagship.abox.manager.ui.session.SessionActionsViewModel
|
||||||
import com.flagship.abox.manager.ui.shell.AccountProviderBottomSheet
|
import com.flagship.abox.manager.ui.shell.AccountProviderDialog
|
||||||
|
import com.flagship.abox.manager.ui.session.SessionUiAction
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ABoxManagerApp(
|
fun ABoxManagerApp(
|
||||||
@@ -74,42 +76,58 @@ fun ABoxManagerApp(
|
|||||||
is AppSessionUiState.Authenticated -> AuthenticatedNavHost(
|
is AppSessionUiState.Authenticated -> AuthenticatedNavHost(
|
||||||
username = state.username,
|
username = state.username,
|
||||||
profile = state.profile,
|
profile = state.profile,
|
||||||
actionInProgress = actionState.inProgress,
|
|
||||||
errorMessage = actionError,
|
|
||||||
onLogout = sessionActionsViewModel::logout,
|
onLogout = sessionActionsViewModel::logout,
|
||||||
|
onSessionAction = sessionActionsViewModel::dispatch,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AuthenticatedNavHost(
|
private fun AuthenticatedNavHost(
|
||||||
username: Username,
|
username: Username,
|
||||||
profile: ProviderProfile,
|
profile: ProviderProfile,
|
||||||
actionInProgress: Boolean,
|
|
||||||
errorMessage: String?,
|
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
onSessionAction: (SessionUiAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
var showAccountSheet by remember { mutableStateOf(false) }
|
var showAccountDialog by remember { mutableStateOf(false) }
|
||||||
|
var showSettings by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
AuthenticatedNavigation(
|
if (showSettings) {
|
||||||
username = username,
|
BackHandler { showSettings = false }
|
||||||
profile = profile,
|
SettingsRoute()
|
||||||
onLogout = onLogout,
|
} else {
|
||||||
onProviderClick = { showAccountSheet = true },
|
AuthenticatedNavigation(
|
||||||
onAvatarClick = { showAccountSheet = true },
|
username = username,
|
||||||
)
|
profile = profile,
|
||||||
|
onLogout = onLogout,
|
||||||
|
onProviderClick = { showAccountDialog = true },
|
||||||
|
onAvatarClick = { showAccountDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (showAccountSheet) {
|
if (showAccountDialog) {
|
||||||
AccountProviderBottomSheet(
|
AccountProviderDialog(
|
||||||
userName = username.value,
|
userName = username.value,
|
||||||
providerName = profile.displayName,
|
providerName = profile.displayName,
|
||||||
onDismissRequest = { showAccountSheet = false },
|
onDismissRequest = { showAccountDialog = false },
|
||||||
onLogout = onLogout,
|
onOpenSettings = {
|
||||||
actionInProgress = actionInProgress,
|
showAccountDialog = false
|
||||||
errorMessage = errorMessage,
|
showSettings = true
|
||||||
|
},
|
||||||
|
onSwitchProvider = {
|
||||||
|
showAccountDialog = false
|
||||||
|
onSessionAction(SessionUiAction.SwitchProvider)
|
||||||
|
},
|
||||||
|
onEndSession = {
|
||||||
|
showAccountDialog = false
|
||||||
|
onSessionAction(SessionUiAction.EndSession)
|
||||||
|
},
|
||||||
|
onForgetCredentials = {
|
||||||
|
showAccountDialog = false
|
||||||
|
onSessionAction(SessionUiAction.ForgetSavedCredentials)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -42,6 +42,7 @@ import com.flagship.abox.manager.infrared.panel.RemotePanelRoute
|
|||||||
import com.flagship.abox.manager.infrared.ui.InfraredControlRoute
|
import com.flagship.abox.manager.infrared.ui.InfraredControlRoute
|
||||||
import com.flagship.abox.manager.ui.devicecontrollist.DeviceListScreen
|
import com.flagship.abox.manager.ui.devicecontrollist.DeviceListScreen
|
||||||
import com.flagship.abox.manager.ui.scene.SceneListScreen
|
import com.flagship.abox.manager.ui.scene.SceneListScreen
|
||||||
|
import com.flagship.abox.manager.ui.preferences.SettingsRoute
|
||||||
import com.flagship.abox.manager.ui.scene.SceneRoutes
|
import com.flagship.abox.manager.ui.scene.SceneRoutes
|
||||||
import com.flagship.abox.manager.ui.scene.sceneEditorDestination
|
import com.flagship.abox.manager.ui.scene.sceneEditorDestination
|
||||||
import com.flagship.abox.manager.ui.scene.sceneExecutionDestination
|
import com.flagship.abox.manager.ui.scene.sceneExecutionDestination
|
||||||
@@ -221,6 +222,7 @@ fun AuthenticatedNavigation(
|
|||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
onProviderClick: () -> Unit,
|
onProviderClick: () -> Unit,
|
||||||
onAvatarClick: () -> Unit,
|
onAvatarClick: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit = {},
|
||||||
windowSizeClass: ABoxWindowSizeClass? = null,
|
windowSizeClass: ABoxWindowSizeClass? = null,
|
||||||
) {
|
) {
|
||||||
val resolvedWindowSizeClass = windowSizeClass ?: widthClass(LocalConfiguration.current.screenWidthDp)
|
val resolvedWindowSizeClass = windowSizeClass ?: widthClass(LocalConfiguration.current.screenWidthDp)
|
||||||
@@ -262,6 +264,7 @@ fun AuthenticatedNavigation(
|
|||||||
onSelectDevice = onSelectDevice,
|
onSelectDevice = onSelectDevice,
|
||||||
onClearDeviceSelection = onClearDeviceSelection,
|
onClearDeviceSelection = onClearDeviceSelection,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
|
onOpenSettings = onOpenSettings,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -279,6 +282,7 @@ private fun AuthenticatedGraphContent(
|
|||||||
onSelectDevice: (DeviceName, DeviceType) -> Unit,
|
onSelectDevice: (DeviceName, DeviceType) -> Unit,
|
||||||
onClearDeviceSelection: () -> Unit,
|
onClearDeviceSelection: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -294,6 +298,7 @@ private fun AuthenticatedGraphContent(
|
|||||||
onSelectDevice = onSelectDevice,
|
onSelectDevice = onSelectDevice,
|
||||||
onClearDeviceSelection = onClearDeviceSelection,
|
onClearDeviceSelection = onClearDeviceSelection,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
|
onOpenSettings = onOpenSettings,
|
||||||
)
|
)
|
||||||
DeviceDetailPane(
|
DeviceDetailPane(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -309,6 +314,7 @@ private fun AuthenticatedGraphContent(
|
|||||||
onSelectDevice = onSelectDevice,
|
onSelectDevice = onSelectDevice,
|
||||||
onClearDeviceSelection = onClearDeviceSelection,
|
onClearDeviceSelection = onClearDeviceSelection,
|
||||||
onLogout = onLogout,
|
onLogout = onLogout,
|
||||||
|
onOpenSettings = onOpenSettings,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,6 +328,7 @@ private fun AuthenticatedNavHost(
|
|||||||
onSelectDevice: (DeviceName, DeviceType) -> Unit,
|
onSelectDevice: (DeviceName, DeviceType) -> Unit,
|
||||||
onClearDeviceSelection: () -> Unit,
|
onClearDeviceSelection: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -342,6 +349,9 @@ private fun AuthenticatedNavHost(
|
|||||||
composable(MainTab.ACTIVITY.route) {
|
composable(MainTab.ACTIVITY.route) {
|
||||||
ActivityDestination()
|
ActivityDestination()
|
||||||
}
|
}
|
||||||
|
composable(Routes.SETTINGS) {
|
||||||
|
SettingsRoute()
|
||||||
|
}
|
||||||
deviceDetailGraph(
|
deviceDetailGraph(
|
||||||
onBackClick = {
|
onBackClick = {
|
||||||
onClearDeviceSelection()
|
onClearDeviceSelection()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ object Routes {
|
|||||||
const val PROVIDER_SELECTION = "provider_selection"
|
const val PROVIDER_SELECTION = "provider_selection"
|
||||||
const val LOGIN = "login"
|
const val LOGIN = "login"
|
||||||
const val AUTHENTICATED = "authenticated"
|
const val AUTHENTICATED = "authenticated"
|
||||||
|
const val SETTINGS = "settings"
|
||||||
|
|
||||||
const val INFRARED_HUB = "infrared_hub"
|
const val INFRARED_HUB = "infrared_hub"
|
||||||
const val INFRARED_PANEL = "infrared_panel"
|
const val INFRARED_PANEL = "infrared_panel"
|
||||||
|
|||||||
-183
@@ -1,183 +0,0 @@
|
|||||||
package com.flagship.abox.manager.ui.shell
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.automirrored.filled.ExitToApp
|
|
||||||
import androidx.compose.material.icons.filled.CloudDone
|
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.SheetState
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.flagship.abox.manager.R
|
|
||||||
import com.flagship.abox.manager.designsystem.ABoxButton
|
|
||||||
import com.flagship.abox.manager.designsystem.ABoxUserAvatar
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Account and Provider management bottom sheet triggered by clicking the top bar user avatar.
|
|
||||||
* Allows viewing current session info and signing out.
|
|
||||||
*/
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun AccountProviderBottomSheet(
|
|
||||||
userName: String,
|
|
||||||
providerName: String,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onLogout: () -> Unit,
|
|
||||||
actionInProgress: Boolean,
|
|
||||||
errorMessage: String?,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
|
||||||
) {
|
|
||||||
ModalBottomSheet(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
sheetState = sheetState,
|
|
||||||
modifier = modifier,
|
|
||||||
containerColor = MaterialTheme.colorScheme.surface,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.padding(horizontal = 24.dp)
|
|
||||||
.padding(bottom = 32.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
) {
|
|
||||||
// Header with large avatar & user info
|
|
||||||
ABoxUserAvatar(
|
|
||||||
name = userName,
|
|
||||||
onClick = {},
|
|
||||||
size = 56.dp,
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = userName.ifBlank { stringResource(R.string.account_sheet_title) },
|
|
||||||
style = MaterialTheme.typography.titleLarge.copy(fontWeight = FontWeight.Bold),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.account_sheet_signed_in_as),
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(vertical = 4.dp),
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Provider Info Card
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
colors = CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
|
||||||
),
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.CloudDone,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
modifier = Modifier.size(22.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.account_sheet_provider),
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = providerName,
|
|
||||||
style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
|
|
||||||
// Action button
|
|
||||||
ABoxButton(
|
|
||||||
onClick = onLogout,
|
|
||||||
enabled = !actionInProgress,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Filled.ExitToApp,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(8.dp))
|
|
||||||
Text(stringResource(R.string.account_sheet_logout))
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMessage?.let { error ->
|
|
||||||
Text(
|
|
||||||
text = error,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onClick = onDismissRequest,
|
|
||||||
enabled = !actionInProgress,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.account_sheet_close),
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+198
@@ -0,0 +1,198 @@
|
|||||||
|
package com.flagship.abox.manager.ui.shell
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.CloudDone
|
||||||
|
import androidx.compose.material.icons.outlined.Logout
|
||||||
|
import androidx.compose.material.icons.outlined.Refresh
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxButton
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxIconButton
|
||||||
|
import com.flagship.abox.manager.designsystem.ABoxUserAvatar
|
||||||
|
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
|
||||||
|
|
||||||
|
private enum class DestructiveAction {
|
||||||
|
END_SESSION,
|
||||||
|
FORGET_CREDENTIALS,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AccountProviderDialog(
|
||||||
|
userName: String,
|
||||||
|
providerName: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onOpenSettings: () -> Unit,
|
||||||
|
onSwitchProvider: () -> Unit,
|
||||||
|
onEndSession: () -> Unit,
|
||||||
|
onForgetCredentials: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var confirmation by remember { mutableStateOf<DestructiveAction?>(null) }
|
||||||
|
|
||||||
|
if (confirmation == null) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier.testTag("account_dialog"),
|
||||||
|
title = {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text("账户与提供方")
|
||||||
|
ABoxIconButton(
|
||||||
|
key = ABoxIconKey.CLOSE,
|
||||||
|
contentDescription = "关闭账户对话框",
|
||||||
|
onClick = onDismissRequest,
|
||||||
|
modifier = Modifier.testTag("account_dialog_close"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ABoxUserAvatar(name = userName, onClick = {}, size = 64.dp)
|
||||||
|
Text(
|
||||||
|
text = userName.ifBlank { "当前账户" },
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = providerName,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.CloudDone,
|
||||||
|
contentDescription = "连接方式",
|
||||||
|
modifier = Modifier.size(24.dp),
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text("当前连接方式", style = MaterialTheme.typography.labelMedium)
|
||||||
|
Text(providerName, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = onSwitchProvider,
|
||||||
|
modifier = Modifier.testTag("account_dialog_switch_provider"),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Outlined.Refresh, "切换提供方")
|
||||||
|
Text("切换")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AccountActionButton(
|
||||||
|
icon = Icons.Outlined.Settings,
|
||||||
|
label = "界面设置",
|
||||||
|
description = "调整主题、渲染器和交互偏好",
|
||||||
|
tag = "account_dialog_open_settings",
|
||||||
|
onClick = onOpenSettings,
|
||||||
|
)
|
||||||
|
AccountActionButton(
|
||||||
|
icon = Icons.Outlined.Logout,
|
||||||
|
label = "退出当前会话",
|
||||||
|
description = "结束当前提供方会话",
|
||||||
|
tag = "account_dialog_end_session",
|
||||||
|
onClick = { confirmation = DestructiveAction.END_SESSION },
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
onClick = { confirmation = DestructiveAction.FORGET_CREDENTIALS },
|
||||||
|
modifier = Modifier.fillMaxWidth().testTag("account_dialog_forget_credentials"),
|
||||||
|
) {
|
||||||
|
Text("清除保存的凭据", color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val action = confirmation ?: return
|
||||||
|
val isForget = action == DestructiveAction.FORGET_CREDENTIALS
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { confirmation = null },
|
||||||
|
title = { Text(if (isForget) "清除保存的凭据" else "退出当前会话") },
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
if (isForget) {
|
||||||
|
"确定清除当前提供方保存的登录凭据吗?"
|
||||||
|
} else {
|
||||||
|
"确定退出当前会话吗?"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
ABoxButton(
|
||||||
|
onClick = {
|
||||||
|
confirmation = null
|
||||||
|
if (isForget) onForgetCredentials() else onEndSession()
|
||||||
|
},
|
||||||
|
modifier = Modifier.testTag("account_dialog_confirm"),
|
||||||
|
) {
|
||||||
|
Text("确定")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { confirmation = null }) { Text("取消") }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AccountActionButton(
|
||||||
|
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||||
|
label: String,
|
||||||
|
description: String,
|
||||||
|
tag: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.fillMaxWidth().testTag(tag),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Icon(icon, contentDescription = label)
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(label)
|
||||||
|
Text(description, style = MaterialTheme.typography.bodySmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user