git.iko.soy/opencode-android
on e37d1bb help sign in
147 lines 6.7 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.Column
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.items
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.text.font.FontFamily
34import androidx.compose.ui.text.style.TextOverflow
35import androidx.compose.ui.unit.dp
36import androidx.lifecycle.compose.collectAsStateWithLifecycle
37import androidx.lifecycle.viewmodel.compose.viewModel
38import soy.iko.opencode.di.AppContainer
39import soy.iko.opencode.ui.components.DiffView
40import soy.iko.opencode.ui.components.copyToClipboard
41import soy.iko.opencode.ui.vmFactory
43@OptIn(ExperimentalMaterial3Api::class)
44@Composable
45fun FileViewScreen(
46 container: AppContainer,
47 path: String,
48 onBack: () -> Unit,
49) {
50 val vm: FileViewModel = viewModel(factory = vmFactory { FileViewModel(container, path) })
51 val state by vm.state.collectAsStateWithLifecycle()
52 val context = LocalContext.current
53 val filename = remember(path) { path.substringAfterLast('/') }
55 Scaffold(
56 topBar = {
57 TopAppBar(
58 title = { Text(filename, maxLines = 1, overflow = TextOverflow.Ellipsis) },
59 navigationIcon = {
60 IconButton(onClick = onBack) {
61 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
62 }
63 },
64 actions = {
65 val content = state.content?.content.orEmpty()
66 if (content.isNotEmpty()) {
67 IconButton(onClick = { copyToClipboard(context, filename, content) }) {
68 Icon(Icons.Filled.ContentCopy, contentDescription = "Copy")
69 }
70 IconButton(onClick = {
71 val send = Intent(Intent.ACTION_SEND).apply {
72 type = "text/plain"
73 putExtra(Intent.EXTRA_SUBJECT, filename)
74 putExtra(Intent.EXTRA_TEXT, content)
75 }
76 context.startActivity(Intent.createChooser(send, "Share $filename"))
77 }) {
78 Icon(Icons.Filled.Share, contentDescription = "Share")
79 }
80 }
81 },
82 )
83 },
84 ) { padding ->
85 Box(modifier = Modifier.fillMaxSize().padding(padding)) {
86 when {
87 state.loading -> CircularProgressIndicator(Modifier.align(Alignment.Center))
88 state.error != null -> Text(
89 state.error!!,
90 modifier = Modifier.align(Alignment.Center).padding(24.dp),
91 color = MaterialTheme.colorScheme.error,
92 )
93 state.content?.isBinary == true -> Text(
94 "Binary file — preview not supported.",
95 modifier = Modifier.align(Alignment.Center).padding(24.dp),
96 color = MaterialTheme.colorScheme.onSurfaceVariant,
97 )
98 else -> {
99 val content = state.content
100 val scrollState = rememberScrollState()
101 if (content?.diff != null && content.diff.isNotBlank()) {
102 DiffView(
103 diff = content.diff,
104 modifier = Modifier
105 .fillMaxSize()
106 .padding(padding),
107 )
108 } else {
109 val text = content?.content.orEmpty()
110 val lines = remember(text) { text.split("\n") }
111 val gutterWidth = remember(lines.size) {
112 (lines.size.toString().length.coerceAtLeast(3) * 10).dp
113 }
114 Row(
115 modifier = Modifier
116 .fillMaxSize()
117 .verticalScroll(scrollState)
118 .horizontalScroll(rememberScrollState())
119 .padding(8.dp),
120 ) {
121 // Line-number gutter
122 Column(modifier = Modifier.width(gutterWidth)) {
123 lines.forEachIndexed { i, _ ->
124 Text(
125 "${i + 1}",
126 style = MaterialTheme.typography.bodySmall,
127 fontFamily = FontFamily.Monospace,
128 color = MaterialTheme.colorScheme.onSurfaceVariant,
129 )
130 }
131 }
132 Column(modifier = Modifier.padding(start = 8.dp)) {
133 lines.forEach { line ->
134 Text(
135 line,
136 style = MaterialTheme.typography.bodySmall,
137 fontFamily = FontFamily.Monospace,
138 )
139 }
140 }
141 }
142 }
143 }
144 }
145 }
146 }