@@ -40,8 +40,11 @@ import app.morphe.gui.ui.theme.LocalMorpheFont
4040import app.morphe.gui.ui.theme.LocalMorpheCorners
4141import app.morphe.gui.ui.theme.MorpheColors
4242import app.morphe.gui.ui.theme.ThemePreference
43+ import app.morphe.gui.util.AdbManager
44+ import app.morphe.gui.util.DeviceMonitor
4345import app.morphe.gui.util.FileUtils
4446import app.morphe.gui.util.Logger
47+ import kotlinx.coroutines.launch
4548import app.morphe.patcher.apk.ApkSigner
4649import java.awt.Desktop
4750import java.awt.FileDialog
@@ -280,6 +283,18 @@ fun SettingsDialog(
280283
281284 SettingsDivider (borderColor)
282285
286+ // ── Patched App Runtime Logs ──
287+ PatchedAppRuntimeLogsSection (
288+ mono = mono,
289+ accentColor = accents.primary,
290+ borderColor = borderColor,
291+ enabled = ! isPatching,
292+ expanded = collapsibleSectionStates[" RUNTIME LOGS" ] == true ,
293+ onExpandedChange = { onCollapsibleSectionToggle(" RUNTIME LOGS" , it) }
294+ )
295+
296+ SettingsDivider (borderColor)
297+
283298 // ── Patch Sources ──
284299 PatchSourcesSection (
285300 sources = patchSources,
@@ -3118,3 +3133,160 @@ private fun resolveGitHubUrl(input: String): String? {
31183133
31193134 return null
31203135}
3136+
3137+ // ── Patched App Runtime Logs Section ──
3138+
3139+ private sealed interface RuntimeLogsStatus {
3140+ data object Idle : RuntimeLogsStatus
3141+ data object Clearing : RuntimeLogsStatus
3142+ data object Saving : RuntimeLogsStatus
3143+ data object Cleared : RuntimeLogsStatus
3144+ data class Saved (val file : File , val lineCount : Int ) : RuntimeLogsStatus
3145+ data class Error (val message : String ) : RuntimeLogsStatus
3146+ }
3147+
3148+ @Composable
3149+ private fun PatchedAppRuntimeLogsSection (
3150+ mono : androidx.compose.ui.text.font.FontFamily ,
3151+ accentColor : Color ,
3152+ borderColor : Color ,
3153+ enabled : Boolean = true,
3154+ expanded : Boolean = false,
3155+ onExpandedChange : (Boolean ) -> Unit = {}
3156+ ) {
3157+ val monitorState by DeviceMonitor .state.collectAsState()
3158+ val selectedDevice = monitorState.selectedDevice
3159+ val scope = rememberCoroutineScope()
3160+ val adbManager = remember { AdbManager () }
3161+ var status by remember { mutableStateOf<RuntimeLogsStatus >(RuntimeLogsStatus .Idle ) }
3162+
3163+ val isWorking = status is RuntimeLogsStatus .Clearing || status is RuntimeLogsStatus .Saving
3164+ val deviceReady = selectedDevice?.isReady == true
3165+ val canAct = enabled && deviceReady && ! isWorking
3166+
3167+ CollapsibleSection (
3168+ title = " PATCHED APP RUNTIME LOGS" ,
3169+ mono = mono,
3170+ expanded = expanded,
3171+ onExpandedChange = onExpandedChange
3172+ ) {
3173+ Column (verticalArrangement = Arrangement .spacedBy(10 .dp)) {
3174+ Text (
3175+ text = " Capture logs from your phone after a patched app crashes or misbehaves. Clear before reproducing the bug, then save the filtered output to attach to a bug report." ,
3176+ fontSize = 11 .sp,
3177+ fontFamily = mono,
3178+ color = MaterialTheme .colorScheme.onSurfaceVariant.copy(alpha = 0.5f )
3179+ )
3180+
3181+ // Device row
3182+ if (deviceReady) {
3183+ Text (
3184+ text = " Device: ${selectedDevice.displayName}${selectedDevice.architecture?.let { " ($it )" } ? : " " } " ,
3185+ fontSize = 11 .sp,
3186+ fontFamily = mono,
3187+ color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.7f )
3188+ )
3189+ } else {
3190+ Text (
3191+ text = " No device connected. Plug in your phone with USB debugging enabled." ,
3192+ fontSize = 11 .sp,
3193+ fontFamily = mono,
3194+ color = MaterialTheme .colorScheme.onSurfaceVariant.copy(alpha = 0.5f )
3195+ )
3196+ }
3197+
3198+ ActionButton (
3199+ label = if (status is RuntimeLogsStatus .Clearing ) " CLEARING…" else " CLEAR DEVICE LOGS" ,
3200+ icon = Icons .Default .DeleteSweep ,
3201+ mono = mono,
3202+ borderColor = borderColor,
3203+ enabled = canAct,
3204+ onClick = {
3205+ val device = selectedDevice ? : return @ActionButton
3206+ status = RuntimeLogsStatus .Clearing
3207+ scope.launch {
3208+ val result = adbManager.clearLogcat(device.id)
3209+ status = result.fold(
3210+ onSuccess = { RuntimeLogsStatus .Cleared },
3211+ onFailure = { RuntimeLogsStatus .Error (it.message ? : " Failed to clear logs" ) }
3212+ )
3213+ }
3214+ }
3215+ )
3216+
3217+ ActionButton (
3218+ label = if (status is RuntimeLogsStatus .Saving ) " SAVING…" else " SAVE DEVICE LOGS" ,
3219+ icon = Icons .Default .Save ,
3220+ mono = mono,
3221+ borderColor = borderColor,
3222+ contentColor = accentColor,
3223+ enabled = canAct,
3224+ onClick = {
3225+ val device = selectedDevice ? : return @ActionButton
3226+ status = RuntimeLogsStatus .Saving
3227+ scope.launch {
3228+ val timestamp = SimpleDateFormat (" yyyy-MM-dd-HHmmss" , java.util.Locale .US ).format(java.util.Date ())
3229+ val outFile = File (FileUtils .getLogsDir(), " device-logcat-$timestamp .txt" )
3230+ val result = adbManager.captureLogcat(device.id, outFile)
3231+ status = result.fold(
3232+ onSuccess = { count -> RuntimeLogsStatus .Saved (outFile, count) },
3233+ onFailure = { RuntimeLogsStatus .Error (it.message ? : " Failed to save logs" ) }
3234+ )
3235+ }
3236+ }
3237+ )
3238+
3239+ // Status line
3240+ when (val s = status) {
3241+ RuntimeLogsStatus .Idle , RuntimeLogsStatus .Clearing , RuntimeLogsStatus .Saving -> Unit
3242+ RuntimeLogsStatus .Cleared -> Text (
3243+ text = " Logs cleared on device." ,
3244+ fontSize = 11 .sp,
3245+ fontFamily = mono,
3246+ color = accentColor.copy(alpha = 0.85f )
3247+ )
3248+ is RuntimeLogsStatus .Saved -> Column (verticalArrangement = Arrangement .spacedBy(4 .dp)) {
3249+ Text (
3250+ text = if (s.lineCount == 0 )
3251+ " Nothing captured yet. Run the patched app on your phone, then save again."
3252+ else
3253+ " Saved ${s.lineCount} line(s) to ${s.file.name} " ,
3254+ fontSize = 11 .sp,
3255+ fontFamily = mono,
3256+ color = if (s.lineCount == 0 ) MaterialTheme .colorScheme.onSurfaceVariant.copy(alpha = 0.6f )
3257+ else accentColor.copy(alpha = 0.85f )
3258+ )
3259+ if (s.lineCount > 0 ) {
3260+ val cornersLocal = LocalMorpheCorners .current
3261+ Text (
3262+ text = " OPEN LOGS" ,
3263+ fontSize = 10 .sp,
3264+ fontFamily = mono,
3265+ fontWeight = FontWeight .SemiBold ,
3266+ letterSpacing = 0.5 .sp,
3267+ color = accentColor,
3268+ modifier = Modifier
3269+ .clip(RoundedCornerShape (cornersLocal.small))
3270+ .clickable {
3271+ try {
3272+ if (Desktop .isDesktopSupported()) {
3273+ Desktop .getDesktop().open(s.file.parentFile)
3274+ }
3275+ } catch (e: Exception ) {
3276+ Logger .error(" Failed to reveal logs folder" , e)
3277+ }
3278+ }
3279+ .padding(horizontal = 10 .dp, vertical = 6 .dp)
3280+ )
3281+ }
3282+ }
3283+ is RuntimeLogsStatus .Error -> Text (
3284+ text = s.message,
3285+ fontSize = 11 .sp,
3286+ fontFamily = mono,
3287+ color = MaterialTheme .colorScheme.error
3288+ )
3289+ }
3290+ }
3291+ }
3292+ }
0 commit comments