git.iko.soy/opencode-android
on master help sign in
124 lines 4.8 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) }
35 var showDisconnectConfirm by remember { mutableStateOf(false) }
37 LaunchedEffect(state.error) {
38 state.error?.let {
39 snackbar.showSnackbar(
40 message = it,
41 actionLabel = "Dismiss",
42 duration = SnackbarDuration.Short,
43 )
44 // Always clear the error from state after showing.
45 dispatch(Event.DismissError)
46 }
47 }
49 BackHandler(enabled = state.screen != Screen.Connect) {
50 when (state.screen) {
51 // Leaving mid-generation would silently drop the reply, so confirm first.
52 Screen.Chat -> if (state.generating) showStopConfirm = true else dispatch(Event.NavigateToSessions)
53 // Disconnecting drops the server session, so guard the back button.
54 Screen.Sessions -> showDisconnectConfirm = true
55 Screen.Connect -> Unit
56 }
57 }
59 if (showStopConfirm) {
60 AlertDialog(
61 onDismissRequest = { showStopConfirm = false },
62 title = { Text("Stop generation?") },
63 text = { Text("Leaving will stop the current generation.") },
64 confirmButton = {
65 TextButton(onClick = {
66 showStopConfirm = false
67 dispatch(Event.CancelGeneration)
68 dispatch(Event.NavigateToSessions)
69 }) {
70 Text("Stop and leave")
71 }
72 },
73 dismissButton = {
74 TextButton(onClick = { showStopConfirm = false }) {
75 Text("Stay")
76 }
77 },
78 )
79 }
81 if (showDisconnectConfirm) {
82 AlertDialog(
83 onDismissRequest = { showDisconnectConfirm = false },
84 title = { Text("Disconnect?") },
85 text = { Text("You will be disconnected from the server.") },
86 confirmButton = {
87 TextButton(onClick = {
88 showDisconnectConfirm = false
89 dispatch(Event.NavigateToConnect)
90 }) {
91 Text("Disconnect")
92 }
93 },
94 dismissButton = {
95 TextButton(onClick = { showDisconnectConfirm = false }) {
96 Text("Cancel")
97 }
98 },
99 )
100 }
102 Scaffold(snackbarHost = { SnackbarHost(snackbar) }) { padding ->
103 AnimatedContent(
104 targetState = state.screen,
105 modifier = Modifier.fillMaxSize().padding(padding),
106 transitionSpec = {
107 // Compare screen ordinals to decide slide direction: forward navigation
108 // (Connect → Sessions → Chat) slides in from the right, back slides from the left.
109 if (targetState.ordinal > initialState.ordinal) {
110 slideInHorizontally { it } togetherWith slideOutHorizontally { -it }
111 } else {
112 slideInHorizontally { -it } togetherWith slideOutHorizontally { it }
113 }
114 },
115 label = "screen",
116 ) { screen ->
117 when (screen) {
118 Screen.Connect -> ConnectScreen(state, dispatch)
119 Screen.Sessions -> SessionsScreen(state, dispatch)
120 Screen.Chat -> ChatScreen(state, dispatch)
121 }
122 }
123 }