1package soy.iko.opencode.ui.file
3import androidx.compose.foundation.clickable
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.fillMaxWidth
9import androidx.compose.foundation.layout.padding
10import androidx.compose.foundation.layout.size
11import androidx.compose.foundation.lazy.LazyColumn
12import androidx.compose.foundation.lazy.items
13import androidx.compose.material.icons.Icons
14import androidx.compose.material.icons.automirrored.filled.ArrowBack
15import androidx.compose.material.icons.filled.Description
16import androidx.compose.material.icons.filled.Folder
17import androidx.compose.material.icons.filled.Search
18import androidx.compose.material3.CircularProgressIndicator
19import androidx.compose.material3.ExperimentalMaterial3Api
20import androidx.compose.material3.HorizontalDivider
21import androidx.compose.material3.Icon
22import androidx.compose.material3.IconButton
23import androidx.compose.material3.MaterialTheme
24import androidx.compose.material3.OutlinedTextField
25import androidx.compose.material3.Scaffold
26import androidx.compose.material3.Text
27import androidx.compose.material3.TopAppBar
28import androidx.compose.runtime.Composable
29import androidx.compose.runtime.getValue
30import androidx.compose.ui.Alignment
31import androidx.compose.ui.Modifier
32import androidx.compose.ui.text.style.TextOverflow
33import androidx.compose.ui.unit.dp
34import androidx.lifecycle.compose.collectAsStateWithLifecycle
35import androidx.lifecycle.viewmodel.compose.viewModel
36import soy.iko.opencode.di.AppContainer
37import soy.iko.opencode.ui.vmFactory
39@OptIn(ExperimentalMaterial3Api::class)
42 container: AppContainer,
43 onOpenFile: (String) -> Unit,
46 val vm: FileBrowserViewModel = viewModel(factory = vmFactory { FileBrowserViewModel(container) })
47 val state by vm.state.collectAsStateWithLifecycle()
52 title = { Text(if (state.path.isBlank()) "Files" else state.path, maxLines = 1, overflow = TextOverflow.Ellipsis) },
54 IconButton(onClick = onBack) {
55 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
61 Column(modifier = Modifier.fillMaxSize().padding(padding)) {
64 onValueChange = vm::setQuery,
65 modifier = Modifier.fillMaxWidth().padding(12.dp),
66 placeholder = { Text("Search files…") },
67 leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
71 Box(modifier = Modifier.fillMaxSize()) {
73 state.loading || state.searching -> CircularProgressIndicator(Modifier.align(Alignment.Center))
74 state.error != null && !state.isSearching -> Text(
76 modifier = Modifier.align(Alignment.Center).padding(24.dp),
77 color = MaterialTheme.colorScheme.error,
79 state.isSearching -> SearchResults(state.results, onOpenFile)
80 else -> DirectoryListing(
84 onOpenFile = onOpenFile,
93private fun SearchResults(results: List<String>, onOpenFile: (String) -> Unit) {
94 if (results.isEmpty()) {
95 Text("No matches.", modifier = Modifier.padding(24.dp))
98 LazyColumn(modifier = Modifier.fillMaxSize()) {
99 items(results, key = { it }) { path ->
100 FileRow(icon = false, label = path, onClick = { onOpenFile(path) })
107private fun DirectoryListing(
108 state: FileBrowserState,
109 onOpenDir: (String) -> Unit,
111 onOpenFile: (String) -> Unit,
113 LazyColumn(modifier = Modifier.fillMaxSize()) {
114 if (state.path.isNotBlank()) {
116 FileRow(icon = true, label = "..", onClick = onUp)
120 items(state.entries, key = { it.path }) { node ->
122 icon = node.isDirectory,
124 onClick = { if (node.isDirectory) onOpenDir(node.path) else onOpenFile(node.path) },
132private fun FileRow(icon: Boolean, label: String, onClick: () -> Unit) {
134 modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp, vertical = 14.dp),
135 verticalAlignment = Alignment.CenterVertically,
138 if (icon) Icons.Filled.Folder else Icons.Filled.Description,
139 contentDescription = null,
140 modifier = Modifier.size(20.dp),
141 tint = if (icon) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
145 style = MaterialTheme.typography.bodyLarge,
147 overflow = TextOverflow.Ellipsis,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list