feat(ui): complete ABox app experience redesign #10

Merged
KremeCN merged 40 commits from feat/ui-ux-redesign into main 2026-08-27 23:25:08 +08:00
4 changed files with 68 additions and 159 deletions
Showing only changes of commit 171d1daa50 - Show all commits
@@ -47,7 +47,11 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.R import com.flagship.abox.manager.R
import com.flagship.abox.manager.designsystem.ABoxButton import com.flagship.abox.manager.designsystem.ABoxButton
import com.flagship.abox.manager.designsystem.ABoxDeviceDetailScaffold
import com.flagship.abox.manager.designsystem.ABoxIconButton
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
import com.flagship.abox.manager.designsystem.icon.ABoxIcon
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
import com.flagship.abox.manager.domain.DeviceName import com.flagship.abox.manager.domain.DeviceName
import com.flagship.abox.manager.domain.SocketPowerState import com.flagship.abox.manager.domain.SocketPowerState
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -79,39 +83,10 @@ fun SocketScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var showEditNameDialog by remember { mutableStateOf(false) } ABoxDeviceDetailScaffold(
Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
topBar = { title = state.deviceName?.value ?: stringResource(R.string.socket_title),
TopAppBar( onBack = onBackClick,
title = {
Text(
text = state.deviceName?.value ?: stringResource(R.string.socket_title),
style = MaterialTheme.typography.titleLarge,
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Text("<", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
},
actions = {
IconButton(
onClick = { showEditNameDialog = true },
enabled = !state.isLoading && !state.isOperating,
) {
Text("✎", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
IconButton(
onClick = { onEvent(SocketUiEvent.Refresh) },
enabled = !state.isLoading && !state.isOperating,
) {
Text("↻", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
},
)
},
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -121,6 +96,17 @@ fun SocketScreen(
.verticalScroll(scrollState), .verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
ABoxIconButton(
key = ABoxIconKey.REFRESH,
contentDescription = "刷新插座状态",
onClick = { onEvent(SocketUiEvent.Refresh) },
enabled = !state.isLoading && !state.isOperating,
)
}
if (state.isOffline) { if (state.isOffline) {
OfflineBanner( OfflineBanner(
onRetry = { onEvent(SocketUiEvent.Refresh) }, onRetry = { onEvent(SocketUiEvent.Refresh) },
@@ -248,41 +234,6 @@ fun SocketScreen(
} }
} }
if (showEditNameDialog) {
var inputName by remember(state.deviceName) { mutableStateOf(state.deviceName?.value.orEmpty()) }
AlertDialog(
onDismissRequest = { showEditNameDialog = false },
title = { Text(stringResource(R.string.dialog_edit_device_name_title)) },
text = {
OutlinedTextField(
value = inputName,
onValueChange = { inputName = it },
label = { Text(stringResource(R.string.dialog_edit_device_name_hint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
},
confirmButton = {
TextButton(
onClick = {
val trimmed = inputName.trim()
if (trimmed.isNotBlank()) {
onEvent(SocketUiEvent.ChangeDeviceName(DeviceName(trimmed)))
}
showEditNameDialog = false
},
enabled = inputName.isNotBlank(),
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
TextButton(onClick = { showEditNameDialog = false }) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
} }
@Composable @Composable
@@ -310,10 +261,10 @@ private fun PowerIndicatorBadge(
if (isLoading) { if (isLoading) {
ABoxLoadingIndicator(modifier = Modifier.size(36.dp)) ABoxLoadingIndicator(modifier = Modifier.size(36.dp))
} else { } else {
Text( ABoxIcon(
text = if (powerState == SocketPowerState.ON) "⚡" else "○", key = if (powerState == SocketPowerState.ON) ABoxIconKey.POWER else ABoxIconKey.SOCKET,
style = MaterialTheme.typography.headlineLarge, contentDescription = if (powerState == SocketPowerState.ON) "插座已开启" else "插座已关闭",
color = indicatorColor, modifier = Modifier.size(40.dp),
) )
} }
} }
@@ -377,9 +328,11 @@ private fun ErrorBanner(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
TextButton(onClick = onDismiss) { ABoxIconButton(
Text("✕") key = ABoxIconKey.CLOSE,
} contentDescription = "关闭错误提示",
onClick = onDismiss,
)
} }
} }
} }
@@ -39,7 +39,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.flagship.abox.manager.R import com.flagship.abox.manager.R
import com.flagship.abox.manager.designsystem.ABoxButton import com.flagship.abox.manager.designsystem.ABoxButton
import com.flagship.abox.manager.designsystem.ABoxDeviceDetailScaffold
import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator import com.flagship.abox.manager.designsystem.ABoxLoadingIndicator
import com.flagship.abox.manager.designsystem.ABoxIconButton
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.domain.DeviceName import com.flagship.abox.manager.domain.DeviceName
@@ -72,39 +74,10 @@ fun TemperatureHumidityScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
var showEditNameDialog by remember { mutableStateOf(false) } ABoxDeviceDetailScaffold(
Scaffold(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
topBar = { title = state.deviceName?.value ?: stringResource(R.string.th_title),
TopAppBar( onBack = onBackClick,
title = {
Text(
text = state.deviceName?.value ?: stringResource(R.string.th_title),
style = MaterialTheme.typography.titleLarge,
)
},
navigationIcon = {
IconButton(onClick = onBackClick) {
Text("<", style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
}
},
actions = {
IconButton(
onClick = { showEditNameDialog = true },
enabled = !state.isLoading && !state.isRefreshing,
) {
Text("✎", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
IconButton(
onClick = { onEvent(TemperatureHumidityUiEvent.Refresh) },
enabled = !state.isLoading && !state.isRefreshing,
) {
Text("↻", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
}
},
)
},
) { paddingValues -> ) { paddingValues ->
Column( Column(
modifier = Modifier modifier = Modifier
@@ -114,6 +87,17 @@ fun TemperatureHumidityScreen(
.verticalScroll(scrollState), .verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
ABoxIconButton(
key = ABoxIconKey.REFRESH,
contentDescription = "刷新温湿度数据",
onClick = { onEvent(TemperatureHumidityUiEvent.Refresh) },
enabled = !state.isLoading && !state.isRefreshing,
)
}
if (state.isOffline) { if (state.isOffline) {
OfflineBanner( OfflineBanner(
onRetry = { onEvent(TemperatureHumidityUiEvent.Refresh) }, onRetry = { onEvent(TemperatureHumidityUiEvent.Refresh) },
@@ -241,41 +225,6 @@ fun TemperatureHumidityScreen(
} }
} }
if (showEditNameDialog) {
var inputName by remember(state.deviceName) { mutableStateOf(state.deviceName?.value.orEmpty()) }
AlertDialog(
onDismissRequest = { showEditNameDialog = false },
title = { Text(stringResource(R.string.dialog_edit_device_name_title)) },
text = {
OutlinedTextField(
value = inputName,
onValueChange = { inputName = it },
label = { Text(stringResource(R.string.dialog_edit_device_name_hint)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
},
confirmButton = {
TextButton(
onClick = {
val trimmed = inputName.trim()
if (trimmed.isNotBlank()) {
onEvent(TemperatureHumidityUiEvent.ChangeDeviceName(DeviceName(trimmed)))
}
showEditNameDialog = false
},
enabled = inputName.isNotBlank(),
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
TextButton(onClick = { showEditNameDialog = false }) {
Text(stringResource(R.string.action_cancel))
}
},
)
}
} }
@Composable @Composable
@@ -353,7 +302,11 @@ private fun NoDataCard(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center, verticalArrangement = Arrangement.Center,
) { ) {
Text("📭", style = MaterialTheme.typography.displaySmall) ABoxIcon(
key = ABoxIconKey.SENSOR,
contentDescription = "暂无传感器数据",
modifier = Modifier.size(40.dp),
)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
Text( Text(
text = stringResource(R.string.th_no_data_title), text = stringResource(R.string.th_no_data_title),
@@ -432,9 +385,11 @@ private fun ErrorBanner(
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
TextButton(onClick = onDismiss) { ABoxIconButton(
Text("✕") key = ABoxIconKey.CLOSE,
} contentDescription = "关闭错误提示",
onClick = onDismiss,
)
} }
} }
} }
@@ -59,6 +59,8 @@ 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.ABoxIconButton
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
import com.flagship.abox.manager.domain.ProviderProfileId import com.flagship.abox.manager.domain.ProviderProfileId
import com.flagship.abox.manager.domain.infrared.PanelLayoutStyle import com.flagship.abox.manager.domain.infrared.PanelLayoutStyle
import com.flagship.abox.manager.domain.infrared.PresetCodeLibrary import com.flagship.abox.manager.domain.infrared.PresetCodeLibrary
@@ -154,17 +156,15 @@ internal fun InfraredHubScreen(
} }
}, },
navigationIcon = { navigationIcon = {
TextButton(onClick = onBack) { ABoxIconButton(ABoxIconKey.BACK, "返回", onBack)
Text(stringResource(R.string.infrared_back))
}
}, },
actions = { actions = {
IconButton( ABoxIconButton(
key = ABoxIconKey.EDIT,
contentDescription = "打开红外设置",
onClick = onNavigateToSettings, onClick = onNavigateToSettings,
modifier = Modifier.testTag("hub_settings_button"), modifier = Modifier.testTag("hub_settings_button"),
) { )
Text("⚙️", fontSize = 20.sp)
}
}, },
) )
}, },
@@ -199,7 +199,7 @@ internal fun InfraredHubScreen(
onClick = { showCreateDialog = true }, onClick = { showCreateDialog = true },
modifier = Modifier.testTag("hub_add_remote_button"), modifier = Modifier.testTag("hub_add_remote_button"),
) { ) {
Text("➕ " + stringResource(R.string.infrared_hub_add_remote)) Text(stringResource(R.string.infrared_hub_add_remote))
} }
} }
@@ -278,7 +278,7 @@ private fun VirtualRemoteCard(
} }
Text(icon, fontSize = 28.sp) Text(icon, fontSize = 28.sp)
IconButton(onClick = onDelete, modifier = Modifier.testTag("delete_remote_${remote.id.value}")) { IconButton(onClick = onDelete, modifier = Modifier.testTag("delete_remote_${remote.id.value}")) {
Text("🗑️", fontSize = 16.sp) Text(stringResource(R.string.infrared_delete))
} }
} }
@@ -60,6 +60,9 @@ 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.ABoxIconButton
import com.flagship.abox.manager.designsystem.icon.ABoxIcon
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
import com.flagship.abox.manager.domain.infrared.AcFanSpeed import com.flagship.abox.manager.domain.infrared.AcFanSpeed
import com.flagship.abox.manager.domain.infrared.AcMode import com.flagship.abox.manager.domain.infrared.AcMode
import com.flagship.abox.manager.domain.infrared.AcStateSnapshot import com.flagship.abox.manager.domain.infrared.AcStateSnapshot
@@ -175,9 +178,7 @@ internal fun RemotePanelScreen(
} }
}, },
navigationIcon = { navigationIcon = {
TextButton(onClick = onBack) { ABoxIconButton(ABoxIconKey.BACK, "返回", onBack)
Text(stringResource(R.string.infrared_back))
}
}, },
actions = { actions = {
// 非空调面板显示学习模式开关 // 非空调面板显示学习模式开关
@@ -185,7 +186,7 @@ internal fun RemotePanelScreen(
FilterChip( FilterChip(
selected = state.isLearnMode, selected = state.isLearnMode,
onClick = onToggleLearnMode, onClick = onToggleLearnMode,
label = { Text(if (state.isLearnMode) "🔴 录制中" else "⚪ 学习模式") }, label = { Text(if (state.isLearnMode) "录制中" else "学习模式") },
colors = FilterChipDefaults.filterChipColors( colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = Color(0xFFFF5252), selectedContainerColor = Color(0xFFFF5252),
selectedLabelColor = Color.White, selectedLabelColor = Color.White,
@@ -222,7 +223,7 @@ internal fun RemotePanelScreen(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
Text("🔴", fontSize = 16.sp) ABoxIcon(ABoxIconKey.PENDING, "录制中", modifier = Modifier.size(20.dp))
Text( Text(
text = "学习模式已开启:点击任意按键,然后将物理实体遥控器对准转发器按下即可录码", text = "学习模式已开启:点击任意按键,然后将物理实体遥控器对准转发器按下即可录码",
color = Color(0xFFC62828), color = Color(0xFFC62828),