git.iko.soy/opencode-android
on 0bc1eda help sign in
158 lines 6.8 kB history raw
1package soy.iko.opencode.ui.file
3import android.content.Intent
4import androidx.compose.foundation.horizontalScroll
5import androidx.compose.foundation.layout.Box
6import androidx.compose.foundation.layout.Row
7import androidx.compose.foundation.layout.fillMaxSize
8import androidx.compose.foundation.layout.padding
9import androidx.compose.foundation.layout.width
10import androidx.compose.foundation.lazy.LazyColumn
11import androidx.compose.foundation.lazy.itemsIndexed
12import androidx.compose.foundation.rememberScrollState
13import androidx.compose.foundation.verticalScroll
14import androidx.compose.material.icons.Icons
15import androidx.compose.material.icons.automirrored.filled.ArrowBack
16import androidx.compose.material.icons.filled.ContentCopy
17import androidx.compose.material.icons.filled.Share
18import androidx.compose.material3.CircularProgressIndicator
19import androidx.compose.material3.ExperimentalMaterial3Api
20import androidx.compose.material3.Icon
21import androidx.compose.material3.IconButton
22import androidx.compose.material3.MaterialTheme
23import androidx.compose.material3.Scaffold
24import androidx.compose.material3.Text
25import androidx.compose.material3.TopAppBar
26import androidx.compose.runtime.Composable
27import androidx.compose.runtime.getValue
28import androidx.compose.runtime.remember
29import androidx.compose.ui.Alignment
30import androidx.compose.ui.Modifier
31import androidx.compose.ui.platform.LocalContext
32import androidx.compose.ui.text.font.FontFamily
33import androidx.compose.ui.text.style.TextOverflow
34import androidx.compose.ui.unit.dp
35import androidx.compose.ui.res.stringResource
36import androidx.lifecycle.compose.collectAsStateWithLifecycle
37import androidx.lifecycle.viewmodel.compose.viewModel
38import soy.iko.opencode.di.AppContainer
39import soy.iko.opencode.R
40import soy.iko.opencode.ui.components.DiffView
41import soy.iko.opencode.ui.components.copyToClipboard
42import soy.iko.opencode.ui.vmFactory
44@OptIn(ExperimentalMaterial3Api::class)
45@Composable
46fun FileViewScreen(
47 container: AppContainer,
48 path: String,
49 onBack: () -> Unit,
50) {
51 val vm: FileViewModel = viewModel(factory = vmFactory { FileViewModel(container, path) })
52 val state by vm.state.collectAsStateWithLifecycle()
53 val context = LocalContext.current
54 val filename = remember(path) { path.substringAfterLast('/') }
55 val shareLabel = stringResource(R.string.share)
57 Scaffold(
58 topBar = {
59 TopAppBar(
60 title = { Text(filename, maxLines = 1, overflow = TextOverflow.Ellipsis) },
61 navigationIcon = {
62 IconButton(onClick = onBack) {
63 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
64 }
65 },
66 actions = {
67 val content = state.content?.content.orEmpty()
68 if (content.isNotEmpty()) {
69 IconButton(onClick = { copyToClipboard(context, filename, content) }) {
70 Icon(Icons.Filled.ContentCopy, contentDescription = stringResource(R.string.copy))
71 }
72 IconButton(onClick = {
73 val send = Intent(Intent.ACTION_SEND).apply {
74 type = "text/plain"
75 putExtra(Intent.EXTRA_SUBJECT, filename)
76 putExtra(Intent.EXTRA_TEXT, content)
77 }
78 runCatching { context.startActivity(Intent.createChooser(send, shareLabel)) }
79 }) {
80 Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
81 }
82 }
83 },
84 )
85 },
86 ) { padding ->
87 Box(modifier = Modifier.fillMaxSize().padding(padding)) {
88 when {
89 state.loading -> CircularProgressIndicator(Modifier.align(Alignment.Center))
90 state.error != null -> Text(
91 state.error ?: "",
92 modifier = Modifier.align(Alignment.Center).padding(24.dp),
93 color = MaterialTheme.colorScheme.error,
94 )
95 state.content?.isBinary == true -> Text(
96 stringResource(R.string.binary_file),
97 modifier = Modifier.align(Alignment.Center).padding(24.dp),
98 color = MaterialTheme.colorScheme.onSurfaceVariant,
99 )
100 else -> {
101 val content = state.content
102 if (content?.diff != null && content.diff.isNotBlank()) {
103 DiffView(
104 diff = content.diff,
105 modifier = Modifier
106 .fillMaxSize()
107 .verticalScroll(rememberScrollState())
108 .padding(8.dp),
109 )
110 } else {
111 val text = content?.content.orEmpty()
112 if (text.isEmpty()) {
113 Text(
114 stringResource(R.string.empty_file),
115 modifier = Modifier.align(Alignment.Center).padding(24.dp),
116 color = MaterialTheme.colorScheme.onSurfaceVariant,
117 )
118 } else {
119 FileTextContent(text)
120 }
121 }
122 }
123 }
124 }
125 }
128@Composable
129private fun FileTextContent(text: String) {
130 val lines = remember(text) { text.split("\n") }
131 val gutterWidth = remember(lines.size) {
132 (lines.size.toString().length.coerceAtLeast(3) * 10).dp
133 }
134 val hScrollState = rememberScrollState()
135 LazyColumn(
136 modifier = Modifier
137 .fillMaxSize()
138 .padding(8.dp),
139 ) {
140 itemsIndexed(lines, key = { index, _ -> index }) { index, line ->
141 Row(modifier = Modifier.horizontalScroll(hScrollState)) {
142 Text(
143 "${index + 1}",
144 modifier = Modifier.width(gutterWidth),
145 style = MaterialTheme.typography.bodySmall,
146 fontFamily = FontFamily.Monospace,
147 color = MaterialTheme.colorScheme.onSurfaceVariant,
148 )
149 Text(
150 line,
151 modifier = Modifier.padding(start = 8.dp),
152 style = MaterialTheme.typography.bodySmall,
153 fontFamily = FontFamily.Monospace,
154 )
155 }
156 }
157 }