2121package me.kavishdevar.aln.screens
2222
2323import android.annotation.SuppressLint
24+ import android.content.ClipData
25+ import android.content.ClipboardManager
2426import android.content.Context
2527import android.os.Build
28+ import android.widget.Toast
2629import androidx.annotation.RequiresApi
30+ import androidx.compose.foundation.ExperimentalFoundationApi
2731import androidx.compose.foundation.background
2832import androidx.compose.foundation.clickable
33+ import androidx.compose.foundation.combinedClickable
2934import androidx.compose.foundation.isSystemInDarkTheme
35+ import androidx.compose.foundation.layout.Box
3036import androidx.compose.foundation.layout.Column
3137import androidx.compose.foundation.layout.ExperimentalLayoutApi
3238import androidx.compose.foundation.layout.Row
@@ -43,10 +49,14 @@ import androidx.compose.foundation.shape.RoundedCornerShape
4349import androidx.compose.material.icons.Icons
4450import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
4551import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
52+ import androidx.compose.material.icons.filled.Delete
53+ import androidx.compose.material.icons.filled.MoreVert
4654import androidx.compose.material.icons.filled.Send
4755import androidx.compose.material3.Card
4856import androidx.compose.material3.CardDefaults
4957import androidx.compose.material3.CenterAlignedTopAppBar
58+ import androidx.compose.material3.DropdownMenu
59+ import androidx.compose.material3.DropdownMenuItem
5060import androidx.compose.material3.ExperimentalMaterial3Api
5161import androidx.compose.material3.HorizontalDivider
5262import androidx.compose.material3.Icon
@@ -64,11 +74,13 @@ import androidx.compose.runtime.derivedStateOf
6474import androidx.compose.runtime.getValue
6575import androidx.compose.runtime.mutableStateOf
6676import androidx.compose.runtime.remember
77+ import androidx.compose.runtime.rememberCoroutineScope
6778import androidx.compose.ui.Alignment
6879import androidx.compose.ui.Modifier
6980import androidx.compose.ui.draw.scale
7081import androidx.compose.ui.graphics.Color
7182import androidx.compose.ui.platform.LocalContext
83+ import androidx.compose.ui.platform.LocalFocusManager
7284import androidx.compose.ui.text.TextStyle
7385import androidx.compose.ui.text.font.Font
7486import androidx.compose.ui.text.font.FontFamily
@@ -83,10 +95,27 @@ import dev.chrisbanes.haze.hazeChild
8395import dev.chrisbanes.haze.materials.CupertinoMaterials
8496import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
8597import kotlinx.coroutines.flow.MutableStateFlow
98+ import kotlinx.coroutines.delay
99+ import kotlinx.coroutines.launch
100+ import kotlinx.coroutines.Dispatchers
101+ import kotlinx.coroutines.CoroutineScope
86102import me.kavishdevar.aln.R
87103import me.kavishdevar.aln.services.ServiceManager
88104import me.kavishdevar.aln.utils.BatteryStatus
89105import me.kavishdevar.aln.utils.isHeadTrackingData
106+ import me.kavishdevar.aln.composables.StyledSwitch
107+ import androidx.compose.foundation.layout.navigationBarsPadding
108+ import androidx.compose.foundation.layout.imePadding
109+ import androidx.compose.ui.geometry.Offset
110+ import androidx.compose.ui.input.pointer.pointerInput
111+ import androidx.compose.ui.platform.LocalDensity
112+ import androidx.compose.ui.layout.onGloballyPositioned
113+ import androidx.compose.ui.layout.positionInRoot
114+ import androidx.compose.ui.text.style.TextAlign
115+ import androidx.compose.foundation.gestures.detectTapGestures
116+ import androidx.compose.foundation.gestures.detectDragGestures
117+ import androidx.compose.material.icons.filled.Check
118+ import androidx.compose.ui.input.pointer.PointerInputChange
90119
91120data class PacketInfo (
92121 val type : String ,
@@ -286,39 +315,84 @@ fun parseOutgoingPacket(bytes: ByteArray, rawData: String): PacketInfo {
286315 }
287316}
288317
318+ @Composable
319+ fun IOSCheckbox (
320+ checked : Boolean ,
321+ onCheckedChange : (Boolean ) -> Unit ,
322+ modifier : Modifier = Modifier
323+ ) {
324+ Box (
325+ modifier = modifier
326+ .size(24 .dp)
327+ .clickable { onCheckedChange(! checked) },
328+ contentAlignment = Alignment .Center
329+ ) {
330+ if (checked) {
331+ Icon (
332+ imageVector = Icons .Default .Check ,
333+ contentDescription = " Checked" ,
334+ tint = if (isSystemInDarkTheme()) Color (0xFF007AFF ) else Color (0xFF3C6DF5 ),
335+ modifier = Modifier .size(20 .dp)
336+ )
337+ }
338+ }
339+ }
340+
289341@RequiresApi(Build .VERSION_CODES .Q )
290- @OptIn(ExperimentalMaterial3Api ::class , ExperimentalLayoutApi ::class )
342+ @OptIn(ExperimentalMaterial3Api ::class , ExperimentalLayoutApi ::class , ExperimentalFoundationApi :: class )
291343@SuppressLint(" UnusedMaterial3ScaffoldPaddingParameter" , " UnspecifiedRegisterReceiverFlag" )
292344@Composable
293345fun DebugScreen (navController : NavController ) {
294346 val hazeState = remember { HazeState () }
295347 val context = LocalContext .current
296348 val listState = rememberLazyListState()
297349 val scrollOffset by remember { derivedStateOf { listState.firstVisibleItemScrollOffset } }
298- val packetLogsFlow = remember { MutableStateFlow (emptySet<String >()) }
350+ val focusManager = LocalFocusManager .current
351+ val coroutineScope = rememberCoroutineScope()
352+
353+ val showMenu = remember { mutableStateOf(false ) }
354+
355+ val airPodsService = remember { ServiceManager .getService() }
356+ val packetLogs = airPodsService?.packetLogsFlow?.collectAsState(emptySet())?.value ? : emptySet()
357+ val shouldScrollToBottom = remember { mutableStateOf(true ) }
358+
359+ val refreshTrigger = remember { mutableStateOf(0 ) }
360+ LaunchedEffect (refreshTrigger.value) {
361+ while (true ) {
362+ delay(1000 )
363+ refreshTrigger.value = refreshTrigger.value + 1
364+ }
365+ }
366+
299367 val expandedItems = remember { mutableStateOf(setOf<Int >()) }
300-
301- LaunchedEffect (Unit ) {
302- ServiceManager .getService()?.packetLogsFlow?.collect { packetLogsFlow.value = it }
368+
369+ fun copyToClipboard (text : String ) {
370+ val clipboard = context.getSystemService(Context .CLIPBOARD_SERVICE ) as ClipboardManager
371+ val clip = ClipData .newPlainText(" Packet Data" , text)
372+ clipboard.setPrimaryClip(clip)
373+ Toast .makeText(context, " Packet copied to clipboard" , Toast .LENGTH_SHORT ).show()
374+ }
375+
376+ LaunchedEffect (packetLogs.size, refreshTrigger.value) {
377+ if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
378+ listState.animateScrollToItem(packetLogs.size - 1 )
379+ }
303380 }
304- val packetLogs = packetLogsFlow.collectAsState(setOf ()).value
305381
306382 Scaffold (
307383 topBar = {
308384 CenterAlignedTopAppBar (
309385 title = { Text (" Debug" ) },
310386 navigationIcon = {
311387 TextButton (
312- onClick = {
313- navController.popBackStack()
314- },
388+ onClick = { navController.popBackStack() },
315389 shape = RoundedCornerShape (8 .dp),
316390 ) {
317391 val sharedPreferences = context.getSharedPreferences(" settings" , Context .MODE_PRIVATE )
318392 Icon (
319393 Icons .AutoMirrored .Filled .KeyboardArrowLeft ,
320394 contentDescription = " Back" ,
321- tint = if (isSystemInDarkTheme()) Color (0xFF007AFF ) else Color (0xFF3C6DF5 ),
395+ tint = if (isSystemInDarkTheme()) Color (0xFF007AFF ) else Color (0xFF3C6DF5 ),
322396 modifier = Modifier .scale(1.5f )
323397 )
324398 Text (
@@ -332,31 +406,105 @@ fun DebugScreen(navController: NavController) {
332406 )
333407 }
334408 },
335- modifier = Modifier
336- .hazeChild(
337- state = hazeState,
338- style = CupertinoMaterials .thick(),
339- block = {
340- alpha = if (scrollOffset > 0 ) {
341- 1f
342- } else {
343- 0f
344- }
409+ actions = {
410+ Box {
411+ IconButton (onClick = { showMenu.value = true }) {
412+ Icon (
413+ imageVector = Icons .Default .MoreVert ,
414+ contentDescription = " More Options" ,
415+ tint = if (isSystemInDarkTheme()) Color .White else Color .Black
416+ )
345417 }
346- ),
347- colors = TopAppBarDefaults .topAppBarColors(
348- containerColor = Color .Transparent
418+
419+ DropdownMenu (
420+ expanded = showMenu.value,
421+ onDismissRequest = { showMenu.value = false },
422+ modifier = Modifier
423+ .width(250 .dp)
424+ .background(
425+ if (isSystemInDarkTheme()) Color (0xFF1C1B20 ) else Color (0xFFF2F2F7 )
426+ )
427+ .padding(vertical = 4 .dp)
428+ ) {
429+ DropdownMenuItem (
430+ text = {
431+ Row (
432+ verticalAlignment = Alignment .CenterVertically ,
433+ modifier = Modifier .fillMaxWidth()
434+ ) {
435+ Text (
436+ " Auto-scroll" ,
437+ style = TextStyle (
438+ fontSize = 16 .sp,
439+ fontWeight = FontWeight .Normal
440+ )
441+ )
442+ Spacer (modifier = Modifier .weight(1f ))
443+ IOSCheckbox (
444+ checked = shouldScrollToBottom.value,
445+ onCheckedChange = { shouldScrollToBottom.value = it }
446+ )
447+ }
448+ },
449+ onClick = {
450+ shouldScrollToBottom.value = ! shouldScrollToBottom.value
451+ showMenu.value = false
452+ }
453+ )
454+
455+ HorizontalDivider (
456+ color = if (isSystemInDarkTheme()) Color (0xFF3A3A3C ) else Color (0xFFE5E5EA ),
457+ thickness = 0.5 .dp
458+ )
459+
460+ DropdownMenuItem (
461+ text = {
462+ Row (
463+ verticalAlignment = Alignment .CenterVertically ,
464+ modifier = Modifier .fillMaxWidth()
465+ ) {
466+ Text (
467+ " Clear logs" ,
468+ style = TextStyle (
469+ fontSize = 16 .sp,
470+ fontWeight = FontWeight .Normal
471+ )
472+ )
473+ Spacer (modifier = Modifier .weight(1f ))
474+ Icon (
475+ imageVector = Icons .Default .Delete ,
476+ contentDescription = " Clear logs" ,
477+ tint = if (isSystemInDarkTheme()) Color (0xFF007AFF ) else Color (0xFF3C6DF5 )
478+ )
479+ }
480+ },
481+ onClick = {
482+ ServiceManager .getService()?.clearLogs()
483+ expandedItems.value = emptySet()
484+ showMenu.value = false
485+ }
486+ )
487+ }
488+ }
489+ },
490+ modifier = Modifier .hazeChild(
491+ state = hazeState,
492+ style = CupertinoMaterials .thick(),
493+ block = {
494+ alpha = if (scrollOffset > 0 ) 1f else 0f
495+ }
349496 ),
497+ colors = TopAppBarDefaults .topAppBarColors(containerColor = Color .Transparent ),
350498 )
351499 },
352- containerColor = if (isSystemInDarkTheme()) Color (0xFF000000 )
353- else Color (0xFFF2F2F7 ),
500+ containerColor = if (isSystemInDarkTheme()) Color (0xFF000000 ) else Color (0xFFF2F2F7 ),
354501 ) { paddingValues ->
355502 Column (
356503 modifier = Modifier
357504 .fillMaxSize()
358505 .haze(hazeState)
359506 .padding(top = paddingValues.calculateTopPadding())
507+ .navigationBarsPadding()
360508 ) {
361509 LazyColumn (
362510 state = listState,
@@ -374,13 +522,18 @@ fun DebugScreen(navController: NavController) {
374522 modifier = Modifier
375523 .fillMaxWidth()
376524 .padding(vertical = 2 .dp, horizontal = 4 .dp)
377- .clickable {
378- expandedItems.value = if (isExpanded) {
379- expandedItems.value - index
380- } else {
381- expandedItems.value + index
525+ .combinedClickable(
526+ onClick = {
527+ expandedItems.value = if (isExpanded) {
528+ expandedItems.value - index
529+ } else {
530+ expandedItems.value + index
531+ }
532+ },
533+ onLongClick = {
534+ copyToClipboard(packetInfo.rawData)
382535 }
383- } ,
536+ ) ,
384537 elevation = CardDefaults .cardElevation(defaultElevation = 2 .dp),
385538 shape = RoundedCornerShape (4 .dp),
386539 colors = CardDefaults .cardColors(
@@ -476,8 +629,27 @@ fun DebugScreen(navController: NavController) {
476629 trailingIcon = {
477630 IconButton (
478631 onClick = {
479- airPodsService?.value?.sendPacket(packet.value.text)
480- packet.value = TextFieldValue (" " )
632+ if (packet.value.text.isNotBlank()) {
633+ airPodsService?.value?.sendPacket(packet.value.text)
634+ packet.value = TextFieldValue (" " )
635+ focusManager.clearFocus()
636+
637+ if (shouldScrollToBottom.value && packetLogs.isNotEmpty()) {
638+ coroutineScope.launch {
639+ try {
640+ delay(100 )
641+ listState.animateScrollToItem(
642+ index = (packetLogs.size - 1 ).coerceAtLeast(0 ),
643+ scrollOffset = 0
644+ )
645+ } catch (e: Exception ) {
646+ listState.scrollToItem(
647+ index = (packetLogs.size - 1 ).coerceAtLeast(0 )
648+ )
649+ }
650+ }
651+ }
652+ }
481653 }
482654 ) {
483655 @Suppress(" DEPRECATION" )
0 commit comments