test(android): Paparazzi preview screenshots with demo data; stateless screen composables
This commit is contained in:
parent
7b92e259ad
commit
1e32997504
20 changed files with 141 additions and 17 deletions
|
|
@ -15,6 +15,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import dev.byteclaw.client.ui.ByteClawTheme
|
||||
import dev.byteclaw.client.ui.ChatScreen
|
||||
import dev.byteclaw.client.ui.ChatViewModel
|
||||
import dev.byteclaw.client.ui.SessionsScreen
|
||||
|
|
@ -25,20 +26,27 @@ class MainActivity : ComponentActivity() {
|
|||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
MaterialTheme(colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()) {
|
||||
ByteClawTheme(dark = isSystemInDarkTheme()) {
|
||||
Surface {
|
||||
val vm: ChatViewModel = viewModel()
|
||||
val state by vm.state.collectAsStateWithLifecycle()
|
||||
val nav = rememberNavController()
|
||||
NavHost(nav, startDestination = if (state.settings.isConfigured) "chat" else "settings") {
|
||||
composable("chat") {
|
||||
ChatScreen(vm, state, onSessions = { nav.navigate("sessions") }, onSettings = { nav.navigate("settings") })
|
||||
ChatScreen(
|
||||
state, onSend = vm::send, onNewChat = vm::newChat, onDismissError = vm::dismissError,
|
||||
onSessions = { nav.navigate("sessions") }, onSettings = { nav.navigate("settings") },
|
||||
)
|
||||
}
|
||||
composable("sessions") {
|
||||
SessionsScreen(vm, state, onBack = { nav.popBackStack() }, onOpen = { nav.popBackStack() })
|
||||
SessionsScreen(
|
||||
state, onRefresh = { vm.refreshSessions() },
|
||||
onOpen = { id -> vm.openSession(id); nav.popBackStack() },
|
||||
onDelete = { id -> vm.deleteSession(id) }, onBack = { nav.popBackStack() },
|
||||
)
|
||||
}
|
||||
composable("settings") {
|
||||
SettingsScreen(vm, state, onBack = {
|
||||
SettingsScreen(state, onSave = vm::saveSettings, onTest = { s, cb -> vm.testConnection(s, cb) }, onBack = {
|
||||
if (!nav.popBackStack()) nav.navigate("chat") { popUpTo("settings") { inclusive = true } }
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,16 @@ import dev.byteclaw.client.data.ServerSettings
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ChatScreen(vm: ChatViewModel, state: UiState, onSessions: () -> Unit, onSettings: () -> Unit) {
|
||||
var input by remember { mutableStateOf("") }
|
||||
fun ChatScreen(
|
||||
state: UiState,
|
||||
onSend: (String) -> Unit,
|
||||
onNewChat: () -> Unit,
|
||||
onDismissError: () -> Unit,
|
||||
onSessions: () -> Unit,
|
||||
onSettings: () -> Unit,
|
||||
initialInput: String = "",
|
||||
) {
|
||||
var input by remember { mutableStateOf(initialInput) }
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(state.lines.size, state.lines.lastOrNull()?.text?.length) {
|
||||
if (state.lines.isNotEmpty()) listState.animateScrollToItem(state.lines.lastIndex)
|
||||
|
|
@ -42,7 +50,7 @@ fun ChatScreen(vm: ChatViewModel, state: UiState, onSessions: () -> Unit, onSett
|
|||
title = { Text("ByteClaw") },
|
||||
navigationIcon = { IconButton(onClick = onSessions) { Icon(Icons.AutoMirrored.Filled.List, "Sessions") } },
|
||||
actions = {
|
||||
IconButton(onClick = vm::newChat) { Icon(Icons.Filled.Add, "Neuer Chat") }
|
||||
IconButton(onClick = onNewChat) { Icon(Icons.Filled.Add, "Neuer Chat") }
|
||||
IconButton(onClick = onSettings) { Icon(Icons.Filled.Settings, "Einstellungen") }
|
||||
},
|
||||
)
|
||||
|
|
@ -56,7 +64,7 @@ fun ChatScreen(vm: ChatViewModel, state: UiState, onSessions: () -> Unit, onSett
|
|||
}
|
||||
state.error?.let {
|
||||
Card(
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 4.dp).fillMaxWidth().clickable(onClick = vm::dismissError),
|
||||
Modifier.padding(horizontal = 12.dp, vertical = 4.dp).fillMaxWidth().clickable(onClick = onDismissError),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.errorContainer),
|
||||
) { Text(it, Modifier.padding(12.dp)) }
|
||||
}
|
||||
|
|
@ -71,7 +79,7 @@ fun ChatScreen(vm: ChatViewModel, state: UiState, onSessions: () -> Unit, onSett
|
|||
enabled = state.settings.isConfigured,
|
||||
)
|
||||
IconButton(
|
||||
onClick = { vm.send(input); input = "" },
|
||||
onClick = { onSend(input); input = "" },
|
||||
enabled = !state.busy && input.isNotBlank() && state.settings.isConfigured,
|
||||
) { Icon(Icons.AutoMirrored.Filled.Send, "Senden") }
|
||||
}
|
||||
|
|
@ -105,8 +113,14 @@ private fun Bubble(line: ChatLine) {
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SessionsScreen(vm: ChatViewModel, state: UiState, onBack: () -> Unit, onOpen: () -> Unit) {
|
||||
LaunchedEffect(Unit) { vm.refreshSessions() }
|
||||
fun SessionsScreen(
|
||||
state: UiState,
|
||||
onRefresh: () -> Unit,
|
||||
onOpen: (String) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
LaunchedEffect(Unit) { onRefresh() }
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(title = { Text("Sessions") },
|
||||
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Zurück") } })
|
||||
|
|
@ -119,8 +133,8 @@ fun SessionsScreen(vm: ChatViewModel, state: UiState, onBack: () -> Unit, onOpen
|
|||
ListItem(
|
||||
headlineContent = { Text(s.title.ifBlank { "(ohne Titel)" }, maxLines = 1) },
|
||||
supportingContent = { Text(s.updatedAt.take(16).replace('T', ' ') + " UTC") },
|
||||
trailingContent = { IconButton(onClick = { vm.deleteSession(s.id) }) { Icon(Icons.Filled.Delete, "Löschen") } },
|
||||
modifier = Modifier.clickable { vm.openSession(s.id); onOpen() },
|
||||
trailingContent = { IconButton(onClick = { onDelete(s.id) }) { Icon(Icons.Filled.Delete, "Löschen") } },
|
||||
modifier = Modifier.clickable { onOpen(s.id) },
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
|
@ -130,10 +144,16 @@ fun SessionsScreen(vm: ChatViewModel, state: UiState, onBack: () -> Unit, onOpen
|
|||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(vm: ChatViewModel, state: UiState, onBack: () -> Unit) {
|
||||
fun SettingsScreen(
|
||||
state: UiState,
|
||||
onSave: (ServerSettings) -> Unit,
|
||||
onTest: (ServerSettings, (String) -> Unit) -> Unit,
|
||||
onBack: () -> Unit,
|
||||
initialStatus: String? = null,
|
||||
) {
|
||||
var url by remember { mutableStateOf(state.settings.serverUrl) }
|
||||
var token by remember { mutableStateOf(state.settings.token) }
|
||||
var status by remember { mutableStateOf<String?>(null) }
|
||||
var status by remember { mutableStateOf(initialStatus) }
|
||||
val urlError = if (url.isBlank()) null else ByteClawApi.validateUrl(url)
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(title = { Text("Einstellungen") },
|
||||
|
|
@ -151,10 +171,10 @@ fun SettingsScreen(vm: ChatViewModel, state: UiState, onBack: () -> Unit) {
|
|||
style = MaterialTheme.typography.bodySmall)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(enabled = urlError == null && url.isNotBlank() && token.isNotBlank(), onClick = {
|
||||
vm.saveSettings(ServerSettings(url, token)); onBack()
|
||||
onSave(ServerSettings(url, token)); onBack()
|
||||
}) { Text("Speichern") }
|
||||
OutlinedButton(enabled = urlError == null && url.isNotBlank(), onClick = {
|
||||
status = "Teste…"; vm.testConnection(ServerSettings(url.trim().trimEnd('/'), token.trim())) { status = it }
|
||||
status = "Teste…"; onTest(ServerSettings(url.trim().trimEnd('/'), token.trim())) { status = it }
|
||||
}) { Text("Verbindung testen") }
|
||||
}
|
||||
status?.let { Text(it) }
|
||||
|
|
|
|||
11
android/app/src/main/java/dev/byteclaw/client/ui/Theme.kt
Normal file
11
android/app/src/main/java/dev/byteclaw/client/ui/Theme.kt
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
package dev.byteclaw.client.ui
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
@Composable
|
||||
fun ByteClawTheme(dark: Boolean, content: @Composable () -> Unit) {
|
||||
MaterialTheme(colorScheme = if (dark) darkColorScheme() else lightColorScheme(), content = content)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue