git.iko.soy/opencode-android
on 06fd41b help sign in
92 lines 3.7 kB history raw
1package soy.iko.opencode.ui.file
3import androidx.activity.compose.BackHandler
4import androidx.compose.foundation.layout.Box
5import androidx.compose.foundation.layout.Row
6import androidx.compose.foundation.layout.fillMaxHeight
7import androidx.compose.foundation.layout.fillMaxSize
8import androidx.compose.foundation.layout.widthIn
9import androidx.compose.material.icons.Icons
10import androidx.compose.material.icons.filled.Description
11import androidx.compose.material3.VerticalDivider
12import androidx.compose.runtime.Composable
13import androidx.compose.runtime.LaunchedEffect
14import androidx.compose.runtime.getValue
15import androidx.compose.runtime.key
16import androidx.compose.runtime.mutableStateOf
17import androidx.compose.runtime.remember
18import androidx.compose.runtime.saveable.rememberSaveable
19import androidx.compose.runtime.setValue
20import androidx.compose.ui.Modifier
21import androidx.compose.ui.res.stringResource
22import androidx.compose.ui.unit.dp
23import androidx.lifecycle.compose.collectAsStateWithLifecycle
24import soy.iko.opencode.R
25import soy.iko.opencode.data.network.NetworkConfig
26import soy.iko.opencode.di.AppContainer
27import soy.iko.opencode.ui.components.EmptyState
29/**
30 * Master–detail layout for the file browser on wide screens (≥ 840dp): the directory listing
31 * on the left, the selected file's viewer on the right. Mirrors [TwoPaneSessionChat]: each
32 * side hosts an existing screen (which carries its own Scaffold + app bar), so the panes are
33 * self-contained. Selecting a file updates the detail pane rather than pushing a destination;
34 * the viewer's onBack clears the selection.
35 */
36@Composable
37fun TwoPaneFileBrowser(
38 container: AppContainer,
39 onBack: () -> Unit,
40 modifier: Modifier = Modifier,
41) {
42 var selected by rememberSaveable { mutableStateOf<String?>(null) }
43 // A deep-linked file open (opencode://file/{path}) targets the detail pane here.
44 val pendingOpenFile by container.pendingOpenFile.collectAsStateWithLifecycle()
45 LaunchedEffect(pendingOpenFile) {
46 pendingOpenFile?.let {
47 selected = it
48 container.consumePendingOpenFile()
49 }
50 }
52 Row(modifier = modifier.fillMaxSize()) {
53 Box(
54 modifier = Modifier
55 .weight(NetworkConfig.twoPaneLeftWeight)
56 .widthIn(max = 460.dp),
57 ) {
58 FileBrowserScreen(
59 container = container,
60 // Selecting a file opens it in the detail pane (not the back stack).
61 onOpenFile = { path, _ -> selected = path },
62 onBack = onBack,
63 )
64 }
66 VerticalDivider(modifier = Modifier.fillMaxHeight())
68 Box(modifier = Modifier.weight(NetworkConfig.twoPaneRightWeight).fillMaxSize()) {
69 val path = selected
70 if (path == null) {
71 EmptyState(
72 icon = Icons.Filled.Description,
73 title = stringResource(R.string.empty_file_detail_title),
74 description = stringResource(R.string.empty_file_detail_desc),
75 modifier = Modifier.fillMaxSize(),
76 )
77 } else {
78 BackHandler { selected = null }
79 // key() on the path so switching files in two-pane mode gives FileViewScreen a
80 // fresh composition (its find/wrap state and scroll are per-file, like the chat
81 // pane's per-session keying).
82 key(path) {
83 FileViewScreen(
84 container = container,
85 path = path,
86 onBack = { selected = null },
87 )
88 }
89 }
90 }
91 }