git.iko.soy/opencode-android
on f9ed438 help sign in
150 lines 5.7 kB history raw
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)
40@Composable
41fun FileBrowserScreen(
42 container: AppContainer,
43 onOpenFile: (String) -> Unit,
44 onBack: () -> Unit,
45) {
46 val vm: FileBrowserViewModel = viewModel(factory = vmFactory { FileBrowserViewModel(container) })
47 val state by vm.state.collectAsStateWithLifecycle()
49 Scaffold(
50 topBar = {
51 TopAppBar(
52 title = { Text(if (state.path.isBlank()) "Files" else state.path, maxLines = 1, overflow = TextOverflow.Ellipsis) },
53 navigationIcon = {
54 IconButton(onClick = onBack) {
55 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
56 }
57 },
58 )
59 },
60 ) { padding ->
61 Column(modifier = Modifier.fillMaxSize().padding(padding)) {
62 OutlinedTextField(
63 value = state.query,
64 onValueChange = vm::setQuery,
65 modifier = Modifier.fillMaxWidth().padding(12.dp),
66 placeholder = { Text("Search files…") },
67 leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },
68 singleLine = true,
69 )
71 Box(modifier = Modifier.fillMaxSize()) {
72 when {
73 state.loading || state.searching -> CircularProgressIndicator(Modifier.align(Alignment.Center))
74 state.error != null && !state.isSearching -> Text(
75 state.error!!,
76 modifier = Modifier.align(Alignment.Center).padding(24.dp),
77 color = MaterialTheme.colorScheme.error,
78 )
79 state.isSearching -> SearchResults(state.results, onOpenFile)
80 else -> DirectoryListing(
81 state = state,
82 onOpenDir = vm::open,
83 onUp = vm::up,
84 onOpenFile = onOpenFile,
85 )
86 }
87 }
88 }
89 }
92@Composable
93private fun SearchResults(results: List<String>, onOpenFile: (String) -> Unit) {
94 if (results.isEmpty()) {
95 Text("No matches.", modifier = Modifier.padding(24.dp))
96 return
97 }
98 LazyColumn(modifier = Modifier.fillMaxSize()) {
99 items(results, key = { it }) { path ->
100 FileRow(icon = false, label = path, onClick = { onOpenFile(path) })
101 HorizontalDivider()
102 }
103 }
106@Composable
107private fun DirectoryListing(
108 state: FileBrowserState,
109 onOpenDir: (String) -> Unit,
110 onUp: () -> Unit,
111 onOpenFile: (String) -> Unit,
112) {
113 LazyColumn(modifier = Modifier.fillMaxSize()) {
114 if (state.path.isNotBlank()) {
115 item(key = "__up") {
116 FileRow(icon = true, label = "..", onClick = onUp)
117 HorizontalDivider()
118 }
119 }
120 items(state.entries, key = { it.path }) { node ->
121 FileRow(
122 icon = node.isDirectory,
123 label = node.name,
124 onClick = { if (node.isDirectory) onOpenDir(node.path) else onOpenFile(node.path) },
125 )
126 HorizontalDivider()
127 }
128 }
131@Composable
132private fun FileRow(icon: Boolean, label: String, onClick: () -> Unit) {
133 Row(
134 modifier = Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp, vertical = 14.dp),
135 verticalAlignment = Alignment.CenterVertically,
136 ) {
137 Icon(
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,
142 )
143 Text(
144 " $label",
145 style = MaterialTheme.typography.bodyLarge,
146 maxLines = 1,
147 overflow = TextOverflow.Ellipsis,
148 )
149 }