feat(app): add About screen and mark 1.0 release #14

Merged
KremeCN merged 1 commits from feat/add-about-screen into main 2026-08-28 09:47:58 +08:00
6 changed files with 246 additions and 1 deletions
Showing only changes of commit 80a2eb8325 - Show all commits
+1 -1
View File
@@ -20,7 +20,7 @@ android {
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 1 versionCode = 1
versionName = "0.1.0" versionName = "1.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -25,6 +25,7 @@ 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.about.AboutScreen
import com.flagship.abox.manager.ui.preferences.SettingsRoute import com.flagship.abox.manager.ui.preferences.SettingsRoute
import com.flagship.abox.manager.ui.preferences.SettingsExperienceState import com.flagship.abox.manager.ui.preferences.SettingsExperienceState
import com.flagship.abox.manager.ui.preferences.SettingsViewModel import com.flagship.abox.manager.ui.preferences.SettingsViewModel
@@ -49,6 +50,7 @@ fun ABoxManagerApp(
val screenKey = sessionState.screenKey() val screenKey = sessionState.screenKey()
var showAccountDialog by rememberSaveable(screenKey) { mutableStateOf(false) } var showAccountDialog by rememberSaveable(screenKey) { mutableStateOf(false) }
var showSettings by rememberSaveable(screenKey) { mutableStateOf(false) } var showSettings by rememberSaveable(screenKey) { mutableStateOf(false) }
var showAbout by rememberSaveable(screenKey) { mutableStateOf(false) }
val settingsExperienceState = rememberSaveable(screenKey, saver = SettingsExperienceState.Saver) { val settingsExperienceState = rememberSaveable(screenKey, saver = SettingsExperienceState.Saver) {
SettingsExperienceState() SettingsExperienceState()
} }
@@ -87,9 +89,11 @@ fun ABoxManagerApp(
onSessionAction = sessionActionsViewModel::dispatch, onSessionAction = sessionActionsViewModel::dispatch,
showAccountDialog = showAccountDialog, showAccountDialog = showAccountDialog,
showSettings = showSettings, showSettings = showSettings,
showAbout = showAbout,
settingsExperienceState = settingsExperienceState, settingsExperienceState = settingsExperienceState,
onShowAccountDialogChange = { showAccountDialog = it }, onShowAccountDialogChange = { showAccountDialog = it },
onShowSettingsChange = { showSettings = it }, onShowSettingsChange = { showSettings = it },
onShowAboutChange = { showAbout = it },
) )
} }
} }
@@ -104,9 +108,11 @@ private fun AuthenticatedNavHost(
onSessionAction: (SessionUiAction) -> Unit, onSessionAction: (SessionUiAction) -> Unit,
showAccountDialog: Boolean, showAccountDialog: Boolean,
showSettings: Boolean, showSettings: Boolean,
showAbout: Boolean,
settingsExperienceState: SettingsExperienceState, settingsExperienceState: SettingsExperienceState,
onShowAccountDialogChange: (Boolean) -> Unit, onShowAccountDialogChange: (Boolean) -> Unit,
onShowSettingsChange: (Boolean) -> Unit, onShowSettingsChange: (Boolean) -> Unit,
onShowAboutChange: (Boolean) -> Unit,
) { ) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
AuthenticatedNavigation( AuthenticatedNavigation(
@@ -127,6 +133,11 @@ private fun AuthenticatedNavHost(
) )
} }
} }
if (showAbout) {
ABoxBackLayer(onBack = { onShowAboutChange(false) }) {
AboutScreen(onBack = { onShowAboutChange(false) })
}
}
} }
if (showAccountDialog) { if (showAccountDialog) {
@@ -139,6 +150,10 @@ private fun AuthenticatedNavHost(
settingsExperienceState.showTheme(returnToHome = false) settingsExperienceState.showTheme(returnToHome = false)
onShowSettingsChange(true) onShowSettingsChange(true)
}, },
onOpenAbout = {
onShowAccountDialogChange(false)
onShowAboutChange(true)
},
onSwitchProvider = { onSwitchProvider = {
onShowAccountDialogChange(false) onShowAccountDialogChange(false)
onSessionAction(SessionUiAction.SwitchProvider) onSessionAction(SessionUiAction.SwitchProvider)
@@ -0,0 +1,183 @@
package com.flagship.abox.manager.ui.about
import androidx.compose.foundation.Image
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.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
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.ABoxIconButton
import com.flagship.abox.manager.designsystem.icon.ABoxIconKey
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val packageInfo = remember(context.packageName) {
runCatching {
context.packageManager.getPackageInfo(context.packageName, 0)
}.getOrNull()
}
val versionName = packageInfo?.versionName ?: stringResource(R.string.about_unknown)
val versionCode = packageInfo?.let {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) it.longVersionCode else {
@Suppress("DEPRECATION")
it.versionCode.toLong()
}
}
Scaffold(
modifier = modifier.fillMaxSize(),
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.about_title)) },
navigationIcon = {
ABoxIconButton(
key = ABoxIconKey.BACK,
contentDescription = stringResource(R.string.action_back),
onClick = onBack,
)
},
)
},
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp, vertical = 24.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Image(
painter = painterResource(R.drawable.ic_abox_logo_master),
contentDescription = stringResource(R.string.app_name),
modifier = Modifier.size(88.dp),
)
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
)
Text(
stringResource(R.string.about_tagline),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
AboutSection(stringResource(R.string.about_app_info)) {
AboutValueRow(stringResource(R.string.about_version), "$versionName (${versionCode ?: stringResource(R.string.about_unknown)})")
AboutValueRow(stringResource(R.string.about_package), context.packageName)
AboutValueRow(stringResource(R.string.about_platform), stringResource(R.string.about_platform_value))
}
AboutSection(stringResource(R.string.about_product)) {
Text(
stringResource(R.string.about_product_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
AboutSection(stringResource(R.string.about_team)) {
TeamMember("KremeCN", stringResource(R.string.about_role_foundation))
TeamMember("alZerNest", stringResource(R.string.about_role_devices))
TeamMember("Aurum", stringResource(R.string.about_role_environment))
TeamMember("Moler", stringResource(R.string.about_role_infrared))
TeamMember("tzh", stringResource(R.string.about_role_scenes))
}
Text(
stringResource(R.string.about_copyright),
modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(12.dp))
}
}
}
@Composable
private fun AboutSection(
title: String,
content: @Composable () -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
) {
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
content()
}
}
}
}
@Composable
private fun AboutValueRow(label: String, value: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, color = MaterialTheme.colorScheme.onSurfaceVariant)
Text(value, fontWeight = FontWeight.Medium, modifier = Modifier.padding(start = 16.dp))
}
}
@Composable
private fun TeamMember(name: String, role: String) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(name, fontWeight = FontWeight.SemiBold)
Text(
role,
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@@ -15,6 +15,7 @@ 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.Palette import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Info
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
@@ -46,6 +47,7 @@ fun AccountProviderDialog(
providerName: String, providerName: String,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onOpenAbout: () -> Unit,
onSwitchProvider: () -> Unit, onSwitchProvider: () -> Unit,
onEndSession: () -> Unit, onEndSession: () -> Unit,
onForgetCredentials: () -> Unit, onForgetCredentials: () -> Unit,
@@ -113,6 +115,13 @@ fun AccountProviderDialog(
tag = "account_dialog_open_settings", tag = "account_dialog_open_settings",
onClick = onOpenSettings, onClick = onOpenSettings,
) )
AccountSheetAction(
icon = Icons.Outlined.Info,
label = stringResource(R.string.account_about),
description = stringResource(R.string.account_about_desc),
tag = "account_dialog_open_about",
onClick = onOpenAbout,
)
AccountSheetAction( AccountSheetAction(
icon = Icons.AutoMirrored.Outlined.Logout, icon = Icons.AutoMirrored.Outlined.Logout,
label = stringResource(R.string.account_logout), label = stringResource(R.string.account_logout),
@@ -348,6 +348,25 @@
<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>
<string name="account_about">关于</string>
<string name="account_about_desc">App 信息、版本与开发团队</string>
<string name="about_title">关于</string>
<string name="about_tagline">面向 Android 的 ABox 智能家居管理应用</string>
<string name="about_app_info">App 信息</string>
<string name="about_version">版本</string>
<string name="about_package">应用包名</string>
<string name="about_platform">平台</string>
<string name="about_platform_value">Android 手机与平板</string>
<string name="about_product">产品介绍</string>
<string name="about_product_desc">ABox 家庭通过 ABSDK 连接 ABox 网关,提供设备控制、红外遥控器、情景、自动化与活动记录。本地体验提供独立的模拟环境,不使用 ABox 凭据。</string>
<string name="about_team">开发团队</string>
<string name="about_role_foundation">项目架构与全模块开发</string>
<string name="about_role_devices">设备目录与导航</string>
<string name="about_role_environment">插座与环境传感器</string>
<string name="about_role_infrared">红外遥控器</string>
<string name="about_role_scenes">情景与应用集成</string>
<string name="about_copyright">由 Flagship 开发团队共同构建。</string>
<string name="about_unknown">未知</string>
<string name="scene_create_sheet_title">新建指令</string> <string name="scene_create_sheet_title">新建指令</string>
<string name="scene_create_sheet_description">选择指令的启动方式</string> <string name="scene_create_sheet_description">选择指令的启动方式</string>
<string name="scene_create_manual_description">手动点击运行,适合常用的一键设备组合。</string> <string name="scene_create_manual_description">手动点击运行,适合常用的一键设备组合。</string>
@@ -348,6 +348,25 @@
<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>
<string name="account_about">About</string>
<string name="account_about_desc">App information, version, and development team</string>
<string name="about_title">About</string>
<string name="about_tagline">ABox smart home management for Android</string>
<string name="about_app_info">App information</string>
<string name="about_version">Version</string>
<string name="about_package">Package</string>
<string name="about_platform">Platform</string>
<string name="about_platform_value">Android phones and tablets</string>
<string name="about_product">Product</string>
<string name="about_product_desc">ABox Home connects to an ABox gateway through ABSDK and provides device control, infrared remotes, scenes, automation, and activity history. Local Experience offers an isolated simulation environment without using ABox credentials.</string>
<string name="about_team">Development team</string>
<string name="about_role_foundation">Project architecture and development across all modules</string>
<string name="about_role_devices">Device catalog and navigation</string>
<string name="about_role_environment">Socket and environment sensors</string>
<string name="about_role_infrared">Infrared remotes</string>
<string name="about_role_scenes">Scenes and integration</string>
<string name="about_copyright">Developed by Flagship.</string>
<string name="about_unknown">Unknown</string>
<string name="scene_create_sheet_title">New command</string> <string name="scene_create_sheet_title">New command</string>
<string name="scene_create_sheet_description">Choose how the command starts</string> <string name="scene_create_sheet_description">Choose how the command starts</string>
<string name="scene_create_manual_description">Run manually for frequently used device combinations.</string> <string name="scene_create_manual_description">Run manually for frequently used device combinations.</string>