Skip to content

Commit 20c4bd9

Browse files
committed
Add structured/rich console logging and override JS
Capture structured console output from the WebView and render it in the DevTools console. - Introduce devtools models: ResultNode, PrimitiveKind, RichLogEntry to represent serialized JS values and arguments. - Add ConsoleOverride JS (CONSOLE_OVERRIDE_JS, FORMAT_VALUE_JS) and wrappedResult() to serialize values in-page and for evaluateJavascript responses. - Add WXConsoleInterface to receive serialized console calls from JS and push RichLogEntry instances. - Expose richLogs and networkRequests through WXView/WXInterface and mark underlying lists internal on clients. - Update ConsoleTab/UI to use richLogs (sorted by timestamp), render structured nodes (primitive & expandable), and handle eval results via wrappedResult. - Inject console override script on page start and serve console.js via InternalPathHandler. - Make NetworkTab accept WXView and read requests from the view. - Remove older ad-hoc parsing and flattening code, move node id logic into ResultNode, and clean up related UI code. These changes enable richer, typed console messages (including objects/arrays) and a unified pipeline for console messages and evaluated JS results.
1 parent bb1bd80 commit 20c4bd9

12 files changed

Lines changed: 493 additions & 295 deletions

File tree

app/src/main/java/com/dergoogler/mmrl/wx/ui/component/devtools/ConsoleTab.kt

Lines changed: 233 additions & 288 deletions
Large diffs are not rendered by default.

app/src/main/java/com/dergoogler/mmrl/wx/ui/component/devtools/DevTools.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ fun DevTools(
3131
when (pageIndex) {
3232
0 -> DomTab(webview)
3333
1 -> ConsoleTab(webview)
34-
2 -> NetworkTab()
34+
2 -> NetworkTab(webview)
3535
}
3636
}
3737
}

app/src/main/java/com/dergoogler/mmrl/wx/ui/component/devtools/NetworkTab.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,11 @@ import androidx.compose.ui.unit.Dp
4141
import androidx.compose.ui.unit.dp
4242
import androidx.compose.ui.unit.sp
4343
import com.dergoogler.mmrl.ui.component.Tab
44-
import com.dergoogler.mmrl.webui.client.WXClient
44+
import com.dergoogler.mmrl.webui.view.WXView
4545

4646
@Composable
47-
fun NetworkTab() {
48-
val requests = WXClient.networkRequests
47+
fun NetworkTab(webview: WXView) {
48+
val requests = webview.networkRequests
4949

5050
val columns = listOf(
5151
ColumnDef("Name", 220.dp),

webui/src/main/kotlin/com/dergoogler/mmrl/webui/client/WXChromeClient.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@ import com.dergoogler.mmrl.ui.component.dialog.PromptData
1313
import com.dergoogler.mmrl.ui.component.dialog.confirm
1414
import com.dergoogler.mmrl.ui.component.dialog.prompt
1515
import com.dergoogler.mmrl.webui.R
16+
import com.dergoogler.mmrl.webui.devtools.RichLogEntry
1617
import com.dergoogler.mmrl.webui.util.WebUIOptions
1718

1819
open class WXChromeClient(
1920
private val options: WebUIOptions,
2021
) : HybridWebUIChromeClient() {
2122
companion object {
22-
val consoleLogs = mutableStateListOf<ConsoleMessage>()
23+
internal val consoleLogs = mutableStateListOf<ConsoleMessage>()
24+
internal val richLogs = mutableStateListOf<RichLogEntry>()
2325

2426
private const val TAG = "WXChromeClient"
2527
}

webui/src/main/kotlin/com/dergoogler/mmrl/webui/client/WXClient.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import android.webkit.WebView
1515
import androidx.compose.runtime.mutableStateListOf
1616
import com.dergoogler.mmrl.ext.nullply
1717
import com.dergoogler.mmrl.hybridwebui.HybridWebUIClient
18+
import com.dergoogler.mmrl.webui.devtools.CONSOLE_OVERRIDE_JS
1819
import com.dergoogler.mmrl.webui.model.invoke
1920
import com.dergoogler.mmrl.webui.util.WebUIOptions
2021
import com.dergoogler.mmrl.webui.view.WXSwipeRefresh
@@ -45,8 +46,9 @@ open class WXClient : HybridWebUIClient {
4546
}
4647
}
4748

48-
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
49+
override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) {
4950
super.onPageStarted(view, url, favicon)
51+
view.evaluateJavascript(CONSOLE_OVERRIDE_JS, null)
5052
mSwipeView.nullply {
5153
isRefreshing = true
5254
}
@@ -178,6 +180,6 @@ open class WXClient : HybridWebUIClient {
178180
companion object {
179181
private const val TAG = "WXClient"
180182

181-
val networkRequests = mutableStateListOf<WebResourceRequest>()
183+
internal val networkRequests = mutableStateListOf<WebResourceRequest>()
182184
}
183185
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.dergoogler.mmrl.webui.devtools
2+
3+
internal const val FORMAT_VALUE_JS = """
4+
function __fv(val, depth) {
5+
if (depth === undefined) depth = 0;
6+
if (val === undefined) return { type:'primitive', kind:'undefined', value:'undefined' };
7+
if (val === null) return { type:'primitive', kind:'null', value:'null' };
8+
if (typeof val === 'boolean') return { type:'primitive', kind:'boolean', value:String(val) };
9+
if (typeof val === 'number') return { type:'primitive', kind:'number', value:String(val) };
10+
if (typeof val === 'string') return { type:'primitive', kind:'string', value:val };
11+
if (typeof val === 'symbol') return { type:'primitive', kind:'other', value:val.toString() };
12+
if (typeof val === 'function') {
13+
var src = Function.prototype.toString.call(val);
14+
var sig = src.split('\n')[0].replace(/\{.*/, '').trim();
15+
return { type:'primitive', kind:'function', value:'f ' + sig };
16+
}
17+
if (Array.isArray(val)) {
18+
if (depth >= 3) return { type:'primitive', kind:'other', value:'[Array(' + val.length + ')]' };
19+
var items = [];
20+
for (var i = 0; i < Math.min(val.length, 50); i++) {
21+
try { var c = __fv(val[i], depth+1); c.key = String(i); items.push(c); }
22+
catch(e) { items.push({ type:'primitive', kind:'other', value:'?', key:String(i) }); }
23+
}
24+
if (val.length > 50) items.push({ type:'primitive', kind:'other', value:'... '+(val.length-50)+' more', key:'...' });
25+
return { type:'expandable', label:'Array('+val.length+')', closeToken:']', children:items };
26+
}
27+
if (typeof val === 'object') {
28+
if (depth >= 3) return { type:'primitive', kind:'other', value:'{...}' };
29+
var prefix = '';
30+
if (val.constructor && val.constructor.name && val.constructor.name !== 'Object') {
31+
prefix = val.constructor.name;
32+
}
33+
var keys = [];
34+
try { keys = Object.getOwnPropertyNames(val).filter(function(k){ return k !== '__proto__'; }); } catch(e) {}
35+
if (keys.length === 0) try { keys = Object.keys(val); } catch(e) {}
36+
var pairs = []; var shown = 0;
37+
for (var i = 0; i < keys.length && shown < 30; i++) {
38+
var k = keys[i];
39+
try { var c = __fv(val[k], depth+1); c.key = k; pairs.push(c); shown++; }
40+
catch(e) { pairs.push({ type:'primitive', kind:'other', value:'[getter]', key:k }); shown++; }
41+
}
42+
if (keys.length > 30) pairs.push({ type:'primitive', kind:'other', value:'... '+(keys.length-30)+' more keys', key:'...' });
43+
return { type:'expandable', label:prefix, closeToken:'}', children:pairs };
44+
}
45+
return { type:'primitive', kind:'other', value:String(val) };
46+
}
47+
"""
48+
49+
// ---------------------------------------------------------------------------
50+
// JS: console override — injected on every onPageStarted
51+
// ---------------------------------------------------------------------------
52+
53+
internal val CONSOLE_OVERRIDE_JS = """
54+
(function() {
55+
$FORMAT_VALUE_JS
56+
var _con = window.__wxConsole;
57+
if (!_con) return;
58+
function intercept(level) {
59+
var orig = console[level].bind(console);
60+
console[level] = function() {
61+
var args = Array.prototype.slice.call(arguments);
62+
var serialized = args.map(function(a) { return __fv(a); });
63+
try { _con[level](JSON.stringify({ v: serialized })); } catch(e) {}
64+
};
65+
}
66+
intercept('log');
67+
intercept('warn');
68+
intercept('error');
69+
intercept('info');
70+
intercept('debug');
71+
})();
72+
""".trimIndent()
73+
74+
private fun escapeJsString(code: String): String {
75+
val escaped = code
76+
.replace("\\", "\\\\")
77+
.replace("\"", "\\\"")
78+
.replace("\n", "\\n")
79+
.replace("\r", "\\r")
80+
return "\"$escaped\""
81+
}
82+
83+
fun wrappedResult(code: String) = """
84+
(function() {
85+
$FORMAT_VALUE_JS
86+
try {
87+
var __r = eval(${escapeJsString(code)});
88+
return JSON.stringify({ ok:true, value:__fv(__r) });
89+
} catch(e) {
90+
return JSON.stringify({ ok:false, value:e.toString() });
91+
}
92+
})()
93+
""".trimIndent()
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.dergoogler.mmrl.webui.devtools
2+
3+
import org.json.JSONObject
4+
5+
sealed class ResultNode {
6+
abstract val key: String?
7+
abstract val depth: Int
8+
9+
data class Primitive(
10+
override val key: String?,
11+
val value: String,
12+
val kind: PrimitiveKind,
13+
override val depth: Int,
14+
) : ResultNode()
15+
16+
data class Expandable(
17+
override val key: String?,
18+
val label: String,
19+
val closeToken: String,
20+
val children: List<ResultNode>,
21+
override val depth: Int,
22+
val id: String,
23+
) : ResultNode()
24+
25+
companion object {
26+
private var _nodeIdCounter = 0
27+
28+
fun parse(obj: JSONObject, key: String?, depth: Int): ResultNode {
29+
return when (obj.optString("type")) {
30+
"expandable" -> {
31+
val label = obj.optString("label", "")
32+
val closeToken = obj.optString("closeToken", "}")
33+
val childArray = obj.optJSONArray("children")
34+
val children = mutableListOf<ResultNode>()
35+
if (childArray != null) {
36+
for (i in 0 until childArray.length()) {
37+
val childObj = childArray.getJSONObject(i)
38+
val childKey = if (childObj.has("key")) childObj.getString("key") else null
39+
children.add(parse(childObj, childKey, depth + 1))
40+
}
41+
}
42+
Expandable(
43+
key = key,
44+
label = label,
45+
closeToken = closeToken,
46+
children = children,
47+
depth = depth,
48+
id = "node_${_nodeIdCounter++}"
49+
)
50+
}
51+
else -> {
52+
val kind = when (obj.optString("kind")) {
53+
"string" -> PrimitiveKind.STRING
54+
"number" -> PrimitiveKind.NUMBER
55+
"boolean" -> PrimitiveKind.BOOLEAN
56+
"null", "undefined" -> PrimitiveKind.NULL_UNDEFINED
57+
"function" -> PrimitiveKind.FUNCTION
58+
else -> PrimitiveKind.OTHER
59+
}
60+
Primitive(key, obj.optString("value", ""), kind, depth)
61+
}
62+
}
63+
}
64+
}
65+
}
66+
67+
enum class PrimitiveKind {
68+
STRING, NUMBER, BOOLEAN, NULL_UNDEFINED, FUNCTION, OTHER
69+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.dergoogler.mmrl.webui.devtools
2+
3+
import android.webkit.ConsoleMessage
4+
5+
data class RichLogEntry(
6+
val level: ConsoleMessage.MessageLevel,
7+
val args: List<ResultNode>,
8+
val source: String,
9+
val line: Int,
10+
val timestamp: Long = System.currentTimeMillis(),
11+
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
@file:Suppress("unused")
2+
3+
package com.dergoogler.mmrl.webui.interfaces
4+
5+
import android.webkit.ConsoleMessage
6+
import android.webkit.JavascriptInterface
7+
import com.dergoogler.mmrl.webui.devtools.PrimitiveKind
8+
import com.dergoogler.mmrl.webui.devtools.ResultNode
9+
import com.dergoogler.mmrl.webui.devtools.RichLogEntry
10+
import org.json.JSONObject
11+
12+
class WXConsoleInterface(wxOptions: WXOptions) : WXInterface(wxOptions) {
13+
override var name = "__wxConsole"
14+
15+
@JavascriptInterface
16+
fun log(json: String) = push(ConsoleMessage.MessageLevel.LOG, json)
17+
18+
@JavascriptInterface
19+
fun warn(json: String) = push(ConsoleMessage.MessageLevel.WARNING, json)
20+
21+
@JavascriptInterface
22+
fun error(json: String) = push(ConsoleMessage.MessageLevel.ERROR, json)
23+
24+
@JavascriptInterface
25+
fun info(json: String) = push(ConsoleMessage.MessageLevel.LOG, json)
26+
27+
@JavascriptInterface
28+
fun debug(json: String) = push(ConsoleMessage.MessageLevel.DEBUG, json)
29+
30+
@JavascriptInterface
31+
fun tip(json: String) = push(ConsoleMessage.MessageLevel.TIP, json)
32+
33+
private fun push(level: ConsoleMessage.MessageLevel, json: String) {
34+
try {
35+
// json arrives as the raw string passed from JS — e.g. '{"v":[...]}'
36+
// No extra JSONObject wrapping needed here unlike evaluateJavascript callbacks
37+
val outer = JSONObject(json)
38+
val arr = outer.getJSONArray("v")
39+
val args = (0 until arr.length()).map { i ->
40+
ResultNode.parse(arr.getJSONObject(i), key = null, depth = 0)
41+
}
42+
richLogs.add(
43+
RichLogEntry(level = level, args = args, source = "", line = 0)
44+
)
45+
} catch (e: Exception) {
46+
richLogs.add(
47+
RichLogEntry(
48+
level = level,
49+
args = listOf(
50+
ResultNode.Primitive(null, json, PrimitiveKind.OTHER, 0)
51+
),
52+
source = "",
53+
line = 0
54+
)
55+
)
56+
}
57+
}
58+
}

webui/src/main/kotlin/com/dergoogler/mmrl/webui/interfaces/WXinterface.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import androidx.annotation.UiThread
1212
import com.dergoogler.mmrl.ext.findActivity
1313
import com.dergoogler.mmrl.hybridwebui.HybridWebUI
1414
import com.dergoogler.mmrl.platform.model.ModId
15+
import com.dergoogler.mmrl.webui.client.WXChromeClient
16+
import com.dergoogler.mmrl.webui.client.WXClient
1517
import com.dergoogler.mmrl.webui.model.WebUIConfig
1618
import com.dergoogler.mmrl.webui.util.WebUIOptions
1719
import com.dergoogler.mmrl.webui.view.WXView
@@ -298,4 +300,8 @@ open class WXInterface(
298300
return false
299301
}
300302
}
303+
304+
val richLogs get() = WXChromeClient.richLogs
305+
val consoleLogs get() = WXChromeClient.consoleLogs
306+
val networkRequests get() = WXClient.networkRequests
301307
}

0 commit comments

Comments
 (0)