git.iko.soy/opencode-android
on d29e57b help sign in
182 lines 7.9 kB history raw
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)
53@Composable
54fun FileViewScreen(
55 container: AppContainer,
56 path: String,
57 onBack: () -> Unit,
58) {
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)
65 Scaffold(
66 topBar = {
67 TopAppBar(
68 title = { Text(filename, maxLines = 1, overflow = TextOverflow.Ellipsis) },
69 navigationIcon = {
70 IconButton(onClick = onBack) {
71 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
72 }
73 },
74 actions = {
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))
79 }
80 IconButton(onClick = {
81 val send = Intent(Intent.ACTION_SEND).apply {
82 type = "text/plain"
83 putExtra(Intent.EXTRA_SUBJECT, filename)
84 putExtra(Intent.EXTRA_TEXT, content)
85 }
86 runCatchingCancellable { context.startActivity(Intent.createChooser(send, shareLabel)) }
87 .onFailure {
88 showToast(context, context.getString(R.string.no_share_app))
89 }
90 }) {
91 Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
92 }
93 }
94 },
95 )
96 },
97 ) { padding ->
98 Box(modifier = Modifier.fillMaxSize().padding(padding)) {
99 when {
100 state.loading -> {
101 val loadingLabel = stringResource(R.string.loading)
102 CircularProgressIndicator(
103 Modifier.align(Alignment.Center).semantics { contentDescription = loadingLabel },
104 )
105 }
106 state.error != null -> Column(
107 modifier = Modifier.align(Alignment.Center).padding(24.dp),
108 horizontalAlignment = Alignment.CenterHorizontally,
109 ) {
110 Text(
111 state.error ?: "",
112 color = MaterialTheme.colorScheme.error,
113 )
114 Spacer(Modifier.size(12.dp))
115 TextButton(onClick = { vm.reload() }) {
116 Text(stringResource(R.string.retry))
117 }
118 }
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,
123 )
124 else -> {
125 val content = state.content
126 if (content?.diff != null && content.diff.isNotBlank()) {
127 DiffView(
128 diff = content.diff,
129 modifier = Modifier
130 .fillMaxSize()
131 .verticalScroll(rememberScrollState())
132 .padding(8.dp),
133 )
134 } else {
135 val text = content?.content.orEmpty()
136 if (text.isEmpty()) {
137 Text(
138 stringResource(R.string.empty_file),
139 modifier = Modifier.align(Alignment.Center).padding(24.dp),
140 color = MaterialTheme.colorScheme.onSurfaceVariant,
141 )
142 } else {
143 FileTextContent(text)
144 }
145 }
146 }
147 }
148 }
149 }
152@Composable
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
157 }
158 val hScrollState = rememberScrollState()
159 LazyColumn(
160 modifier = Modifier
161 .fillMaxSize()
162 .padding(8.dp),
163 ) {
164 itemsIndexed(lines, key = { index, _ -> index }) { index, line ->
165 Row(modifier = Modifier.horizontalScroll(hScrollState)) {
166 Text(
167 "${index + 1}",
168 modifier = Modifier.width(gutterWidth),
169 style = MaterialTheme.typography.bodySmall,
170 fontFamily = FontFamily.Monospace,
171 color = MaterialTheme.colorScheme.onSurfaceVariant,
172 )
173 Text(
174 line,
175 modifier = Modifier.padding(start = 8.dp),
176 style = MaterialTheme.typography.bodySmall,
177 fontFamily = FontFamily.Monospace,
178 )
179 }
180 }
181 }