git.iko.soy/opencode-android
on 66d239c help sign in
127 lines 4.8 kB history raw
1package soy.iko.opencode.ui.session
3import androidx.compose.foundation.layout.Arrangement
4import androidx.compose.foundation.layout.Box
5import androidx.compose.foundation.layout.Column
6import androidx.compose.foundation.layout.Row
7import androidx.compose.foundation.layout.fillMaxSize
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.widthIn
10import androidx.activity.compose.BackHandler
11import androidx.compose.material.icons.Icons
12import androidx.compose.material.icons.automirrored.filled.Chat
13import androidx.compose.material3.Icon
14import androidx.compose.material3.MaterialTheme
15import androidx.compose.material3.Text
16import androidx.compose.runtime.Composable
17import androidx.compose.runtime.LaunchedEffect
18import androidx.compose.runtime.getValue
19import androidx.compose.runtime.key
20import androidx.compose.runtime.mutableStateOf
21import androidx.compose.runtime.saveable.rememberSaveable
22import androidx.compose.runtime.setValue
23import androidx.compose.ui.Alignment
24import androidx.compose.ui.Modifier
25import androidx.compose.ui.res.stringResource
26import androidx.compose.ui.unit.dp
27import androidx.lifecycle.compose.collectAsStateWithLifecycle
28import soy.iko.opencode.R
29import soy.iko.opencode.di.AppContainer
30import soy.iko.opencode.ui.chat.ChatScreen
32/**
33 * Master–detail layout for wide screens (≥ 840dp, e.g. tablets / unfolded foldables):
34 * the session list on the left, the selected conversation on the right. On compact
35 * widths the app uses the single-pane [androidx.navigation] back stack instead.
36 *
37 * Each side hosts an existing screen (which carries its own Scaffold + app bar), so the
38 * two panes are self-contained. Selecting a session updates the detail pane rather than
39 * pushing a destination; the chat [onBack] clears the selection. The chat composition is
40 * keyed by session id so each conversation gets its own ViewModel.
41 */
42@Composable
43fun TwoPaneSessionChat(
44 container: AppContainer,
45 onOpenFiles: () -> Unit,
46 onOpenSettings: () -> Unit,
47 onDisconnect: () -> Unit,
48 modifier: Modifier = Modifier,
49) {
50 var selected by rememberSaveable { mutableStateOf<String?>(null) }
51 val pendingOpenSession by container.pendingOpenSession.collectAsStateWithLifecycle()
52 val pendingShare by container.pendingShare.collectAsStateWithLifecycle()
54 // A notification tap / deep link requests a session: open it in the detail pane.
55 LaunchedEffect(pendingOpenSession) {
56 pendingOpenSession?.let {
57 selected = it
58 container.consumePendingOpenSession()
59 }
60 }
62 // Inject a pending share into the currently selected session's draft (if any).
63 // Unlike single-pane mode where the session list is navigated to specifically for
64 // the share, in two-pane mode the list is always visible — so we inject into the
65 // session the user is already viewing rather than whichever one they tap next.
66 LaunchedEffect(pendingShare, selected) {
67 val target = selected
68 if (pendingShare != null && target != null) {
69 container.consumePendingShare()?.let { container.draftStore.set(target, it) }
70 }
71 }
73 Row(modifier = modifier.fillMaxSize()) {
74 Box(
75 modifier = Modifier
76 .weight(0.38f)
77 .widthIn(max = 460.dp),
78 ) {
79 SessionListScreen(
80 container = container,
81 onOpenSession = { id ->
82 selected = id
83 },
84 onDisconnect = onDisconnect,
85 onOpenFiles = onOpenFiles,
86 onOpenSettings = onOpenSettings,
87 )
88 }
90 Box(modifier = Modifier.weight(0.62f).fillMaxSize()) {
91 val sessionId = selected
92 if (sessionId == null) {
93 EmptyDetail()
94 } else {
95 BackHandler { selected = null }
96 key(sessionId) {
97 ChatScreen(
98 container = container,
99 sessionId = sessionId,
100 onBack = { selected = null },
101 )
102 }
103 }
104 }
105 }
108@Composable
109private fun EmptyDetail() {
110 Column(
111 modifier = Modifier.fillMaxSize().padding(32.dp),
112 verticalArrangement = Arrangement.Center,
113 horizontalAlignment = Alignment.CenterHorizontally,
114 ) {
115 Icon(
116 Icons.AutoMirrored.Filled.Chat,
117 contentDescription = null,
118 tint = MaterialTheme.colorScheme.outline,
119 )
120 Text(
121 stringResource(R.string.empty_detail_pane),
122 style = MaterialTheme.typography.bodyLarge,
123 color = MaterialTheme.colorScheme.onSurfaceVariant,
124 modifier = Modifier.padding(top = 12.dp),
125 )
126 }