feat: support 1-100 numeric infrared key slots and gateway login entry
This commit is contained in:
@@ -24,7 +24,9 @@ import javax.inject.Singleton
|
||||
object FoundationModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSessionGateway(): SessionGateway = FakeSessionGateway()
|
||||
fun provideSessionGateway(
|
||||
@OptIn(CleartextAbsdkTransport::class) real: AbsdkBridge,
|
||||
): SessionGateway = real
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
@@ -2,9 +2,19 @@ package com.flagship.abox.manager.ui
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -14,8 +24,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
@@ -84,34 +98,139 @@ private fun FoundationScreen(
|
||||
sessionInvalidMessage: String?,
|
||||
onContinue: () -> Unit,
|
||||
onOpenInfrared: () -> Unit,
|
||||
viewModel: FoundationViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(32.dp),
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
MessageScreen(
|
||||
title = stringResource(R.string.foundation_title),
|
||||
description = sessionInvalidMessage ?: stringResource(R.string.foundation_description),
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.foundation_title),
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
ABoxButton(
|
||||
onClick = onContinue,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp),
|
||||
Text(
|
||||
text = sessionInvalidMessage ?: stringResource(R.string.foundation_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = if (sessionInvalidMessage != null) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
|
||||
// 临时网关登录卡片
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f),
|
||||
),
|
||||
) {
|
||||
Text(stringResource(R.string.continue_action))
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = "ABox 网关登录 (Real 模式前置)",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
if (state.session != null) {
|
||||
OutlinedButton(
|
||||
onClick = viewModel::logout,
|
||||
enabled = !state.isLoading,
|
||||
) {
|
||||
Text("退出")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.session == null) {
|
||||
OutlinedTextField(
|
||||
value = state.usernameInput,
|
||||
onValueChange = viewModel::onUsernameChanged,
|
||||
label = { Text("账号 (默认 a)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = state.passwordInput,
|
||||
onValueChange = viewModel::onPasswordChanged,
|
||||
label = { Text("密码 (默认 a)") },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
ABoxButton(
|
||||
onClick = viewModel::login,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = !state.isLoading,
|
||||
) {
|
||||
if (state.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
} else {
|
||||
Text("连接并登录网关")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "✅ 已登录 ABox (账号: ${state.session?.username?.value})",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
Text(
|
||||
text = "网关会话已建立,现在可以在红外控制中使用真实 ABSDK 模式。",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
|
||||
state.errorMessage?.let { error ->
|
||||
Text(
|
||||
text = "⚠️ $error",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
state.successMessage?.let { success ->
|
||||
Text(
|
||||
text = "ℹ️ $success",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
ABoxButton(
|
||||
onClick = onOpenInfrared,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.infrared_control_action))
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = onContinue,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Text(stringResource(R.string.continue_action))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,12 +246,12 @@ private fun MessageScreen(
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.headlineMedium,
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
Text(
|
||||
text = description,
|
||||
style = androidx.compose.material3.MaterialTheme.typography.bodyLarge,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.flagship.abox.manager.ui
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.flagship.abox.manager.domain.DomainError
|
||||
import com.flagship.abox.manager.domain.DomainResult
|
||||
import com.flagship.abox.manager.domain.LoginCredentials
|
||||
import com.flagship.abox.manager.domain.Password
|
||||
import com.flagship.abox.manager.domain.ProviderProfileId
|
||||
import com.flagship.abox.manager.domain.Session
|
||||
import com.flagship.abox.manager.domain.SessionGateway
|
||||
import com.flagship.abox.manager.domain.Username
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class FoundationUiState(
|
||||
val usernameInput: String = "a",
|
||||
val passwordInput: String = "a",
|
||||
val isLoading: Boolean = false,
|
||||
val session: Session? = null,
|
||||
val errorMessage: String? = null,
|
||||
val successMessage: String? = null,
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class FoundationViewModel @Inject constructor(
|
||||
private val sessionGateway: SessionGateway,
|
||||
) : ViewModel() {
|
||||
|
||||
private val profileId = ProviderProfileId("abox")
|
||||
private val _uiState = MutableStateFlow(FoundationUiState())
|
||||
val uiState: StateFlow<FoundationUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
sessionGateway.observeSession(profileId).collect { session ->
|
||||
_uiState.value = _uiState.value.copy(session = session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onUsernameChanged(value: String) {
|
||||
_uiState.value = _uiState.value.copy(usernameInput = value, errorMessage = null)
|
||||
}
|
||||
|
||||
fun onPasswordChanged(value: String) {
|
||||
_uiState.value = _uiState.value.copy(passwordInput = value, errorMessage = null)
|
||||
}
|
||||
|
||||
fun login() {
|
||||
val username = _uiState.value.usernameInput.trim()
|
||||
val password = _uiState.value.passwordInput.trim()
|
||||
if (username.isBlank() || password.isBlank()) {
|
||||
_uiState.value = _uiState.value.copy(errorMessage = "用户名和密码不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null, successMessage = null)
|
||||
|
||||
viewModelScope.launch {
|
||||
val result = sessionGateway.login(
|
||||
profileId = profileId,
|
||||
credentials = LoginCredentials(Username(username), Password(password)),
|
||||
)
|
||||
when (result) {
|
||||
is DomainResult.Success -> {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
session = result.value,
|
||||
successMessage = "登录成功!已获取 ABox 会话 Token",
|
||||
)
|
||||
}
|
||||
is DomainResult.Failure -> {
|
||||
val detail = when (val error = result.error) {
|
||||
is DomainError.Authentication -> "认证失败 (错误码: ${error.externalCode ?: "20004"}),请检查账号密码"
|
||||
is DomainError.Network -> "网络连接失败: ${error.detail ?: "请确认已连接网关 WiFi"}"
|
||||
is DomainError.TokenInvalid -> "Token 无效"
|
||||
else -> error.detail ?: "登录失败 (${error.externalCode ?: "unknown"})"
|
||||
}
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = detail,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
sessionGateway.logout(profileId)
|
||||
_uiState.value = _uiState.value.copy(session = null, successMessage = "已退出登录")
|
||||
}
|
||||
}
|
||||
|
||||
fun clearMessage() {
|
||||
_uiState.value = _uiState.value.copy(errorMessage = null, successMessage = null)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user