Skip to content

Commit fa249f2

Browse files
authored
[feat] add logcat detail bottom sheet (#73)
1 parent 8d75f30 commit fa249f2

File tree

4 files changed

+348
-52
lines changed

4 files changed

+348
-52
lines changed

debugoverlay-core/src/main/kotlin/com/ms/square/debugoverlay/internal/data/model/LogcatEntry.kt

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package com.ms.square.debugoverlay.internal.data.model
22

3+
import androidx.compose.foundation.isSystemInDarkTheme
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.ui.graphics.Color
6+
37
/**
48
* Data class representing a single log entry.
59
*/
@@ -34,3 +38,32 @@ internal enum class LogLevel {
3438
}
3539
}
3640
}
41+
42+
// Log level colors for light theme
43+
private val VERBOSE_COLOR_LIGHT = Color(0xFF757575)
44+
private val DEBUG_COLOR_LIGHT = Color(0xFF2196F3)
45+
private val INFO_COLOR_LIGHT = Color(0xFF4CAF50)
46+
private val WARN_COLOR_LIGHT = Color(0xFFFF9800)
47+
private val ERROR_COLOR_LIGHT = Color(0xFFF44336)
48+
49+
// Log level colors for dark theme
50+
private val VERBOSE_COLOR_DARK = Color(0xFFBDBDBD)
51+
private val DEBUG_COLOR_DARK = Color(0xFF64B5F6)
52+
private val INFO_COLOR_DARK = Color(0xFF81C784)
53+
private val WARN_COLOR_DARK = Color(0xFFFFB74D)
54+
private val ERROR_COLOR_DARK = Color(0xFFE57373)
55+
56+
/**
57+
* Get the color for this log level based on the current theme.
58+
*/
59+
@Composable
60+
internal fun LogLevel.toColor(): Color {
61+
val isDark = isSystemInDarkTheme()
62+
return when (this) {
63+
LogLevel.VERBOSE -> if (isDark) VERBOSE_COLOR_DARK else VERBOSE_COLOR_LIGHT
64+
LogLevel.DEBUG -> if (isDark) DEBUG_COLOR_DARK else DEBUG_COLOR_LIGHT
65+
LogLevel.INFO -> if (isDark) INFO_COLOR_DARK else INFO_COLOR_LIGHT
66+
LogLevel.WARN -> if (isDark) WARN_COLOR_DARK else WARN_COLOR_LIGHT
67+
LogLevel.ERROR -> if (isDark) ERROR_COLOR_DARK else ERROR_COLOR_LIGHT
68+
}
69+
}
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
package com.ms.square.debugoverlay.internal.ui
2+
3+
import android.content.ClipData
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Arrangement
6+
import androidx.compose.foundation.layout.Box
7+
import androidx.compose.foundation.layout.Column
8+
import androidx.compose.foundation.layout.Row
9+
import androidx.compose.foundation.layout.fillMaxHeight
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.rememberScrollState
14+
import androidx.compose.foundation.shape.RoundedCornerShape
15+
import androidx.compose.foundation.verticalScroll
16+
import androidx.compose.material.icons.Icons
17+
import androidx.compose.material.icons.filled.Close
18+
import androidx.compose.material3.Button
19+
import androidx.compose.material3.ExperimentalMaterial3Api
20+
import androidx.compose.material3.Icon
21+
import androidx.compose.material3.IconButton
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.ModalBottomSheet
24+
import androidx.compose.material3.Surface
25+
import androidx.compose.material3.Text
26+
import androidx.compose.material3.rememberModalBottomSheetState
27+
import androidx.compose.runtime.Composable
28+
import androidx.compose.runtime.rememberCoroutineScope
29+
import androidx.compose.ui.Alignment
30+
import androidx.compose.ui.Modifier
31+
import androidx.compose.ui.platform.ClipEntry
32+
import androidx.compose.ui.platform.LocalClipboard
33+
import androidx.compose.ui.platform.LocalContext
34+
import androidx.compose.ui.res.stringResource
35+
import androidx.compose.ui.text.font.FontFamily
36+
import androidx.compose.ui.text.font.FontWeight
37+
import androidx.compose.ui.unit.dp
38+
import androidx.compose.ui.unit.sp
39+
import com.ms.square.debugoverlay.core.R
40+
import com.ms.square.debugoverlay.internal.data.model.LogcatEntry
41+
import com.ms.square.debugoverlay.internal.data.model.toColor
42+
import kotlinx.coroutines.launch
43+
44+
private const val BOTTOM_SHEET_HEIGHT_FRACTION = 0.8f
45+
private const val TIMESTAMP_DISPLAY_LENGTH = 12 // HH:MM:SS.mmm
46+
47+
@OptIn(ExperimentalMaterial3Api::class)
48+
@Composable
49+
internal fun LogEntryDetailBottomSheet(logEntry: LogcatEntry, onDismiss: () -> Unit, onFilterTag: (String) -> Unit) {
50+
val sheetState = rememberModalBottomSheetState(
51+
skipPartiallyExpanded = true
52+
)
53+
54+
ModalBottomSheet(
55+
onDismissRequest = onDismiss,
56+
sheetState = sheetState,
57+
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
58+
tonalElevation = 3.dp,
59+
shape = RoundedCornerShape(topStart = 28.dp, topEnd = 28.dp),
60+
dragHandle = {
61+
Box(
62+
modifier = Modifier
63+
.padding(top = 12.dp, bottom = 8.dp)
64+
.size(width = 32.dp, height = 4.dp)
65+
.background(
66+
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f),
67+
shape = RoundedCornerShape(2.dp)
68+
)
69+
)
70+
}
71+
) {
72+
LogEntryDetailContent(
73+
logEntry = logEntry,
74+
onDismiss = onDismiss,
75+
onFilterTag = onFilterTag
76+
)
77+
}
78+
}
79+
80+
@Composable
81+
private fun LogEntryDetailContent(
82+
logEntry: LogcatEntry,
83+
onDismiss: () -> Unit,
84+
onFilterTag: (String) -> Unit,
85+
modifier: Modifier = Modifier,
86+
) {
87+
Column(
88+
modifier = modifier
89+
.fillMaxWidth()
90+
.fillMaxHeight(BOTTOM_SHEET_HEIGHT_FRACTION)
91+
) {
92+
DetailHeader(onDismiss = onDismiss)
93+
94+
Column(
95+
modifier = Modifier
96+
.weight(1f)
97+
.verticalScroll(rememberScrollState())
98+
.padding(horizontal = 16.dp)
99+
) {
100+
DetailMetadataRow(logEntry = logEntry)
101+
DetailMessageSection(message = logEntry.message)
102+
}
103+
104+
DetailActionButtons(
105+
logEntry = logEntry,
106+
onFilterTag = onFilterTag
107+
)
108+
}
109+
}
110+
111+
@Composable
112+
private fun DetailHeader(onDismiss: () -> Unit, modifier: Modifier = Modifier) {
113+
Row(
114+
modifier = modifier
115+
.fillMaxWidth()
116+
.padding(horizontal = 16.dp, vertical = 4.dp),
117+
verticalAlignment = Alignment.CenterVertically
118+
) {
119+
Text(
120+
text = stringResource(R.string.debugoverlay_log_details),
121+
style = MaterialTheme.typography.titleMedium,
122+
modifier = Modifier.weight(1f)
123+
)
124+
IconButton(onClick = onDismiss) {
125+
Icon(
126+
imageVector = Icons.Default.Close,
127+
contentDescription = stringResource(R.string.debugoverlay_close_description),
128+
tint = MaterialTheme.colorScheme.onSurface
129+
)
130+
}
131+
}
132+
}
133+
134+
@Composable
135+
private fun DetailMetadataRow(logEntry: LogcatEntry, modifier: Modifier = Modifier) {
136+
Row(
137+
modifier = modifier
138+
.fillMaxWidth()
139+
.padding(vertical = 12.dp),
140+
horizontalArrangement = Arrangement.spacedBy(16.dp)
141+
) {
142+
// Level
143+
Column {
144+
Text(
145+
text = stringResource(R.string.debugoverlay_level_label),
146+
style = MaterialTheme.typography.labelSmall,
147+
color = MaterialTheme.colorScheme.onSurfaceVariant
148+
)
149+
Surface(
150+
shape = RoundedCornerShape(12.dp),
151+
color = logEntry.level.toColor().copy(alpha = 0.2f)
152+
) {
153+
Text(
154+
text = logEntry.level.name,
155+
style = MaterialTheme.typography.labelMedium,
156+
fontWeight = FontWeight.Bold,
157+
color = logEntry.level.toColor(),
158+
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp)
159+
)
160+
}
161+
}
162+
163+
// Tag
164+
Column(modifier = Modifier.weight(1f)) {
165+
Text(
166+
text = stringResource(R.string.debugoverlay_tag_label),
167+
style = MaterialTheme.typography.labelSmall,
168+
color = MaterialTheme.colorScheme.onSurfaceVariant
169+
)
170+
Text(
171+
text = logEntry.tag,
172+
style = MaterialTheme.typography.bodyMedium,
173+
fontFamily = FontFamily.Monospace,
174+
color = logEntry.level.toColor(),
175+
fontWeight = FontWeight.SemiBold
176+
)
177+
}
178+
179+
// Time
180+
Column {
181+
Text(
182+
text = stringResource(R.string.debugoverlay_time_label),
183+
style = MaterialTheme.typography.labelSmall,
184+
color = MaterialTheme.colorScheme.onSurfaceVariant
185+
)
186+
Text(
187+
text = logEntry.timestamp.takeLast(TIMESTAMP_DISPLAY_LENGTH),
188+
style = MaterialTheme.typography.bodyMedium,
189+
fontFamily = FontFamily.Monospace,
190+
color = MaterialTheme.colorScheme.onSurface
191+
)
192+
}
193+
}
194+
}
195+
196+
@Composable
197+
private fun DetailMessageSection(message: String, modifier: Modifier = Modifier) {
198+
Column(modifier = modifier.fillMaxWidth()) {
199+
Text(
200+
text = stringResource(R.string.debugoverlay_message_label),
201+
style = MaterialTheme.typography.labelSmall,
202+
color = MaterialTheme.colorScheme.onSurfaceVariant,
203+
modifier = Modifier.padding(bottom = 8.dp)
204+
)
205+
Surface(
206+
shape = RoundedCornerShape(8.dp),
207+
color = MaterialTheme.colorScheme.surfaceContainer,
208+
modifier = Modifier.fillMaxWidth()
209+
) {
210+
Text(
211+
text = message,
212+
style = MaterialTheme.typography.bodyMedium,
213+
fontFamily = FontFamily.Monospace,
214+
color = MaterialTheme.colorScheme.onSurface,
215+
fontSize = 13.sp,
216+
lineHeight = 18.sp,
217+
modifier = Modifier.padding(12.dp)
218+
)
219+
}
220+
}
221+
}
222+
223+
@Composable
224+
private fun DetailActionButtons(logEntry: LogcatEntry, onFilterTag: (String) -> Unit, modifier: Modifier = Modifier) {
225+
val clipboard = LocalClipboard.current
226+
val context = LocalContext.current
227+
val scope = rememberCoroutineScope()
228+
229+
Row(
230+
modifier = modifier
231+
.fillMaxWidth()
232+
.padding(16.dp),
233+
horizontalArrangement = Arrangement.spacedBy(12.dp)
234+
) {
235+
// Copy button
236+
Button(
237+
onClick = {
238+
scope.launch {
239+
val clipboardLabel = context.getString(R.string.debugoverlay_clipboard_label)
240+
val clipEntry = ClipEntry(ClipData.newPlainText(clipboardLabel, logEntry.rawLine))
241+
clipboard.setClipEntry(clipEntry)
242+
}
243+
},
244+
modifier = Modifier.weight(1f)
245+
) {
246+
Text(stringResource(R.string.debugoverlay_copy))
247+
}
248+
249+
// Filter Tag button
250+
Button(
251+
onClick = { onFilterTag(logEntry.tag) },
252+
modifier = Modifier.weight(1f)
253+
) {
254+
Text(stringResource(R.string.debugoverlay_filter_tag))
255+
}
256+
}
257+
}

0 commit comments

Comments
 (0)