1package soy.iko.opencode.ui.file
3import android.content.Intent
4import soy.iko.opencode.ui.components.showToast
5import androidx.compose.foundation.horizontalScroll
6import androidx.compose.foundation.layout.Box
7import androidx.compose.foundation.layout.Column
8import androidx.compose.foundation.layout.Row
9import androidx.compose.foundation.layout.Spacer
10import androidx.compose.foundation.layout.fillMaxSize
11import androidx.compose.foundation.layout.padding
12import androidx.compose.foundation.layout.size
13import androidx.compose.foundation.layout.width
14import androidx.compose.foundation.lazy.LazyColumn
15import androidx.compose.foundation.lazy.itemsIndexed
16import androidx.compose.foundation.rememberScrollState
17import androidx.compose.foundation.verticalScroll
18import androidx.compose.material.icons.Icons
19import androidx.compose.material.icons.automirrored.filled.ArrowBack
20import androidx.compose.material.icons.filled.ContentCopy
21import androidx.compose.material.icons.filled.Share
22import androidx.compose.material3.CircularProgressIndicator
23import androidx.compose.material3.ExperimentalMaterial3Api
24import androidx.compose.material3.Icon
25import androidx.compose.material3.IconButton
26import androidx.compose.material3.MaterialTheme
27import androidx.compose.material3.Scaffold
28import androidx.compose.material3.Text
29import androidx.compose.material3.TextButton
30import androidx.compose.material3.TopAppBar
31import androidx.compose.runtime.Composable
32import androidx.compose.runtime.getValue
33import androidx.compose.runtime.remember
34import androidx.compose.ui.Alignment
35import androidx.compose.ui.Modifier
36import androidx.compose.ui.platform.LocalContext
37import androidx.compose.ui.semantics.contentDescription
38import androidx.compose.ui.semantics.semantics
39import androidx.compose.ui.text.font.FontFamily
40import androidx.compose.ui.text.style.TextOverflow
41import androidx.compose.ui.unit.dp
42import androidx.compose.ui.res.stringResource
43import androidx.lifecycle.compose.collectAsStateWithLifecycle
44import androidx.lifecycle.viewmodel.compose.viewModel
45import soy.iko.opencode.di.AppContainer
46import soy.iko.opencode.R
47import soy.iko.opencode.ui.components.DiffView
48import soy.iko.opencode.ui.components.copyToClipboard
49import soy.iko.opencode.ui.vmFactory
50import soy.iko.opencode.util.runCatchingCancellable
52@OptIn(ExperimentalMaterial3Api::class)
55 container: AppContainer,
59 val vm: FileViewModel = viewModel(factory = vmFactory { FileViewModel(container, path) })
60 val state by vm.state.collectAsStateWithLifecycle()
61 val context = LocalContext.current
62 val filename = remember(path) { path.substringAfterLast('/') }
63 val shareLabel = stringResource(R.string.share)
68 title = { Text(filename, maxLines = 1, overflow = TextOverflow.Ellipsis) },
70 IconButton(onClick = onBack) {
71 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
75 val content = state.content?.content.orEmpty()
76 if (content.isNotEmpty()) {
77 IconButton(onClick = { copyToClipboard(context, filename, content) }) {
78 Icon(Icons.Filled.ContentCopy, contentDescription = stringResource(R.string.copy))
80 IconButton(onClick = {
81 val send = Intent(Intent.ACTION_SEND).apply {
83 putExtra(Intent.EXTRA_SUBJECT, filename)
84 putExtra(Intent.EXTRA_TEXT, content)
86 runCatchingCancellable { context.startActivity(Intent.createChooser(send, shareLabel)) }
88 showToast(context, context.getString(R.string.no_share_app))
91 Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
98 Box(modifier = Modifier.fillMaxSize().padding(padding)) {
101 val loadingLabel = stringResource(R.string.loading)
102 CircularProgressIndicator(
103 Modifier.align(Alignment.Center).semantics { contentDescription = loadingLabel },
106 state.error != null -> Column(
107 modifier = Modifier.align(Alignment.Center).padding(24.dp),
108 horizontalAlignment = Alignment.CenterHorizontally,
112 color = MaterialTheme.colorScheme.error,
114 Spacer(Modifier.size(12.dp))
115 TextButton(onClick = { vm.reload() }) {
116 Text(stringResource(R.string.retry))
119 state.content?.isBinary == true -> Text(
120 stringResource(R.string.binary_file),
121 modifier = Modifier.align(Alignment.Center).padding(24.dp),
122 color = MaterialTheme.colorScheme.onSurfaceVariant,
125 val content = state.content
126 if (content?.diff != null && content.diff.isNotBlank()) {
131 .verticalScroll(rememberScrollState())
135 val text = content?.content.orEmpty()
136 if (text.isEmpty()) {
138 stringResource(R.string.empty_file),
139 modifier = Modifier.align(Alignment.Center).padding(24.dp),
140 color = MaterialTheme.colorScheme.onSurfaceVariant,
143 FileTextContent(text)
153private fun FileTextContent(text: String) {
154 val lines = remember(text) { text.split("\n") }
155 val gutterWidth = remember(lines.size) {
156 (lines.size.toString().length.coerceAtLeast(3) * 10).dp
158 val hScrollState = rememberScrollState()
164 itemsIndexed(lines, key = { index, _ -> index }) { index, line ->
165 Row(modifier = Modifier.horizontalScroll(hScrollState)) {
168 modifier = Modifier.width(gutterWidth),
169 style = MaterialTheme.typography.bodySmall,
170 fontFamily = FontFamily.Monospace,
171 color = MaterialTheme.colorScheme.onSurfaceVariant,
175 modifier = Modifier.padding(start = 8.dp),
176 style = MaterialTheme.typography.bodySmall,
177 fontFamily = FontFamily.Monospace,
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