git.iko.soy/opencode-android
on 325e359 help sign in
214 lines 10.0 kB history raw
1package soy.iko.opencode.ui.settings
3import android.content.Intent
4import android.util.Log
5import androidx.compose.foundation.clickable
6import androidx.compose.foundation.layout.Arrangement
7import androidx.compose.foundation.layout.Box
8import androidx.compose.foundation.layout.Column
9import androidx.compose.foundation.layout.Row
10import androidx.compose.foundation.layout.Spacer
11import androidx.compose.foundation.layout.fillMaxSize
12import androidx.compose.foundation.layout.fillMaxWidth
13import androidx.compose.foundation.layout.padding
14import androidx.compose.foundation.layout.size
15import androidx.compose.foundation.lazy.LazyColumn
16import androidx.compose.foundation.lazy.items
17import androidx.compose.material.icons.Icons
18import androidx.compose.material.icons.automirrored.filled.ArrowBack
19import androidx.compose.material.icons.filled.BugReport
20import androidx.compose.material.icons.filled.DeleteSweep
21import androidx.compose.material.icons.filled.Share
22import androidx.compose.material3.AlertDialog
23import androidx.compose.material3.CircularProgressIndicator
24import androidx.compose.material3.ExperimentalMaterial3Api
25import androidx.compose.material3.HorizontalDivider
26import androidx.compose.material3.Icon
27import androidx.compose.material3.IconButton
28import androidx.compose.material3.MaterialTheme
29import androidx.compose.material3.Scaffold
30import androidx.compose.material3.Text
31import androidx.compose.material3.TextButton
32import androidx.compose.material3.TopAppBar
33import androidx.compose.runtime.Composable
34import androidx.compose.runtime.LaunchedEffect
35import androidx.compose.runtime.getValue
36import androidx.compose.runtime.mutableStateOf
37import androidx.compose.runtime.remember
38import androidx.compose.runtime.rememberCoroutineScope
39import androidx.compose.runtime.saveable.rememberSaveable
40import androidx.compose.runtime.setValue
41import kotlinx.coroutines.Dispatchers
42import kotlinx.coroutines.launch
43import kotlinx.coroutines.withContext
44import soy.iko.opencode.util.runCatchingCancellable
45import androidx.compose.ui.Alignment
46import androidx.compose.ui.Modifier
47import androidx.compose.ui.platform.LocalContext
48import androidx.compose.ui.res.stringResource
49import androidx.compose.ui.semantics.Role
50import androidx.compose.ui.semantics.contentDescription
51import androidx.compose.ui.semantics.heading
52import androidx.compose.ui.semantics.role
53import androidx.compose.ui.semantics.semantics
54import androidx.compose.ui.unit.dp
55import androidx.lifecycle.compose.collectAsStateWithLifecycle
56import soy.iko.opencode.R
57import soy.iko.opencode.data.repo.CrashLogger
58import soy.iko.opencode.ui.components.rememberRelativeTime
60@OptIn(ExperimentalMaterial3Api::class)
61@Composable
62fun DiagnosticsScreen(onBack: () -> Unit) {
63 val context = LocalContext.current
64 val logger = remember { CrashLogger.get(context) }
65 val reports by logger.reports.collectAsStateWithLifecycle()
66 var viewing by rememberSaveable { mutableStateOf<String?>(null) }
67 val shareScope = rememberCoroutineScope()
68 val shareLabel = stringResource(R.string.share)
70 Scaffold(
71 topBar = {
72 TopAppBar(
73 title = { Text(stringResource(R.string.diagnostics)) },
74 navigationIcon = {
75 IconButton(onClick = onBack) {
76 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = stringResource(R.string.back))
77 }
78 },
79 actions = {
80 if (reports.isNotEmpty()) {
81 IconButton(onClick = {
82 logger.clearAll()
83 }) {
84 Icon(Icons.Filled.DeleteSweep, contentDescription = stringResource(R.string.clear_all))
85 }
86 }
87 },
88 )
89 },
90 ) { padding ->
91 Column(modifier = Modifier.fillMaxSize().padding(padding)) {
92 Text(
93 stringResource(R.string.diagnostics_desc),
94 style = MaterialTheme.typography.bodySmall,
95 color = MaterialTheme.colorScheme.onSurfaceVariant,
96 modifier = Modifier.padding(16.dp),
97 )
98 Text(
99 stringResource(R.string.crash_reports),
100 style = MaterialTheme.typography.titleSmall,
101 color = MaterialTheme.colorScheme.primary,
102 modifier = Modifier
103 .padding(start = 16.dp, bottom = 8.dp)
104 .semantics { heading() },
105 )
106 if (reports.isEmpty()) {
107 Text(
108 stringResource(R.string.no_crash_reports),
109 style = MaterialTheme.typography.bodyMedium,
110 color = MaterialTheme.colorScheme.onSurfaceVariant,
111 modifier = Modifier.padding(24.dp),
112 )
113 } else {
114 LazyColumn(modifier = Modifier.fillMaxSize()) {
115 items(reports, key = { it.fileName }) { report ->
116 Row(
117 modifier = Modifier
118 .fillMaxWidth()
119 .clickable(role = Role.Button) { viewing = report.fileName }
120 .padding(horizontal = 16.dp, vertical = 12.dp),
121 verticalAlignment = Alignment.CenterVertically,
122 horizontalArrangement = Arrangement.spacedBy(12.dp),
123 ) {
124 Icon(
125 Icons.Filled.BugReport,
126 contentDescription = null,
127 tint = MaterialTheme.colorScheme.error,
128 )
129 Column(modifier = Modifier.weight(1f)) {
130 Text(report.preview, style = MaterialTheme.typography.bodyMedium, maxLines = 1)
131 Text(
132 rememberRelativeTime(report.timestamp),
133 style = MaterialTheme.typography.bodySmall,
134 color = MaterialTheme.colorScheme.onSurfaceVariant,
135 )
136 }
137 IconButton(onClick = {
138 shareScope.launch {
139 val content = withContext(Dispatchers.IO) {
140 logger.readReport(report.fileName).orEmpty()
141 }
142 val send = Intent(Intent.ACTION_SEND).apply {
143 type = "text/plain"
144 putExtra(Intent.EXTRA_SUBJECT, shareLabel)
145 putExtra(Intent.EXTRA_TEXT, content)
146 }
147 runCatchingCancellable { context.startActivity(Intent.createChooser(send, shareLabel)) }
148 .onFailure { Log.w("Diagnostics", "Failed to share crash report", it) }
149 }
150 }) {
151 Icon(Icons.Filled.Share, contentDescription = stringResource(R.string.share))
152 }
153 }
154 HorizontalDivider()
155 }
156 }
157 }
158 }
159 }
161 val reportName = viewing
162 if (reportName != null) {
163 var reportContent by remember(reportName) { mutableStateOf<String?>(null) }
164 LaunchedEffect(reportName) {
165 reportContent = withContext(Dispatchers.IO) {
166 runCatching { logger.readReport(reportName) }.getOrNull()
167 }
168 }
169 val content = reportContent
170 AlertDialog(
171 onDismissRequest = { viewing = null },
172 confirmButton = {
173 TextButton(onClick = { viewing = null }) { Text(stringResource(R.string.back)) }
174 },
175 dismissButton = {
176 Row {
177 val shareLabel2 = stringResource(R.string.share)
178 TextButton(onClick = {
179 val send = Intent(Intent.ACTION_SEND).apply {
180 type = "text/plain"
181 putExtra(Intent.EXTRA_SUBJECT, shareLabel2)
182 putExtra(Intent.EXTRA_TEXT, content.orEmpty())
183 }
184 runCatchingCancellable { context.startActivity(Intent.createChooser(send, shareLabel2)) }
185 .onFailure { Log.w("Diagnostics", "Failed to share crash report", it) }
186 }) { Text(stringResource(R.string.share)) }
187 Spacer(Modifier.size(8.dp))
188 TextButton(onClick = {
189 logger.deleteReport(reportName)
190 viewing = null
191 }) { Text(stringResource(R.string.delete)) }
192 }
193 },
194 title = { Text(stringResource(R.string.crash_report)) },
195 text = {
196 if (content != null) {
197 Text(
198 content,
199 style = MaterialTheme.typography.bodySmall,
200 modifier = Modifier.fillMaxWidth(),
201 )
202 } else {
203 val loadingLabel = stringResource(R.string.loading)
204 Box(
205 modifier = Modifier.fillMaxWidth().padding(vertical = 24.dp),
206 contentAlignment = Alignment.Center,
207 ) {
208 CircularProgressIndicator(Modifier.semantics { contentDescription = loadingLabel })
209 }
210 }
211 },
212 )
213 }