@windypro-rourou/dsh-logcat 0.6.3 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +80 -23
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -61,6 +61,11 @@ window.__ModuleLoader__.load({
|
|
|
61
61
|
.lc-mem-bar { display: inline-block; width: 44px; height: 5px; border-radius: 3px; background: light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14)); overflow: hidden; vertical-align: middle; }
|
|
62
62
|
.lc-mem-fill { display: block; height: 100%; border-radius: 3px; background: linear-gradient(90deg, #4caf50, #fbc02d); }
|
|
63
63
|
.lc-mem-fill.hot { background: linear-gradient(90deg, #fbc02d, #ef5350); }
|
|
64
|
+
.lc-icobtn { border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); background: transparent; color: inherit; font: inherit; font-size: 13px; width: 30px; height: 26px; padding: 0; border-radius: 7px; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
|
|
65
|
+
.lc-icobtn:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
|
|
66
|
+
.lc-icobtn[data-on] { background: light-dark(rgba(66,133,244,.12), rgba(66,133,244,.22)); border-color: rgba(66,133,244,.45); color: #4285f4; }
|
|
67
|
+
.lc-hit { background: rgba(66,133,244,.28); border-radius: 2px; color: inherit; }
|
|
68
|
+
.lc-screen-img:fullscreen { background: #000; }
|
|
64
69
|
.lc-toolbar { display: flex; align-items: center; gap: 6px; padding: 6px 12px; border-bottom: 1px solid light-dark(rgba(0,0,0,.1), rgba(255,255,255,.12)); flex: none; flex-wrap: wrap; }
|
|
65
70
|
.lc-select, .lc-search { background: light-dark(rgba(0,0,0,.05), rgba(255,255,255,.08)); border: 1px solid light-dark(rgba(0,0,0,.16), rgba(255,255,255,.2)); border-radius: 7px; color: inherit; font: inherit; font-size: 12px; padding: 5px 9px; }
|
|
66
71
|
.lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
|
|
@@ -203,7 +208,8 @@ window.__ModuleLoader__.load({
|
|
|
203
208
|
const [keyword, setKeyword] = useState("");
|
|
204
209
|
const [autoScroll, setAutoScroll] = useState(true);
|
|
205
210
|
const [scrollTop, setScrollTop] = useState(0);
|
|
206
|
-
const [tab, setTab] = useState("log");
|
|
211
|
+
const [tab, setTab] = useState(() => { try { const t = localStorage.getItem("dsh-logcat-tab"); return t === "screen" || t === "re" ? t : "log"; } catch { return "log"; } });
|
|
212
|
+
const switchTab = (t) => { setTab(t); try { localStorage.setItem("dsh-logcat-tab", t); } catch { /* private mode */ } };
|
|
207
213
|
const [eventsOn, setEventsOn] = useState(false);
|
|
208
214
|
const [eventsEntries, setEventsEntries] = useState([]);
|
|
209
215
|
const [eventsStreaming, setEventsStreaming] = useState([]);
|
|
@@ -550,6 +556,24 @@ window.__ModuleLoader__.load({
|
|
|
550
556
|
}
|
|
551
557
|
};
|
|
552
558
|
|
|
559
|
+
// Append a tag/term to the keyword filter (matches any term).
|
|
560
|
+
const addKeyword = (term) => {
|
|
561
|
+
const t = (term ?? "").trim();
|
|
562
|
+
if (t === "") return;
|
|
563
|
+
setKeyword((prev) => {
|
|
564
|
+
const terms = prev.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
565
|
+
if (terms.includes(t.toLowerCase())) return prev;
|
|
566
|
+
return prev.trim() === "" ? t : prev.trim() + " " + t;
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// Copy one log line (double-click).
|
|
571
|
+
const copyLine = (raw) => {
|
|
572
|
+
if (navigator.clipboard !== undefined && raw !== "") {
|
|
573
|
+
navigator.clipboard.writeText(raw).catch(() => { /* denied */ });
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
553
577
|
const device = devices.find((d) => d.serial === serial);
|
|
554
578
|
const deviceState = device?.state ?? "";
|
|
555
579
|
const live = connected && serial !== "" && streaming.includes(serial);
|
|
@@ -905,6 +929,13 @@ window.__ModuleLoader__.load({
|
|
|
905
929
|
} catch { /* empty canvas */ }
|
|
906
930
|
};
|
|
907
931
|
|
|
932
|
+
const toggleScreenFullscreen = () => {
|
|
933
|
+
const el = screenWrapRef.current;
|
|
934
|
+
if (el === null) return;
|
|
935
|
+
if (document.fullscreenElement !== null) { try { document.exitFullscreen(); } catch { /* ignored */ } return; }
|
|
936
|
+
try { el.requestFullscreen?.(); } catch { /* denied */ }
|
|
937
|
+
};
|
|
938
|
+
|
|
908
939
|
return h("div", { className: "lc-panel" },
|
|
909
940
|
h("div", { className: "lc-header" },
|
|
910
941
|
h("button", { type: "button", className: "lc-back", onClick: () => controller.close() },
|
|
@@ -958,9 +989,9 @@ window.__ModuleLoader__.load({
|
|
|
958
989
|
stats === null && serial !== "" ? h("span", { className: "lc-chip" }, "正在采集设备状态…") : null,
|
|
959
990
|
),
|
|
960
991
|
h("div", { className: "lc-tabs" },
|
|
961
|
-
h("button", { type: "button", className: "lc-tab", "data-on": tab === "log" ? "" : undefined, onClick: () =>
|
|
962
|
-
h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () =>
|
|
963
|
-
h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () =>
|
|
992
|
+
h("button", { type: "button", className: "lc-tab", "data-on": tab === "log" ? "" : undefined, onClick: () => switchTab("log") }, "日志"),
|
|
993
|
+
h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () => switchTab("screen") }, "屏幕"),
|
|
994
|
+
h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () => switchTab("re") }, "逆向"),
|
|
964
995
|
),
|
|
965
996
|
tab === "log"
|
|
966
997
|
? h("div", { className: "lc-toolbar" },
|
|
@@ -1000,11 +1031,11 @@ window.__ModuleLoader__.load({
|
|
|
1000
1031
|
style: { maxWidth: 180 },
|
|
1001
1032
|
}),
|
|
1002
1033
|
h("button", { type: "button", className: "lc-btn", onClick: () => setPackageFilter(pkgInput), title: "按包名过滤日志(agent 侧 logcat_recent 同步生效)" }, "包名"),
|
|
1003
|
-
h("button", { type: "button", className: "lc-
|
|
1004
|
-
h("button", { type: "button", className: "lc-
|
|
1005
|
-
h("button", { type: "button", className: "lc-
|
|
1006
|
-
h("button", { type: "button", className: "lc-
|
|
1007
|
-
h("button", { type: "button", className: "lc-
|
|
1034
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: takeScreenshot, title: "截取真机屏幕并下载 PNG" }, "📷"),
|
|
1035
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: loadHistory, title: "从磁盘加载历史日志(重启不丢)" }, "🕘"),
|
|
1036
|
+
h("button", { type: "button", className: "lc-icobtn", "data-on": eventsOn ? "" : undefined, onClick: toggleEvents, title: "logcat -b events 事件缓冲(应用启动/崩溃/生命周期)" }, "📑"),
|
|
1037
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: openCrashModal, title: "崩溃自动快照历史(截图+上下文)" }, "💥"),
|
|
1038
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: () => setWifiOpen(true), title: "WiFi 无线调试(adb pair + connect)" }, "📶"),
|
|
1008
1039
|
h("input", {
|
|
1009
1040
|
ref: fileInputRef,
|
|
1010
1041
|
type: "file",
|
|
@@ -1012,19 +1043,19 @@ window.__ModuleLoader__.load({
|
|
|
1012
1043
|
style: { display: "none" },
|
|
1013
1044
|
onChange: (e) => { installApkFile(e.target.files?.[0] ?? null); e.target.value = ""; },
|
|
1014
1045
|
}),
|
|
1015
|
-
h("button", { type: "button", className: "lc-
|
|
1016
|
-
h("button", { type: "button", className: "lc-
|
|
1017
|
-
paused ? "
|
|
1018
|
-
h("button", { type: "button", className: "lc-
|
|
1019
|
-
h("button", { type: "button", className: "lc-
|
|
1020
|
-
h("button", { type: "button", className: "lc-
|
|
1046
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, "📦"),
|
|
1047
|
+
h("button", { type: "button", className: "lc-icobtn", "data-on": paused ? "" : undefined, onClick: togglePause, title: "暂停冻结视图(新日志缓冲) / 继续回放" },
|
|
1048
|
+
paused ? "▶" : "⏸"),
|
|
1049
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: clearLog, title: "清空当前视图" }, "🗑"),
|
|
1050
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: copyLog, title: "复制过滤后日志" }, "📋"),
|
|
1051
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: exportLog, title: "导出过滤后日志 .txt" }, "⬇"),
|
|
1021
1052
|
h("button", {
|
|
1022
1053
|
type: "button",
|
|
1023
|
-
className: "lc-
|
|
1054
|
+
className: "lc-icobtn",
|
|
1024
1055
|
"data-on": autoScroll ? "" : undefined,
|
|
1025
1056
|
title: "新日志自动滚到底部",
|
|
1026
1057
|
onClick: () => setAutoScroll(!autoScroll),
|
|
1027
|
-
}, "
|
|
1058
|
+
}, "📌"),
|
|
1028
1059
|
)
|
|
1029
1060
|
: null,
|
|
1030
1061
|
tab === "log" || tab === "screen"
|
|
@@ -1107,6 +1138,7 @@ window.__ModuleLoader__.load({
|
|
|
1107
1138
|
h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~" + screenFps + "fps)" : "投屏未开启")),
|
|
1108
1139
|
h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 140 } }),
|
|
1109
1140
|
h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
|
|
1141
|
+
h("button", { type: "button", className: "lc-btn", onClick: toggleScreenFullscreen, title: "全屏查看(Esc 退出)" }, "⛶"),
|
|
1110
1142
|
h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
|
|
1111
1143
|
),
|
|
1112
1144
|
!hostScreenCapable
|
|
@@ -1119,10 +1151,10 @@ window.__ModuleLoader__.load({
|
|
|
1119
1151
|
: eventsOn
|
|
1120
1152
|
? (filteredEvents.length === 0
|
|
1121
1153
|
? h("div", { className: "lc-empty" }, "events 事件缓冲(已开启流,等待事件…)")
|
|
1122
|
-
: h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef }))
|
|
1154
|
+
: h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine }))
|
|
1123
1155
|
: (filtered.length === 0
|
|
1124
1156
|
? h("div", { className: "lc-empty" }, paused ? "已暂停(" + entries.length + " 条已缓冲)" : "暂无日志")
|
|
1125
|
-
: h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef })),
|
|
1157
|
+
: h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine })),
|
|
1126
1158
|
tab === "log" && !eventsOn && live && !paused
|
|
1127
1159
|
? h("button", { type: "button", className: "lc-fab", onClick: togglePause, title: "暂停:冻结当前视图(新日志进入缓冲)" }, "⏸ 暂停")
|
|
1128
1160
|
: null,
|
|
@@ -1231,8 +1263,32 @@ window.__ModuleLoader__.load({
|
|
|
1231
1263
|
);
|
|
1232
1264
|
}
|
|
1233
1265
|
|
|
1266
|
+
/** Split `text` on keyword hits; render hit ranges as <mark>. */
|
|
1267
|
+
function highlightMsg(text, keyword) {
|
|
1268
|
+
const terms = (keyword ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
1269
|
+
if (terms.length === 0 || text === "") return text;
|
|
1270
|
+
const lower = text.toLowerCase();
|
|
1271
|
+
const ranges = [];
|
|
1272
|
+
for (const t of terms) {
|
|
1273
|
+
const idx = lower.indexOf(t);
|
|
1274
|
+
if (idx >= 0) ranges.push([idx, idx + t.length]);
|
|
1275
|
+
}
|
|
1276
|
+
if (ranges.length === 0) return text;
|
|
1277
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
1278
|
+
const parts = [];
|
|
1279
|
+
let pos = 0;
|
|
1280
|
+
for (const [s, e] of ranges) {
|
|
1281
|
+
if (s < pos) continue; // overlapping hits: keep the first
|
|
1282
|
+
if (s > pos) parts.push(text.slice(pos, s));
|
|
1283
|
+
parts.push(h("mark", { className: "lc-hit", key: s }, text.slice(s, e)));
|
|
1284
|
+
pos = e;
|
|
1285
|
+
}
|
|
1286
|
+
if (pos < text.length) parts.push(text.slice(pos));
|
|
1287
|
+
return parts;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1234
1290
|
/** Windowed log list: fixed 20px rows, only the visible slice is rendered. */
|
|
1235
|
-
function VirtualLog({ entries, scrollTop, onScrollTop, bodyRef }) {
|
|
1291
|
+
function VirtualLog({ entries, scrollTop, onScrollTop, bodyRef, keyword, onTagClick, onLineDblClick }) {
|
|
1236
1292
|
const ROW = 20;
|
|
1237
1293
|
const height = useRef(0);
|
|
1238
1294
|
const [viewport, setViewport] = useState({ top: 0, bottom: 100 });
|
|
@@ -1271,13 +1327,14 @@ window.__ModuleLoader__.load({
|
|
|
1271
1327
|
key: i,
|
|
1272
1328
|
className: "lc-line" + (e.cont === true ? " cont" : "") + (isCrash(e) ? " crash" : ""),
|
|
1273
1329
|
style: { top: i * ROW },
|
|
1274
|
-
title: e.raw,
|
|
1330
|
+
title: e.raw + "\n(点 tag 过滤 · 双击复制该行)",
|
|
1331
|
+
onDoubleClick: () => { if (onLineDblClick) onLineDblClick(e.raw); },
|
|
1275
1332
|
},
|
|
1276
1333
|
e.ts !== "" ? h("span", { className: "ts" }, e.ts) : null,
|
|
1277
1334
|
e.pid > 0 ? h("span", { className: "pid" }, e.pid + "-" + e.tid) : null,
|
|
1278
1335
|
e.level !== "" ? h("span", { className: "lv " + level }, level) : null,
|
|
1279
|
-
e.tag !== "" ? h("span", { className: "tag" }, e.tag) : null,
|
|
1280
|
-
h("span", { className: "msg" }, e.msg !== "" ? e.msg : e.raw),
|
|
1336
|
+
e.tag !== "" ? h("span", { className: "tag", title: "按此 tag 过滤", onClick: (ev) => { ev.stopPropagation(); if (onTagClick) onTagClick(e.tag); } }, e.tag) : null,
|
|
1337
|
+
h("span", { className: "msg" }, highlightMsg(e.msg !== "" ? e.msg : e.raw, keyword)),
|
|
1281
1338
|
),
|
|
1282
1339
|
);
|
|
1283
1340
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@windypro-rourou/dsh-logcat",
|
|
3
3
|
"description": "Android Logcat viewer for the dsh web GUI: auto-connects to any adb device in debug mode, live logcat stream with level/keyword filters, pause/clear/export, plus agent tools (logcat_recent). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules copy, no dsh source changes.",
|
|
4
|
-
"version": "0.6.
|
|
4
|
+
"version": "0.6.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.22.0",
|
|
7
7
|
"engines": {
|