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