chore: 机器特定的构建配置移出仓库
aapt2FromMavenOverride(ARM64 专用绝对路径)与 sdkDownload 移到本机 ~/.gradle/gradle.properties,仓库配置保持跨平台可用。
This commit is contained in:
1 parent
84de51c7b7
commit
253913ad82
8 files changed
+450
-62
No files matched your search
@@ -3,9 +3,45 @@ package top.yeij.cyrene.app
|
||||
import android.content.Context
|
||||
import top.yeij.cyrene.core.auth.AuthManager
|
||||
import top.yeij.cyrene.core.auth.SessionStore
|
||||
import top.yeij.cyrene.data.api.ApiClientFactory
|
||||
import top.yeij.cyrene.data.api.SessionsApi
|
||||
import top.yeij.cyrene.data.repository.MessageRepository
|
||||
import top.yeij.cyrene.data.repository.SessionRepository
|
||||
import top.yeij.cyrene.data.ws.ChatSocket
|
||||
|
||||
/** 手写 DI 容器(D:不引 Hilt)。 */
|
||||
class AppContainer(context: Context) {
|
||||
|
||||
val sessionStore: SessionStore = SessionStore(context.applicationContext)
|
||||
val authManager: AuthManager = AuthManager(sessionStore)
|
||||
|
||||
private var cachedBaseUrl: String? = null
|
||||
private var cachedSessionsApi: SessionsApi? = null
|
||||
|
||||
/** 会话/消息 REST 客户端;baseUrl 变化时自动重建。 */
|
||||
val sessionsApi: SessionsApi
|
||||
get() {
|
||||
val baseUrl = authManager.currentBaseUrl
|
||||
val cached = cachedSessionsApi
|
||||
if (cached != null && cachedBaseUrl == baseUrl) return cached
|
||||
val created = ApiClientFactory.create(
|
||||
baseUrl = baseUrl,
|
||||
service = SessionsApi::class.java,
|
||||
tokenProvider = { sessionStore.load().token },
|
||||
onUnauthorized = { authManager.onSessionExpired() },
|
||||
)
|
||||
cachedBaseUrl = baseUrl
|
||||
cachedSessionsApi = created
|
||||
return created
|
||||
}
|
||||
|
||||
val sessionRepository: SessionRepository get() = SessionRepository(sessionsApi)
|
||||
|
||||
val messageRepository: MessageRepository get() = MessageRepository(sessionsApi)
|
||||
|
||||
fun createChatSocket(): ChatSocket = ChatSocket(
|
||||
baseUrlProvider = { authManager.currentBaseUrl },
|
||||
tokenProvider = { sessionStore.load().token },
|
||||
onAuthFailed = { authManager.onSessionExpired() },
|
||||
)
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import top.yeij.cyrene.core.auth.SessionState
|
||||
import top.yeij.cyrene.ui.chat.ChatScreen
|
||||
import top.yeij.cyrene.ui.common.LoadingScreen
|
||||
import top.yeij.cyrene.ui.home.HomeScreen
|
||||
import top.yeij.cyrene.ui.login.LoginScreen
|
||||
|
||||
@Composable
|
||||
@@ -20,7 +20,8 @@ fun CyreneApp(container: AppContainer) {
|
||||
when (state) {
|
||||
SessionState.Unknown -> LoadingScreen()
|
||||
SessionState.LoggedOut -> LoginScreen(container)
|
||||
is SessionState.LoggedIn -> HomeScreen(
|
||||
is SessionState.LoggedIn -> ChatScreen(
|
||||
container = container,
|
||||
onLogout = { container.authManager.logout() },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -19,11 +19,20 @@ object ApiClientFactory {
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
/** 登录接口(不注入 token)。 */
|
||||
fun create(
|
||||
baseUrl: String,
|
||||
tokenProvider: () -> String? = { null },
|
||||
onUnauthorized: () -> Unit = {},
|
||||
): CyreneApi {
|
||||
): CyreneApi = create(baseUrl, CyreneApi::class.java, tokenProvider, onUnauthorized)
|
||||
|
||||
/** 通用入口:按需构建任意 Retrofit service。 */
|
||||
fun <T : Any> create(
|
||||
baseUrl: String,
|
||||
service: Class<T>,
|
||||
tokenProvider: () -> String? = { null },
|
||||
onUnauthorized: () -> Unit = {},
|
||||
): T {
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
@@ -46,7 +55,7 @@ object ApiClientFactory {
|
||||
.client(client)
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
.create(CyreneApi::class.java)
|
||||
.create(service)
|
||||
}
|
||||
|
||||
/** Retrofit 要求 baseUrl 以 `/` 结尾;用户没写 scheme 时补 `http://`。 */
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package top.yeij.cyrene.ui.chat
|
||||
|
||||
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.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.viewmodel.initializer
|
||||
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import top.yeij.cyrene.app.AppContainer
|
||||
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
|
||||
import top.yeij.cyrene.data.ws.WsState
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
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.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.basic.Sidebar
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
container: AppContainer,
|
||||
onOpenDrawer: () -> Unit = {},
|
||||
onLogout: () -> Unit = {},
|
||||
) {
|
||||
val viewModel: ChatViewModel = viewModel(
|
||||
factory = viewModelFactory { initializer { ChatViewModel(container) } },
|
||||
)
|
||||
val state by viewModel.state.collectAsState()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
// reverseLayout 下「列表末尾」就是屏幕顶部;接近末尾时加载更早的一页。
|
||||
LaunchedEffect(listState) {
|
||||
snapshotFlow {
|
||||
val info = listState.layoutInfo
|
||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - 2
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.collect { nearTop -> if (nearTop) viewModel.loadOlder() }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
title = "与昔涟",
|
||||
subtitle = statusLabel(state),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onOpenDrawer) {
|
||||
Icon(imageVector = MiuixIcons.Basic.Sidebar, contentDescription = "菜单")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton("登出", onClick = onLogout)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
when {
|
||||
state.loading && state.messages.isEmpty() -> CenterHint("加载中…")
|
||||
state.messages.isEmpty() -> CenterHint("还没有消息")
|
||||
else -> LazyColumn(
|
||||
state = listState,
|
||||
reverseLayout = true,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
items(items = state.messages.asReversed(), key = { it.id }) { message ->
|
||||
MessageBubble(message)
|
||||
}
|
||||
if (state.hasMore) {
|
||||
item(key = "__load_older__") {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 16.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = if (state.loadingOlder) "加载更早的消息…" else "上滑加载更早的消息",
|
||||
style = MiuixTheme.textStyles.footnote2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.error?.let { message ->
|
||||
Text(
|
||||
text = message,
|
||||
style = MiuixTheme.textStyles.footnote1,
|
||||
color = MiuixTheme.colorScheme.error,
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.fillMaxWidth()
|
||||
.background(MiuixTheme.colorScheme.surfaceContainer)
|
||||
.padding(12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBubble(message: HistoryMessageDto) {
|
||||
val isUser = message.role == "user"
|
||||
val bubbleColor = if (isUser) MiuixTheme.colorScheme.primary else MiuixTheme.colorScheme.surfaceContainer
|
||||
val textColor = if (isUser) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurfaceContainer
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 14.dp),
|
||||
horizontalAlignment = if (isUser) Alignment.End else Alignment.Start,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 300.dp)
|
||||
.background(color = bubbleColor, shape = RoundedCornerShape(18.dp))
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = message.content,
|
||||
style = MiuixTheme.textStyles.main,
|
||||
color = textColor,
|
||||
)
|
||||
}
|
||||
messageSourceLabel(message)?.let { source ->
|
||||
Text(
|
||||
text = source,
|
||||
style = MiuixTheme.textStyles.footnote2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
modifier = Modifier.padding(top = 3.dp, start = 6.dp, end = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CenterHint(text: String) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MiuixTheme.textStyles.main,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun statusLabel(state: ChatUiState): String = when {
|
||||
state.busyMode != null && state.queueDepth > 0 -> "正在思考… 已排队 ${state.queueDepth} 条"
|
||||
state.busyMode != null -> "正在思考…"
|
||||
else -> when (state.connection) {
|
||||
WsState.Connected -> "已连接"
|
||||
WsState.Connecting -> "连接中…"
|
||||
WsState.Reconnecting -> "重连中…"
|
||||
WsState.Offline -> "离线"
|
||||
WsState.Idle -> ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package top.yeij.cyrene.ui.chat
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import top.yeij.cyrene.app.AppContainer
|
||||
import top.yeij.cyrene.data.api.CyreneException
|
||||
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
|
||||
import top.yeij.cyrene.data.repository.MessageRepository
|
||||
import top.yeij.cyrene.data.repository.mergeMessages
|
||||
import top.yeij.cyrene.data.ws.ChatSocket
|
||||
import top.yeij.cyrene.data.ws.WsEvent
|
||||
import top.yeij.cyrene.data.ws.WsState
|
||||
|
||||
const val MAIN_SESSION_ID = "web"
|
||||
|
||||
data class ChatUiState(
|
||||
/** 升序(旧 → 新);渲染时反向喂给 reverseLayout 的列表。 */
|
||||
val messages: List<HistoryMessageDto> = emptyList(),
|
||||
val loading: Boolean = true,
|
||||
val loadingOlder: Boolean = false,
|
||||
val hasMore: Boolean = true,
|
||||
val error: String? = null,
|
||||
val connection: WsState = WsState.Idle,
|
||||
/** 编排忙碌态(`idle`/`thinking`/`executing`/`subagent`),null = 空闲。 */
|
||||
val busyMode: String? = null,
|
||||
val queueDepth: Int = 0,
|
||||
)
|
||||
|
||||
/**
|
||||
* M2:只读主会话。
|
||||
*
|
||||
* 数据来源三路合并(都走 `mergeMessages` 按 `id` 去重):
|
||||
* 1. REST 首屏 `loadLatest`(权威、带分页游标)
|
||||
* 2. WS 首帧 `history`(与 REST 重叠,靠 id 去重收敛)
|
||||
* 3. WS 增量 `message_persisted`(其它端/本端落库后的消息)
|
||||
*
|
||||
* 注意(D14):结构化流帧不带 `sessionId`,同一时刻只允许一个会话在流式;
|
||||
* 当前只有主会话,暂不涉及归属问题。
|
||||
*/
|
||||
class ChatViewModel(
|
||||
private val container: AppContainer,
|
||||
private val sessionId: String = MAIN_SESSION_ID,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow(ChatUiState())
|
||||
val state: StateFlow<ChatUiState> = _state.asStateFlow()
|
||||
|
||||
private var socket: ChatSocket? = null
|
||||
private var repository: MessageRepository? = null
|
||||
private var oldestLoadedAt: Long? = null
|
||||
|
||||
init {
|
||||
start()
|
||||
}
|
||||
|
||||
private fun start() {
|
||||
val repo = container.messageRepository
|
||||
repository = repo
|
||||
|
||||
viewModelScope.launch {
|
||||
repo.loadLatest(sessionId)
|
||||
.onSuccess { page ->
|
||||
oldestLoadedAt = page.oldestCreatedAt
|
||||
_state.update {
|
||||
it.copy(
|
||||
messages = mergeMessages(it.messages, page.messages),
|
||||
loading = false,
|
||||
hasMore = page.hasMore,
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
_state.update { it.copy(loading = false, error = e.userMessage()) }
|
||||
}
|
||||
}
|
||||
|
||||
val ws = container.createChatSocket()
|
||||
socket = ws
|
||||
viewModelScope.launch {
|
||||
ws.state.collect { connection -> _state.update { it.copy(connection = connection) } }
|
||||
}
|
||||
viewModelScope.launch {
|
||||
ws.events.collect(::onEvent)
|
||||
}
|
||||
ws.connect()
|
||||
}
|
||||
|
||||
private fun onEvent(event: WsEvent) {
|
||||
when (event) {
|
||||
is WsEvent.History -> {
|
||||
val incoming = event.messages
|
||||
if (incoming.isEmpty()) {
|
||||
_state.update { it.copy(loading = false) }
|
||||
} else {
|
||||
val oldest = incoming.minOf { it.createdAt }
|
||||
oldestLoadedAt = oldestLoadedAt?.let { minOf(it, oldest) } ?: oldest
|
||||
_state.update {
|
||||
it.copy(messages = mergeMessages(it.messages, incoming), loading = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is WsEvent.MessagePersisted -> {
|
||||
val message = event.message ?: return
|
||||
if (message.sessionId != sessionId) return
|
||||
_state.update { it.copy(messages = mergeMessages(it.messages, listOf(message))) }
|
||||
}
|
||||
|
||||
is WsEvent.SessionBusy -> if (event.sessionId == sessionId) {
|
||||
_state.update { it.copy(busyMode = event.mode) }
|
||||
}
|
||||
|
||||
is WsEvent.PendingAck -> if (event.sessionId == sessionId) {
|
||||
_state.update { it.copy(busyMode = event.mode, queueDepth = event.queueDepth) }
|
||||
}
|
||||
|
||||
is WsEvent.PendingDrain -> if (event.sessionId == sessionId) {
|
||||
_state.update { it.copy(queueDepth = event.queueDepth) }
|
||||
}
|
||||
|
||||
is WsEvent.SessionIdle -> if (event.sessionId == sessionId) {
|
||||
_state.update { it.copy(busyMode = null, queueDepth = 0) }
|
||||
}
|
||||
|
||||
is WsEvent.SessionInterrupted -> if (event.sessionId == sessionId) {
|
||||
_state.update { it.copy(busyMode = null, queueDepth = 0) }
|
||||
}
|
||||
|
||||
is WsEvent.Error -> _state.update { it.copy(error = event.message) }
|
||||
|
||||
else -> Unit // 流式/思维链/工具卡等留到 M3、M4
|
||||
}
|
||||
}
|
||||
|
||||
/** 上拉加载更早的一页(`before` 游标 = 当前已加载最旧一条的 createdAt)。 */
|
||||
fun loadOlder() {
|
||||
val repo = repository ?: return
|
||||
val before = oldestLoadedAt ?: return
|
||||
val snapshot = _state.value
|
||||
if (snapshot.loadingOlder || !snapshot.hasMore) return
|
||||
|
||||
_state.update { it.copy(loadingOlder = true) }
|
||||
viewModelScope.launch {
|
||||
repo.loadPage(sessionId, before = before)
|
||||
.onSuccess { page ->
|
||||
if (page.oldestCreatedAt != null) oldestLoadedAt = page.oldestCreatedAt
|
||||
_state.update {
|
||||
it.copy(
|
||||
messages = mergeMessages(it.messages, page.messages),
|
||||
loadingOlder = false,
|
||||
hasMore = page.hasMore && page.messages.isNotEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
.onFailure { e ->
|
||||
_state.update { it.copy(loadingOlder = false, error = e.userMessage()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissError() = _state.update { it.copy(error = null) }
|
||||
|
||||
override fun onCleared() {
|
||||
socket?.close()
|
||||
socket = null
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.userMessage(): String =
|
||||
(this as? CyreneException)?.error?.userMessage ?: "加载失败,请稍后重试"
|
||||
@@ -0,0 +1,34 @@
|
||||
package top.yeij.cyrene.ui.chat
|
||||
|
||||
import top.yeij.cyrene.data.api.dto.HistoryMessageDto
|
||||
|
||||
private val platformLabels = mapOf(
|
||||
"web" to "Web",
|
||||
"kook" to "KOOK",
|
||||
"weixin-ilink" to "微信",
|
||||
"qq-obv11" to "QQ",
|
||||
"telegram" to "Telegram",
|
||||
"webhook" to "Webhook",
|
||||
)
|
||||
|
||||
/**
|
||||
* 来源角标(S1.3):每条消息默认显示来源。
|
||||
* 优先用 `deviceId` 区分具体端,再回落到 `platform`;两者都没有则返回 null(不显示)。
|
||||
*/
|
||||
fun messageSourceLabel(message: HistoryMessageDto): String? {
|
||||
val deviceId = message.deviceId
|
||||
if (!deviceId.isNullOrBlank()) {
|
||||
if (deviceId.startsWith("voiceassist")) return "来自安卓"
|
||||
if (deviceId.startsWith("web:")) {
|
||||
val browser = deviceId.removePrefix("web:").substringBefore(':')
|
||||
return "来自 Web($browser)"
|
||||
}
|
||||
return "来自 $deviceId"
|
||||
}
|
||||
|
||||
val platform = message.platform
|
||||
if (!platform.isNullOrBlank()) {
|
||||
return "来自 ${platformLabels[platform] ?: platform}"
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package top.yeij.cyrene.ui.home
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import top.yukonga.miuix.kmp.basic.Icon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton
|
||||
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.icon.MiuixIcons
|
||||
import top.yukonga.miuix.kmp.icon.basic.Sidebar
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
@Composable
|
||||
fun HomeScreen(onLogout: () -> Unit = {}) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
title = "Cyrene",
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { /* M5:打开侧边栏 */ }) {
|
||||
Icon(imageVector = MiuixIcons.Basic.Sidebar, contentDescription = "菜单")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
TextButton("登出", onClick = onLogout)
|
||||
},
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Text(text = "与昔涟", style = MiuixTheme.textStyles.title2)
|
||||
Text(
|
||||
text = "会话列表 · 规划中",
|
||||
style = MiuixTheme.textStyles.footnote1,
|
||||
color = MiuixTheme.colorScheme.onSurfaceContainerVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,4 @@ org.gradle.configuration-cache=false
|
||||
android.useAndroidX=true
|
||||
android.nonTransitiveRClass=true
|
||||
|
||||
# 本机无法访问 dl.google.com,禁止 AGP 尝试联网下载 SDK 组件(否则会卡住)
|
||||
android.builder.sdkDownload=false
|
||||
|
||||
kotlin.code.style=official
|
||||
|
||||
# ARM64 机器:Google Maven 只提供 x86_64 的 aapt2,改用本机 ARM64 版本
|
||||
android.aapt2FromMavenOverride=/home/Aska/Android/Sdk/build-tools/37.0.0/aapt2
|
||||
Reference in new issue
Block a user