package exh.debug import android.app.Activity import android.util.Log import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.background import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.unit.dp import eu.kanade.core.prefs.PreferenceMutableState import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.Divider import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.PreferenceRow import eu.kanade.presentation.components.Scaffold import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.SwitchPreference import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.topPaddingValues import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController import exh.util.capitalize import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.Locale import kotlin.reflect.KFunction import kotlin.reflect.KVisibility import kotlin.reflect.full.declaredFunctions class SettingsDebugController : BasicFullComposeController() { data class DebugToggle(val name: String, val pref: PreferenceMutableState, val default: Boolean) @Composable override fun ComposeContent() { val functions by produceState, String>>?>(initialValue = null) { value = withContext(Dispatchers.Default) { DebugFunctions::class.declaredFunctions.filter { it.visibility == KVisibility.PUBLIC }.map { it to it.name.replace("(.)(\\p{Upper})".toRegex(), "$1 $2") .lowercase(Locale.getDefault()) .capitalize(Locale.getDefault()) } } } val toggles by produceState(initialValue = emptyList()) { value = withContext(Dispatchers.Default) { DebugToggles.values().map { DebugToggle(it.name, it.asPref(viewScope), it.default) } } } Scaffold( topBar = { scrollBehavior -> AppBar( title = "DEBUG MENU", scrollBehavior = scrollBehavior, ) }, ) { paddingValues -> Crossfade(functions == null) { when (it) { true -> LoadingScreen() false -> FunctionList(paddingValues, functions.orEmpty(), toggles) } } } } @Composable fun FunctionList( paddingValues: PaddingValues, functions: List, String>>, toggles: List, ) { val scope = rememberCoroutineScope() Box(Modifier.fillMaxSize()) { var running by remember { mutableStateOf(false) } var result by remember { mutableStateOf?>(null) } ScrollbarLazyColumn( Modifier.fillMaxSize(), contentPadding = paddingValues + WindowInsets.navigationBars.only(WindowInsetsSides.Vertical).asPaddingValues() + topPaddingValues, ) { item { Text( text = "Functions", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(16.dp), ) } items(functions) { (func, name) -> PreferenceRow( title = name, onClick = { scope.launch(Dispatchers.Default) { val text = try { running = true "Function returned result:\n\n${func.call(DebugFunctions)}" } catch (e: Exception) { "Function threw exception:\n\n${Log.getStackTraceString(e)}" } finally { running = false } result = name to text } }, ) } item { Divider() } item { Text( text = "Toggles", color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(16.dp), ) } items(toggles) { (name, pref, default) -> SwitchPreference( preference = pref, title = name.replace('_', ' ') .lowercase(Locale.getDefault()) .capitalize(Locale.getDefault()), subtitleAnnotated = if (pref.value != default) { AnnotatedString("MODIFIED", SpanStyle(color = Color.Red)) } else { null }, ) } item { Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } } AnimatedVisibility( running && result == null, enter = fadeIn(), exit = fadeOut(), modifier = Modifier.fillMaxSize(), ) { Box( Modifier .fillMaxSize() .background(color = Color.White.copy(alpha = 0.3F)) .pointerInput(running && result == null) { forEachGesture { awaitPointerEventScope { waitForUpOrCancellation()?.consume() } } }, contentAlignment = Alignment.Center, ) { CircularProgressIndicator() } } ResultTextDialog( result = result, onDismissRequest = { result = null }, ) } } @Composable private fun ResultTextDialog(result: Pair?, onDismissRequest: () -> Unit) { if (result != null) { AlertDialog( onDismissRequest = onDismissRequest, title = { Text(text = result.first) }, confirmButton = {}, text = { SelectionContainer(Modifier.verticalScroll(rememberScrollState())) { Text(text = result.second) } }, ) } } override fun onActivityStopped(activity: Activity) { super.onActivityStopped(activity) router.popCurrentController() } }