@@ -13,6 +13,7 @@ import androidx.compose.animation.slideInHorizontally
1313import androidx.compose.animation.slideOutHorizontally
1414import androidx.compose.animation.togetherWith
1515import androidx.compose.foundation.background
16+ import androidx.compose.foundation.horizontalScroll
1617import androidx.compose.foundation.isSystemInDarkTheme
1718import androidx.compose.foundation.layout.Arrangement
1819import androidx.compose.foundation.layout.Box
@@ -30,16 +31,22 @@ import androidx.compose.foundation.layout.systemBarsPadding
3031import androidx.compose.foundation.rememberScrollState
3132import androidx.compose.foundation.shape.CircleShape
3233import androidx.compose.foundation.shape.RoundedCornerShape
34+ import androidx.compose.foundation.text.BasicTextField
35+ import androidx.compose.foundation.text.KeyboardActions
36+ import androidx.compose.foundation.text.KeyboardOptions
3337import androidx.compose.foundation.verticalScroll
3438import androidx.compose.material.icons.Icons
3539import androidx.compose.material.icons.automirrored.filled.ArrowBack
3640import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
3741import androidx.compose.material.icons.automirrored.filled.VolumeUp
3842import androidx.compose.material.icons.filled.BrightnessHigh
3943import androidx.compose.material.icons.filled.Check
44+ import androidx.compose.material.icons.filled.Code
4045import androidx.compose.material.icons.filled.Info
4146import androidx.compose.material.icons.filled.Palette
47+ import androidx.compose.material.icons.filled.PlayArrow
4248import androidx.compose.material.icons.filled.SystemUpdate
49+ import androidx.compose.material3.CircularProgressIndicator
4350import androidx.compose.material3.Icon
4451import androidx.compose.material3.IconButton
4552import androidx.compose.material3.MaterialTheme
@@ -51,13 +58,17 @@ import androidx.compose.runtime.collectAsState
5158import androidx.compose.runtime.getValue
5259import androidx.compose.runtime.mutableStateOf
5360import androidx.compose.runtime.remember
61+ import androidx.compose.runtime.rememberCoroutineScope
5462import androidx.compose.runtime.setValue
5563import androidx.compose.ui.Alignment
5664import androidx.compose.ui.Modifier
5765import androidx.compose.ui.draw.clip
5866import androidx.compose.ui.graphics.Color
67+ import androidx.compose.ui.graphics.SolidColor
5968import androidx.compose.ui.graphics.vector.ImageVector
6069import androidx.compose.ui.platform.LocalContext
70+ import androidx.compose.ui.text.font.FontFamily
71+ import androidx.compose.ui.text.input.ImeAction
6172import androidx.compose.ui.text.style.TextOverflow
6273import androidx.compose.ui.unit.dp
6374import androidx.lifecycle.viewmodel.compose.viewModel
@@ -66,8 +77,11 @@ import dev.helm.core.neumorphicClickable
6677import dev.helm.core.neumorphicShadow
6778import dev.helm.themes.HelmThemeVariant
6879import dev.helm.themes.ThemeDefaults
80+ import kotlinx.coroutines.Dispatchers
81+ import kotlinx.coroutines.launch
82+ import kotlinx.coroutines.withContext
6983
70- private enum class Section { Root , Appearance , Display , Sound , About , Updates }
84+ private enum class Section { Root , Appearance , Display , Sound , About , Updates , Diagnostics }
7185
7286@Composable
7387fun SettingsScreen (
@@ -108,6 +122,9 @@ fun SettingsScreen(
108122 Section .Updates -> UpdatesSection (
109123 onBack = { section = Section .Root },
110124 )
125+ Section .Diagnostics -> DiagnosticsSection (
126+ onBack = { section = Section .Root },
127+ )
111128 }
112129 }
113130}
@@ -172,6 +189,12 @@ private fun SettingsRoot(
172189 subtitle = " Versión y dispositivo" ,
173190 onClick = { onSection(Section .About ) },
174191 )
192+ SettingsRow (
193+ icon = Icons .Filled .Code ,
194+ title = " Diagnóstico" ,
195+ subtitle = " Terminal de comandos" ,
196+ onClick = { onSection(Section .Diagnostics ) },
197+ )
175198 }
176199 }
177200}
@@ -619,3 +642,183 @@ private fun SettingsSectionCard(
619642 content = content,
620643 )
621644}
645+
646+ // ─── Diagnostics ──────────────────────────────────────────────────────────────
647+
648+ private val QUICK_CMDS = listOf (
649+ " getprop ro.product.model" ,
650+ " getprop ro.build.version.release" ,
651+ " dumpsys battery" ,
652+ " ip addr show wlan0" ,
653+ " ping -c 3 8.8.8.8" ,
654+ )
655+
656+ @Composable
657+ private fun DiagnosticsSection (onBack : () -> Unit ) {
658+ val scope = rememberCoroutineScope()
659+ var command by remember { mutableStateOf(" " ) }
660+ var output by remember { mutableStateOf(" Listo." ) }
661+ var running by remember { mutableStateOf(false ) }
662+ val outputScroll = rememberScrollState()
663+ val bg = MaterialTheme .colorScheme.background
664+
665+ fun execute (cmd : String ) {
666+ if (cmd.isBlank() || running) return
667+ running = true
668+ output = " $ $cmd \n\n Ejecutando…"
669+ scope.launch {
670+ val result = withContext(Dispatchers .IO ) {
671+ try {
672+ val proc = ProcessBuilder (" sh" , " -c" , cmd)
673+ .redirectErrorStream(true )
674+ .start()
675+ val text = proc.inputStream.bufferedReader().readText().take(8192 )
676+ proc.waitFor()
677+ " $ $cmd \n\n ${text.ifBlank { " (sin salida)" }} "
678+ } catch (e: java.io.IOException ) {
679+ " $ $cmd \n\n Error: ${e.message} "
680+ }
681+ }
682+ output = result
683+ running = false
684+ outputScroll.animateScrollTo(0 )
685+ }
686+ }
687+
688+ Column (
689+ modifier = Modifier
690+ .fillMaxSize()
691+ .background(bg)
692+ .systemBarsPadding(),
693+ ) {
694+ ScreenHeader (title = " Diagnóstico" , onBack = onBack)
695+
696+ Column (
697+ modifier = Modifier
698+ .fillMaxWidth()
699+ .padding(horizontal = Spacing .lg),
700+ verticalArrangement = Arrangement .spacedBy(Spacing .sm),
701+ ) {
702+ Row (
703+ modifier = Modifier
704+ .fillMaxWidth()
705+ .horizontalScroll(rememberScrollState()),
706+ horizontalArrangement = Arrangement .spacedBy(Spacing .sm),
707+ ) {
708+ QUICK_CMDS .forEach { cmd ->
709+ QuickCommandChip (text = cmd) {
710+ command = cmd
711+ execute(cmd)
712+ }
713+ }
714+ }
715+
716+ Row (
717+ verticalAlignment = Alignment .CenterVertically ,
718+ horizontalArrangement = Arrangement .spacedBy(Spacing .sm),
719+ modifier = Modifier .fillMaxWidth(),
720+ ) {
721+ BasicTextField (
722+ value = command,
723+ onValueChange = { command = it },
724+ modifier = Modifier .weight(1f ),
725+ textStyle = MaterialTheme .typography.bodyMedium.copy(
726+ color = MaterialTheme .colorScheme.onBackground,
727+ fontFamily = FontFamily .Monospace ,
728+ ),
729+ cursorBrush = SolidColor (MaterialTheme .colorScheme.primary),
730+ singleLine = true ,
731+ keyboardOptions = KeyboardOptions (imeAction = ImeAction .Done ),
732+ keyboardActions = KeyboardActions (onDone = { execute(command.trim()) }),
733+ decorationBox = { inner ->
734+ Box (
735+ modifier = Modifier
736+ .neumorphicShadow(backgroundColor = bg, cornerRadius = 12 .dp, elevation = 4 .dp)
737+ .background(bg, RoundedCornerShape (12 .dp))
738+ .padding(horizontal = Spacing .md, vertical = Spacing .sm)
739+ .defaultMinSize(minHeight = 56 .dp),
740+ contentAlignment = Alignment .CenterStart ,
741+ ) {
742+ if (command.isEmpty()) {
743+ Text (
744+ text = " $ comando…" ,
745+ style = MaterialTheme .typography.bodyMedium.copy(fontFamily = FontFamily .Monospace ),
746+ color = MaterialTheme .colorScheme.onSurfaceVariant.copy(alpha = 0.5f ),
747+ )
748+ }
749+ inner()
750+ }
751+ },
752+ )
753+
754+ Box (
755+ modifier = Modifier
756+ .size(56 .dp)
757+ .neumorphicClickable(
758+ onClick = { execute(command.trim()) },
759+ cornerRadius = 14 .dp,
760+ elevation = 4 .dp,
761+ ),
762+ contentAlignment = Alignment .Center ,
763+ ) {
764+ if (running) {
765+ CircularProgressIndicator (
766+ modifier = Modifier .size(24 .dp),
767+ color = MaterialTheme .colorScheme.primary,
768+ strokeWidth = 2 .dp,
769+ )
770+ } else {
771+ Icon (
772+ imageVector = Icons .Filled .PlayArrow ,
773+ contentDescription = " Ejecutar" ,
774+ tint = MaterialTheme .colorScheme.primary,
775+ modifier = Modifier .size(28 .dp),
776+ )
777+ }
778+ }
779+ }
780+ }
781+
782+ Box (
783+ modifier = Modifier
784+ .weight(1f )
785+ .fillMaxWidth()
786+ .padding(horizontal = Spacing .lg, vertical = Spacing .md)
787+ .neumorphicShadow(backgroundColor = bg, cornerRadius = 16 .dp, elevation = 6 .dp)
788+ .background(bg, RoundedCornerShape (16 .dp))
789+ .clip(RoundedCornerShape (16 .dp)),
790+ ) {
791+ Column (
792+ modifier = Modifier
793+ .fillMaxSize()
794+ .verticalScroll(outputScroll)
795+ .padding(Spacing .md),
796+ ) {
797+ Text (
798+ text = output,
799+ style = MaterialTheme .typography.bodySmall.copy(fontFamily = FontFamily .Monospace ),
800+ color = MaterialTheme .colorScheme.onBackground,
801+ )
802+ }
803+ }
804+ }
805+ }
806+
807+ @Composable
808+ private fun QuickCommandChip (text : String , onClick : () -> Unit ) {
809+ val bg = MaterialTheme .colorScheme.background
810+ Box (
811+ modifier = Modifier
812+ .defaultMinSize(minHeight = 40 .dp)
813+ .neumorphicClickable(onClick = onClick, cornerRadius = 20 .dp, elevation = 3 .dp)
814+ .padding(horizontal = Spacing .md, vertical = Spacing .sm),
815+ contentAlignment = Alignment .Center ,
816+ ) {
817+ Text (
818+ text = text,
819+ style = MaterialTheme .typography.labelSmall.copy(fontFamily = FontFamily .Monospace ),
820+ color = MaterialTheme .colorScheme.onSurfaceVariant,
821+ maxLines = 1 ,
822+ )
823+ }
824+ }
0 commit comments