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.Row
8import androidx.compose.foundation.layout.fillMaxSize
9import androidx.compose.foundation.layout.padding
10import androidx.compose.foundation.layout.width
11import androidx.compose.foundation.lazy.LazyColumn
12import androidx.compose.foundation.lazy.itemsIndexed
13import androidx.compose.foundation.rememberScrollState
14import androidx.compose.foundation.verticalScroll
15import androidx.compose.material.icons.Icons
16import androidx.compose.material.icons.automirrored.filled.ArrowBack
17import androidx.compose.material.icons.filled.ContentCopy
18import androidx.compose.material.icons.filled.Share
19import androidx.compose.material3.CircularProgressIndicator
20import androidx.compose.material3.ExperimentalMaterial3Api
21import androidx.compose.material3.Icon
22import androidx.compose.material3.IconButton
23import androidx.compose.material3.MaterialTheme
24import androidx.compose.material3.Scaffold
25import androidx.compose.material3.Text
26import androidx.compose.material3.TopAppBar
27import androidx.compose.runtime.Composable
28import androidx.compose.runtime.getValue
29import androidx.compose.runtime.remember
30import androidx.compose.ui.Alignment
31import androidx.compose.ui.Modifier
32import androidx.compose.ui.platform.LocalContext
33import androidx.compose.ui.semantics.contentDescription
34import androidx.compose.ui.semantics.semantics
35import androidx.compose.ui.text.font.FontFamily
36import androidx.compose.ui.text.style.TextOverflow
37import androidx.compose.ui.unit.dp
38import androidx.compose.ui.res.stringResource
39import androidx.lifecycle.compose.collectAsStateWithLifecycle
40import androidx.lifecycle.viewmodel.compose.viewModel
41import soy.iko.opencode.di.AppContainer
42import soy.iko.opencode.R
43import soy.iko.opencode.ui.components.DiffView
44import soy.iko.opencode.ui.components.copyToClipboard
45import soy.iko.opencode.ui.vmFactory
46import soy.iko.opencode.util.runCatchingCancellable
48@OptIn(ExperimentalMaterial3Api::class)
51 container: AppContainer,
55 val vm: FileViewModel = viewModel(factory = vmFactory { FileViewModel(container, path) })
56 val state by vm.state.collectAsStateWithLifecycle()
57 val context = LocalContext.current
58 val filename = remember(path) { path.substringAfterLast('/') }
59 val shareLabel = stringResource(R.string.share)
64 title = { Text(filename, maxLines = 1, overflow = TextOverflow.Ellipsis) },
66 IconButton(onClick = onBack) {
67 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
71 val content = state.content?.content.orEmpty()
72 if (content.isNotEmpty()) {
73 IconButton(onClick = { copyToClipboard(context, filename, content) }) {
74 Icon(Icons.Filled.ContentCopy, contentDescription = stringResource(R.string.copy))
76 IconButton(onClick = {
77 val send = Intent(Intent.ACTION_SEND).apply {
79 putExtra(Intent.EXTRA_SUBJECT, filename)
80 putExtra(Intent.EXTRA_TEXT, content)
82 runCatchingCancellable { context.startActivity(Intent.createChooser(send, shareLabel)) }
84 showToast(context, context.getString(R.string.no_share_app))
87 Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
94 Box(modifier = Modifier.fillMaxSize().padding(padding)) {
97 val loadingLabel = stringResource(R.string.loading)
98 CircularProgressIndicator(
99 Modifier.align(Alignment.Center).semantics { contentDescription = loadingLabel },
102 state.error != null -> Text(
104 modifier = Modifier.align(Alignment.Center).padding(24.dp),
105 color = MaterialTheme.colorScheme.error,
107 state.content?.isBinary == true -> Text(
108 stringResource(R.string.binary_file),
109 modifier = Modifier.align(Alignment.Center).padding(24.dp),
110 color = MaterialTheme.colorScheme.onSurfaceVariant,
113 val content = state.content
114 if (content?.diff != null && content.diff.isNotBlank()) {
119 .verticalScroll(rememberScrollState())
123 val text = content?.content.orEmpty()
124 if (text.isEmpty()) {
126 stringResource(R.string.empty_file),
127 modifier = Modifier.align(Alignment.Center).padding(24.dp),
128 color = MaterialTheme.colorScheme.onSurfaceVariant,
131 FileTextContent(text)
141private fun FileTextContent(text: String) {
142 val lines = remember(text) { text.split("\n") }
143 val gutterWidth = remember(lines.size) {
144 (lines.size.toString().length.coerceAtLeast(3) * 10).dp
146 val hScrollState = rememberScrollState()
152 itemsIndexed(lines, key = { index, _ -> index }) { index, line ->
153 Row(modifier = Modifier.horizontalScroll(hScrollState)) {
156 modifier = Modifier.width(gutterWidth),
157 style = MaterialTheme.typography.bodySmall,
158 fontFamily = FontFamily.Monospace,
159 color = MaterialTheme.colorScheme.onSurfaceVariant,
163 modifier = Modifier.padding(start = 8.dp),
164 style = MaterialTheme.typography.bodySmall,
165 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