git.iko.soy/opencode-android
on 29df003 help sign in
101 lines 4.0 kB history raw
1package soy.iko.opencode.ui
3import androidx.activity.compose.BackHandler
4import androidx.compose.animation.AnimatedContent
5import androidx.compose.animation.slideInHorizontally
6import androidx.compose.animation.slideOutHorizontally
7import androidx.compose.animation.togetherWith
8import androidx.compose.foundation.layout.fillMaxSize
9import androidx.compose.foundation.layout.padding
10import androidx.compose.material3.AlertDialog
11import androidx.compose.material3.Scaffold
12import androidx.compose.material3.SnackbarDuration
13import androidx.compose.material3.SnackbarHost
14import androidx.compose.material3.SnackbarHostState
15import androidx.compose.material3.Text
16import androidx.compose.material3.TextButton
17import androidx.compose.runtime.Composable
18import androidx.compose.runtime.LaunchedEffect
19import androidx.compose.runtime.getValue
20import androidx.compose.runtime.mutableStateOf
21import androidx.compose.runtime.remember
22import androidx.compose.runtime.setValue
23import androidx.compose.ui.Modifier
24import soy.iko.opencode.core.Event
25import soy.iko.opencode.core.Screen
26import soy.iko.opencode.core.UiState
27import soy.iko.opencode.ui.screens.ChatScreen
28import soy.iko.opencode.ui.screens.ConnectScreen
29import soy.iko.opencode.ui.screens.SessionsScreen
31@Composable
32fun OpencodeApp(state: UiState, dispatch: (Event) -> Unit) {
33 val snackbar = remember { SnackbarHostState() }
34 var showStopConfirm by remember { mutableStateOf(false) }
36 LaunchedEffect(state.error) {
37 state.error?.let {
38 snackbar.showSnackbar(
39 message = it,
40 actionLabel = "Dismiss",
41 duration = SnackbarDuration.Short,
42 )
43 // Always clear the error from state after showing.
44 dispatch(Event.DismissError)
45 }
46 }
48 BackHandler(enabled = state.screen != Screen.Connect) {
49 when (state.screen) {
50 // Leaving mid-generation would silently drop the reply, so confirm first.
51 Screen.Chat -> if (state.generating) showStopConfirm = true else dispatch(Event.NavigateToSessions)
52 Screen.Sessions -> dispatch(Event.NavigateToConnect)
53 Screen.Connect -> Unit
54 }
55 }
57 if (showStopConfirm) {
58 AlertDialog(
59 onDismissRequest = { showStopConfirm = false },
60 title = { Text("Stop generation?") },
61 text = { Text("Leaving will stop the current generation.") },
62 confirmButton = {
63 TextButton(onClick = {
64 showStopConfirm = false
65 dispatch(Event.CancelGeneration)
66 dispatch(Event.NavigateToSessions)
67 }) {
68 Text("Stop and leave")
69 }
70 },
71 dismissButton = {
72 TextButton(onClick = { showStopConfirm = false }) {
73 Text("Stay")
74 }
75 },
76 )
77 }
79 Scaffold(snackbarHost = { SnackbarHost(snackbar) }) { padding ->
80 AnimatedContent(
81 targetState = state.screen,
82 modifier = Modifier.fillMaxSize().padding(padding),
83 transitionSpec = {
84 // Compare screen ordinals to decide slide direction: forward navigation
85 // (Connect → Sessions → Chat) slides in from the right, back slides from the left.
86 if (targetState.ordinal > initialState.ordinal) {
87 slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
88 } else {
89 slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
90 }
91 },
92 label = "screen",
93 ) { screen ->
94 when (screen) {
95 Screen.Connect -> ConnectScreen(state, dispatch)
96 Screen.Sessions -> SessionsScreen(state, dispatch)
97 Screen.Chat -> ChatScreen(state, dispatch)
98 }
99 }
100 }