feat(settings): C 起步——App 设置页(主题/旁白/思维链默认展开/关于)

新增 AppSettings(DataStore)与 LocalAppPrefs;CyreneRoot 按偏好决定 Miuix 主题模式并下发偏好;设置页从侧边栏进入,返回回聊天。旁白开关与思维链默认展开已接入渲染层。
This commit is contained in:
AskaEth committed 2026-09-08 22:41:37 +08:00
1 parent 91117986e4
commit 61214a18fa
8 files changed
+279 -33

No files matched your search

@@ -13,8 +13,9 @@ import top.yeij.cyrene.app.AppContainer
import top.yeij.cyrene.ui.common.MiuixDrawer
import top.yeij.cyrene.ui.sessions.SessionDrawerContent
import top.yeij.cyrene.ui.sessions.SessionsViewModel
import top.yeij.cyrene.ui.settings.SettingsScreen
/** 聊天页 + 侧边栏(会话切换)。 */
/** 聊天页 + 侧边栏(会话切换)+ 设置页。 */
@Composable
fun ChatHomeScreen(
container: AppContainer,
@@ -25,31 +26,39 @@ fun ChatHomeScreen(
)
val state by sessionsViewModel.state.collectAsState()
var drawerOpen by remember { mutableStateOf(false) }
var showSettings by remember { mutableStateOf(false) }
MiuixDrawer(
open = drawerOpen,
onOpenChange = { drawerOpen = it },
content = {
ChatScreen(
container = container,
sessionId = state.activeSessionId,
onOpenDrawer = { drawerOpen = true },
onLogout = onLogout,
)
},
drawerContent = {
SessionDrawerContent(
state = state,
onSelect = { sessionId ->
sessionsViewModel.select(sessionId)
drawerOpen = false
},
onCreate = sessionsViewModel::createSession,
onDelete = { sessionsViewModel.deleteSession(it.id) },
onOpenIot = { /* P2:IoT 页面 */ },
onOpenSettings = { /* P2:设置页面 */ },
onLogout = onLogout,
)
},
)
if (showSettings) {
SettingsScreen(container = container, onBack = { showSettings = false })
} else {
MiuixDrawer(
open = drawerOpen,
onOpenChange = { drawerOpen = it },
content = {
ChatScreen(
container = container,
sessionId = state.activeSessionId,
onOpenDrawer = { drawerOpen = true },
onLogout = onLogout,
)
},
drawerContent = {
SessionDrawerContent(
state = state,
onSelect = { sessionId ->
sessionsViewModel.select(sessionId)
drawerOpen = false
},
onCreate = sessionsViewModel::createSession,
onDelete = { sessionsViewModel.deleteSession(it.id) },
onOpenIot = { /* P2:IoT 页面 */ },
onOpenSettings = {
drawerOpen = false
showSettings = true
},
onLogout = onLogout,
)
},
)
}
}
@@ -18,6 +18,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import top.yeij.cyrene.core.prefs.LocalAppPrefs
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
import top.yeij.cyrene.domain.parse.ContentPart
import top.yeij.cyrene.domain.parse.ParsedBubble
@@ -131,6 +132,7 @@ private fun Bubble(
/** 旁白/动作:灰斜体小字,依附在气泡上/下。 */
@Composable
private fun AnnotationLine(parts: List<ContentPart>) {
if (!LocalAppPrefs.current.showNarration) return
val text = parts.joinToString(" ") { it.text }.trim()
if (text.isEmpty()) return
SelectionContainer(modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)) {
@@ -21,6 +21,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import top.yeij.cyrene.core.prefs.LocalAppPrefs
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -35,7 +36,8 @@ fun ThinkingCard(
modifier: Modifier = Modifier,
) {
if (text.isBlank() && !active) return
var expanded by remember { mutableStateOf(active) }
val defaultExpanded = LocalAppPrefs.current.thinkingExpanded
var expanded by remember { mutableStateOf(active && defaultExpanded) }
LaunchedEffect(active) {
if (!active) expanded = false
}
@@ -0,0 +1,135 @@
package top.yeij.cyrene.ui.settings
import androidx.compose.foundation.clickable
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import top.yeij.cyrene.BuildConfig
import top.yeij.cyrene.app.AppContainer
import top.yeij.cyrene.core.prefs.AppPrefs
import top.yeij.cyrene.core.prefs.ThemeMode
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.SmallTopAppBar
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.preference.SwitchPreference
import top.yukonga.miuix.kmp.theme.MiuixTheme
@Composable
fun SettingsScreen(
container: AppContainer,
onBack: () -> Unit,
) {
val prefs by container.appSettings.data.collectAsState(initial = AppPrefs())
val scope = rememberCoroutineScope()
val settings = container.appSettings
Scaffold(
topBar = {
SmallTopAppBar(
title = "App 设置",
navigationIcon = { TextButton("返回", onClick = onBack) },
)
},
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState()),
) {
SectionTitle("外观")
ThemeMode.entries.forEach { mode ->
ThemeOption(
label = mode.label(),
selected = prefs.themeMode == mode,
onClick = { scope.launch { settings.setThemeMode(mode) } },
)
}
SectionTitle("聊天")
SwitchPreference(
title = "显示旁白",
summary = "以灰斜体小字显示 <action> / <narration>",
checked = prefs.showNarration,
onCheckedChange = { scope.launch { settings.setShowNarration(it) } },
)
SwitchPreference(
title = "思维链默认展开",
summary = "流式期间展开思维链,结束后自动折叠",
checked = prefs.thinkingExpanded,
onCheckedChange = { scope.launch { settings.setThinkingExpanded(it) } },
)
SectionTitle("关于")
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text("版本", style = MiuixTheme.textStyles.main, modifier = Modifier.weight(1f))
Text(
text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
style = MiuixTheme.textStyles.footnote1,
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
)
}
Spacer(Modifier.height(24.dp))
}
}
}
@Composable
private fun SectionTitle(text: String) {
Text(
text = text,
style = MiuixTheme.textStyles.subtitle,
color = MiuixTheme.colorScheme.primary,
modifier = Modifier.padding(start = 20.dp, top = 20.dp, bottom = 8.dp),
)
}
@Composable
private fun ThemeOption(
label: String,
selected: Boolean,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 20.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(label, style = MiuixTheme.textStyles.main, modifier = Modifier.weight(1f))
if (selected) {
Text(
text = "✓",
style = MiuixTheme.textStyles.main,
color = MiuixTheme.colorScheme.primary,
)
}
}
}
private fun ThemeMode.label(): String = when (this) {
ThemeMode.System -> "跟随系统"
ThemeMode.Light -> "浅色"
ThemeMode.Dark -> "深色"
}