git.iko.soy/opencode-android
on 3e723a1 help sign in
116 lines 4.3 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()
53 // A notification tap / deep link requests a session: open it in the detail pane.
54 LaunchedEffect(pendingOpenSession) {
55 pendingOpenSession?.let {
56 selected = it
57 container.consumePendingOpenSession()
58 }
59 }
61 Row(modifier = modifier.fillMaxSize()) {
62 Box(
63 modifier = Modifier
64 .weight(0.38f)
65 .widthIn(max = 460.dp),
66 ) {
67 SessionListScreen(
68 container = container,
69 onOpenSession = { id ->
70 container.consumePendingShare()?.let { container.draftStore.set(id, it) }
71 selected = id
72 },
73 onDisconnect = onDisconnect,
74 onOpenFiles = onOpenFiles,
75 onOpenSettings = onOpenSettings,
76 )
77 }
79 Box(modifier = Modifier.weight(0.62f).fillMaxSize()) {
80 val sessionId = selected
81 if (sessionId == null) {
82 EmptyDetail()
83 } else {
84 BackHandler { selected = null }
85 key(sessionId) {
86 ChatScreen(
87 container = container,
88 sessionId = sessionId,
89 onBack = { selected = null },
90 )
91 }
92 }
93 }
94 }
97@Composable
98private fun EmptyDetail() {
99 Column(
100 modifier = Modifier.fillMaxSize().padding(32.dp),
101 verticalArrangement = Arrangement.Center,
102 horizontalAlignment = Alignment.CenterHorizontally,
103 ) {
104 Icon(
105 Icons.AutoMirrored.Filled.Chat,
106 contentDescription = null,
107 tint = MaterialTheme.colorScheme.outline,
108 )
109 Text(
110 stringResource(R.string.empty_detail_pane),
111 style = MaterialTheme.typography.bodyLarge,
112 color = MaterialTheme.colorScheme.onSurfaceVariant,
113 modifier = Modifier.padding(top = 12.dp),
114 )
115 }