1package soy.iko.opencode.ui.file
3import android.content.Intent
4import android.widget.Toast
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
47@OptIn(ExperimentalMaterial3Api::class)
50 container: AppContainer,
54 val vm: FileViewModel = viewModel(factory = vmFactory { FileViewModel(container, path) })
55 val state by vm.state.collectAsStateWithLifecycle()
56 val context = LocalContext.current
57 val filename = remember(path) { path.substringAfterLast('/') }
58 val shareLabel = stringResource(R.string.share)
63 title = { Text(filename, maxLines = 1, overflow = TextOverflow.Ellipsis) },
65 IconButton(onClick = onBack) {
66 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
70 val content = state.content?.content.orEmpty()
71 if (content.isNotEmpty()) {
72 IconButton(onClick = { copyToClipboard(context, filename, content) }) {
73 Icon(Icons.Filled.ContentCopy, contentDescription = stringResource(R.string.copy))
75 IconButton(onClick = {
76 val send = Intent(Intent.ACTION_SEND).apply {
78 putExtra(Intent.EXTRA_SUBJECT, filename)
79 putExtra(Intent.EXTRA_TEXT, content)
81 runCatching { context.startActivity(Intent.createChooser(send, shareLabel)) }
83 Toast.makeText(context, context.getString(R.string.no_share_app), Toast.LENGTH_SHORT).show()
86 Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
93 Box(modifier = Modifier.fillMaxSize().padding(padding)) {
96 val loadingLabel = stringResource(R.string.loading)
97 CircularProgressIndicator(
98 Modifier.align(Alignment.Center).semantics { contentDescription = loadingLabel },
101 state.error != null -> Text(
103 modifier = Modifier.align(Alignment.Center).padding(24.dp),
104 color = MaterialTheme.colorScheme.error,
106 state.content?.isBinary == true -> Text(
107 stringResource(R.string.binary_file),
108 modifier = Modifier.align(Alignment.Center).padding(24.dp),
109 color = MaterialTheme.colorScheme.onSurfaceVariant,
112 val content = state.content
113 if (content?.diff != null && content.diff.isNotBlank()) {
118 .verticalScroll(rememberScrollState())
122 val text = content?.content.orEmpty()
123 if (text.isEmpty()) {
125 stringResource(R.string.empty_file),
126 modifier = Modifier.align(Alignment.Center).padding(24.dp),
127 color = MaterialTheme.colorScheme.onSurfaceVariant,
130 FileTextContent(text)
140private fun FileTextContent(text: String) {
141 val lines = remember(text) { text.split("\n") }
142 val gutterWidth = remember(lines.size) {
143 (lines.size.toString().length.coerceAtLeast(3) * 10).dp
145 val hScrollState = rememberScrollState()
151 itemsIndexed(lines, key = { index, _ -> index }) { index, line ->
152 Row(modifier = Modifier.horizontalScroll(hScrollState)) {
155 modifier = Modifier.width(gutterWidth),
156 style = MaterialTheme.typography.bodySmall,
157 fontFamily = FontFamily.Monospace,
158 color = MaterialTheme.colorScheme.onSurfaceVariant,
162 modifier = Modifier.padding(start = 8.dp),
163 style = MaterialTheme.typography.bodySmall,
164 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