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