@windypro-rourou/dsh-logcat 0.6.3 → 0.6.5

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.
Files changed (2) hide show
  1. package/lib/client.js +110 -30
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -61,6 +61,18 @@ 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-log.compact { font-size: 11px; line-height: 15px; }
69
+ .lc-line.lw-V { border-left: 3px solid rgba(158,158,158,.35); }
70
+ .lc-line.lw-D { border-left: 3px solid #4fc3f7; }
71
+ .lc-line.lw-I { border-left: 3px solid #4caf50; }
72
+ .lc-line.lw-W { border-left: 3px solid #fbc02d; }
73
+ .lc-line.lw-E { border-left: 3px solid #ef5350; }
74
+ .lc-line.lw-F { border-left: 3px solid #ab47bc; }
75
+ .lc-screen-img:fullscreen { background: #000; }
64
76
  .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
77
  .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
78
  .lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
@@ -203,7 +215,9 @@ window.__ModuleLoader__.load({
203
215
  const [keyword, setKeyword] = useState("");
204
216
  const [autoScroll, setAutoScroll] = useState(true);
205
217
  const [scrollTop, setScrollTop] = useState(0);
206
- const [tab, setTab] = useState("log");
218
+ const [rowDensity, setRowDensity] = useState(() => { try { return localStorage.getItem("dsh-logcat-density") === "compact" ? "compact" : "std"; } catch { return "std"; } });
219
+ const [tab, setTab] = useState(() => { try { const t = localStorage.getItem("dsh-logcat-tab"); return t === "screen" || t === "re" ? t : "log"; } catch { return "log"; } });
220
+ const switchTab = (t) => { setTab(t); try { localStorage.setItem("dsh-logcat-tab", t); } catch { /* private mode */ } };
207
221
  const [eventsOn, setEventsOn] = useState(false);
208
222
  const [eventsEntries, setEventsEntries] = useState([]);
209
223
  const [eventsStreaming, setEventsStreaming] = useState([]);
@@ -550,6 +564,33 @@ window.__ModuleLoader__.load({
550
564
  }
551
565
  };
552
566
 
567
+ // Append a tag/term to the keyword filter (matches any term).
568
+ const addKeyword = (term) => {
569
+ const t = (term ?? "").trim();
570
+ if (t === "") return;
571
+ setKeyword((prev) => {
572
+ const terms = prev.trim().toLowerCase().split(/\s+/).filter(Boolean);
573
+ if (terms.includes(t.toLowerCase())) return prev;
574
+ return prev.trim() === "" ? t : prev.trim() + " " + t;
575
+ });
576
+ };
577
+
578
+ // Copy one log line (double-click).
579
+ const copyLine = (raw) => {
580
+ if (navigator.clipboard !== undefined && raw !== "") {
581
+ navigator.clipboard.writeText(raw).catch(() => { /* denied */ });
582
+ }
583
+ };
584
+
585
+ // Toggle log row density (std 20px / compact 15px, remembered).
586
+ const toggleDensity = () => {
587
+ setRowDensity((prev) => {
588
+ const next = prev === "compact" ? "std" : "compact";
589
+ try { localStorage.setItem("dsh-logcat-density", next); } catch { /* private */ }
590
+ return next;
591
+ });
592
+ };
593
+
553
594
  const device = devices.find((d) => d.serial === serial);
554
595
  const deviceState = device?.state ?? "";
555
596
  const live = connected && serial !== "" && streaming.includes(serial);
@@ -905,6 +946,13 @@ window.__ModuleLoader__.load({
905
946
  } catch { /* empty canvas */ }
906
947
  };
907
948
 
949
+ const toggleScreenFullscreen = () => {
950
+ const el = screenWrapRef.current;
951
+ if (el === null) return;
952
+ if (document.fullscreenElement !== null) { try { document.exitFullscreen(); } catch { /* ignored */ } return; }
953
+ try { el.requestFullscreen?.(); } catch { /* denied */ }
954
+ };
955
+
908
956
  return h("div", { className: "lc-panel" },
909
957
  h("div", { className: "lc-header" },
910
958
  h("button", { type: "button", className: "lc-back", onClick: () => controller.close() },
@@ -914,7 +962,7 @@ window.__ModuleLoader__.load({
914
962
  h("h2", { className: "lc-title" }, "Logcat"),
915
963
  h("span", { className: "lc-dot " + (live ? "on" : connected ? "warn" : "off") }),
916
964
  h("span", { style: { fontSize: 12, color: "var(--lc-dim, #9e9e9e)" } },
917
- live ? "实机日志流中" : connected ? "未选设备" : "未连接"),
965
+ live ? "实机日志流中" : devices.length === 0 ? "无设备" : connected ? "未选设备" : "未连接"),
918
966
  h("select", {
919
967
  className: "lc-select",
920
968
  value: serial,
@@ -955,12 +1003,16 @@ window.__ModuleLoader__.load({
955
1003
  "电量 " + stats.batteryLevel + "%" + (BATTERY_STATUS[stats.batteryStatus] !== undefined ? " · " + BATTERY_STATUS[stats.batteryStatus] : ""))
956
1004
  : null,
957
1005
  spark.bat.length >= 2 ? h(Spark, { data: spark.bat, color: "#4caf50", width: 44, height: 14, title: "电量趋势" }) : null,
958
- stats === null && serial !== "" ? h("span", { className: "lc-chip" }, "正在采集设备状态…") : null,
1006
+ stats === null && serial !== "" && devices.length > 0
1007
+ ? h("span", { className: "lc-chip" }, "正在采集设备状态…")
1008
+ : devices.length === 0
1009
+ ? h("span", { className: "lc-chip" }, "无设备 — 连接后自动采集状态")
1010
+ : null,
959
1011
  ),
960
1012
  h("div", { className: "lc-tabs" },
961
- h("button", { type: "button", className: "lc-tab", "data-on": tab === "log" ? "" : undefined, onClick: () => setTab("log") }, "日志"),
962
- h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () => setTab("screen") }, "屏幕"),
963
- h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () => setTab("re") }, "逆向"),
1013
+ h("button", { type: "button", className: "lc-tab", "data-on": tab === "log" ? "" : undefined, onClick: () => switchTab("log") }, "日志"),
1014
+ h("button", { type: "button", className: "lc-tab", "data-on": tab === "screen" ? "" : undefined, onClick: () => switchTab("screen") }, "屏幕"),
1015
+ h("button", { type: "button", className: "lc-tab", "data-on": tab === "re" ? "" : undefined, onClick: () => switchTab("re") }, "逆向"),
964
1016
  ),
965
1017
  tab === "log"
966
1018
  ? h("div", { className: "lc-toolbar" },
@@ -1000,11 +1052,11 @@ window.__ModuleLoader__.load({
1000
1052
  style: { maxWidth: 180 },
1001
1053
  }),
1002
1054
  h("button", { type: "button", className: "lc-btn", onClick: () => setPackageFilter(pkgInput), title: "按包名过滤日志(agent 侧 logcat_recent 同步生效)" }, "包名"),
1003
- h("button", { type: "button", className: "lc-btn", onClick: takeScreenshot, title: "截取真机屏幕并下载 PNG" }, "截图"),
1004
- h("button", { type: "button", className: "lc-btn", onClick: loadHistory, title: "从磁盘加载历史日志(重启不丢)" }, "历史"),
1005
- h("button", { type: "button", className: "lc-btn", "data-on": eventsOn ? "" : undefined, onClick: toggleEvents, title: "logcat -b events 事件缓冲(应用启动/崩溃/生命周期)" }, "events"),
1006
- h("button", { type: "button", className: "lc-btn", onClick: openCrashModal, title: "崩溃自动快照历史(截图+上下文)" }, "崩溃"),
1007
- h("button", { type: "button", className: "lc-btn", onClick: () => setWifiOpen(true), title: "WiFi 无线调试(adb pair + connect)" }, "WiFi"),
1055
+ h("button", { type: "button", className: "lc-icobtn", onClick: takeScreenshot, title: "截取真机屏幕并下载 PNG" }, "📷"),
1056
+ h("button", { type: "button", className: "lc-icobtn", onClick: loadHistory, title: "从磁盘加载历史日志(重启不丢)" }, "🕘"),
1057
+ h("button", { type: "button", className: "lc-icobtn", "data-on": eventsOn ? "" : undefined, onClick: toggleEvents, title: "logcat -b events 事件缓冲(应用启动/崩溃/生命周期)" }, "📑"),
1058
+ h("button", { type: "button", className: "lc-icobtn", onClick: openCrashModal, title: "崩溃自动快照历史(截图+上下文)" }, "💥"),
1059
+ h("button", { type: "button", className: "lc-icobtn", onClick: () => setWifiOpen(true), title: "WiFi 无线调试(adb pair + connect)" }, "📶"),
1008
1060
  h("input", {
1009
1061
  ref: fileInputRef,
1010
1062
  type: "file",
@@ -1012,19 +1064,19 @@ window.__ModuleLoader__.load({
1012
1064
  style: { display: "none" },
1013
1065
  onChange: (e) => { installApkFile(e.target.files?.[0] ?? null); e.target.value = ""; },
1014
1066
  }),
1015
- h("button", { type: "button", className: "lc-btn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, installingApk ? "安装中…" : "装APK"),
1016
- h("button", { type: "button", className: "lc-btn", "data-on": paused ? "" : undefined, onClick: togglePause },
1017
- paused ? "继续(" + pendingCount + ")" : "暂停"),
1018
- h("button", { type: "button", className: "lc-btn", onClick: clearLog }, "清空"),
1019
- h("button", { type: "button", className: "lc-btn", onClick: copyLog }, "复制"),
1020
- h("button", { type: "button", className: "lc-btn", onClick: exportLog }, "导出"),
1067
+ h("button", { type: "button", className: "lc-icobtn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, "📦"),
1068
+ h("button", { type: "button", className: "lc-icobtn", "data-on": paused ? "" : undefined, onClick: togglePause, title: "暂停冻结视图(新日志缓冲) / 继续回放" },
1069
+ paused ? "" : ""),
1070
+ h("button", { type: "button", className: "lc-icobtn", onClick: clearLog, title: "清空当前视图" }, "🗑"),
1071
+ h("button", { type: "button", className: "lc-icobtn", onClick: copyLog, title: "复制过滤后日志" }, "📋"),
1072
+ h("button", { type: "button", className: "lc-icobtn", onClick: exportLog, title: "导出过滤后日志 .txt" }, ""),
1021
1073
  h("button", {
1022
1074
  type: "button",
1023
- className: "lc-btn",
1075
+ className: "lc-icobtn",
1024
1076
  "data-on": autoScroll ? "" : undefined,
1025
1077
  title: "新日志自动滚到底部",
1026
1078
  onClick: () => setAutoScroll(!autoScroll),
1027
- }, "自动滚动"),
1079
+ }, "📌"),
1028
1080
  )
1029
1081
  : null,
1030
1082
  tab === "log" || tab === "screen"
@@ -1107,6 +1159,7 @@ window.__ModuleLoader__.load({
1107
1159
  h("span", null, !hostScreenCapable ? "宿主版本过旧" : (screenOn ? "投屏中 (~" + screenFps + "fps)" : "投屏未开启")),
1108
1160
  h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 140 } }),
1109
1161
  h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
1162
+ h("button", { type: "button", className: "lc-btn", onClick: toggleScreenFullscreen, title: "全屏查看(Esc 退出)" }, "⛶"),
1110
1163
  h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
1111
1164
  ),
1112
1165
  !hostScreenCapable
@@ -1119,10 +1172,10 @@ window.__ModuleLoader__.load({
1119
1172
  : eventsOn
1120
1173
  ? (filteredEvents.length === 0
1121
1174
  ? h("div", { className: "lc-empty" }, "events 事件缓冲(已开启流,等待事件…)")
1122
- : h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef }))
1175
+ : h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine, rowDensity }))
1123
1176
  : (filtered.length === 0
1124
- ? h("div", { className: "lc-empty" }, paused ? "已暂停(" + entries.length + " 条已缓冲)" : "暂无日志")
1125
- : h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef })),
1177
+ ? h("div", { className: "lc-empty" }, paused ? "已暂停(" + entries.length + " 条已缓冲)" : (devices.length === 0 ? "连接设备并开启 USB 调试后自动开始…" : "暂无日志"))
1178
+ : h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine, rowDensity })),
1126
1179
  tab === "log" && !eventsOn && live && !paused
1127
1180
  ? h("button", { type: "button", className: "lc-fab", onClick: togglePause, title: "暂停:冻结当前视图(新日志进入缓冲)" }, "⏸ 暂停")
1128
1181
  : null,
@@ -1210,6 +1263,8 @@ window.__ModuleLoader__.load({
1210
1263
  deviceState === "unauthorized"
1211
1264
  ? h("span", { style: { color: "#ef5350" } }, "⚠ 设备未授权 — 请在手机上点击“允许 USB 调试”")
1212
1265
  : null,
1266
+ h("button", { type: "button", className: "lc-btn", style: { padding: "1px 6px", marginLeft: "auto" }, onClick: toggleDensity, title: "日志行密度(标准 20px / 紧凑 15px)" },
1267
+ rowDensity === "compact" ? "密集" : "标准"),
1213
1268
  ),
1214
1269
  );
1215
1270
  }
@@ -1231,9 +1286,33 @@ window.__ModuleLoader__.load({
1231
1286
  );
1232
1287
  }
1233
1288
 
1234
- /** Windowed log list: fixed 20px rows, only the visible slice is rendered. */
1235
- function VirtualLog({ entries, scrollTop, onScrollTop, bodyRef }) {
1236
- const ROW = 20;
1289
+ /** Split `text` on keyword hits; render hit ranges as <mark>. */
1290
+ function highlightMsg(text, keyword) {
1291
+ const terms = (keyword ?? "").trim().toLowerCase().split(/\s+/).filter(Boolean);
1292
+ if (terms.length === 0 || text === "") return text;
1293
+ const lower = text.toLowerCase();
1294
+ const ranges = [];
1295
+ for (const t of terms) {
1296
+ const idx = lower.indexOf(t);
1297
+ if (idx >= 0) ranges.push([idx, idx + t.length]);
1298
+ }
1299
+ if (ranges.length === 0) return text;
1300
+ ranges.sort((a, b) => a[0] - b[0]);
1301
+ const parts = [];
1302
+ let pos = 0;
1303
+ for (const [s, e] of ranges) {
1304
+ if (s < pos) continue; // overlapping hits: keep the first
1305
+ if (s > pos) parts.push(text.slice(pos, s));
1306
+ parts.push(h("mark", { className: "lc-hit", key: s }, text.slice(s, e)));
1307
+ pos = e;
1308
+ }
1309
+ if (pos < text.length) parts.push(text.slice(pos));
1310
+ return parts;
1311
+ }
1312
+
1313
+ /** Windowed log list: fixed rows, only the visible slice is rendered. */
1314
+ function VirtualLog({ entries, scrollTop, onScrollTop, bodyRef, keyword, onTagClick, onLineDblClick, rowDensity }) {
1315
+ const ROW = rowDensity === "compact" ? 15 : 20;
1237
1316
  const height = useRef(0);
1238
1317
  const [viewport, setViewport] = useState({ top: 0, bottom: 100 });
1239
1318
 
@@ -1269,22 +1348,23 @@ window.__ModuleLoader__.load({
1269
1348
  rows.push(
1270
1349
  h("div", {
1271
1350
  key: i,
1272
- className: "lc-line" + (e.cont === true ? " cont" : "") + (isCrash(e) ? " crash" : ""),
1351
+ className: "lc-line" + (e.cont === true ? " cont" : "") + (isCrash(e) ? " crash" : "") + (level !== " " ? " lw-" + level : ""),
1273
1352
  style: { top: i * ROW },
1274
- title: e.raw,
1353
+ title: e.raw + "\n(点 tag 过滤 · 双击复制该行)",
1354
+ onDoubleClick: () => { if (onLineDblClick) onLineDblClick(e.raw); },
1275
1355
  },
1276
1356
  e.ts !== "" ? h("span", { className: "ts" }, e.ts) : null,
1277
1357
  e.pid > 0 ? h("span", { className: "pid" }, e.pid + "-" + e.tid) : null,
1278
1358
  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),
1359
+ e.tag !== "" ? h("span", { className: "tag", title: "按此 tag 过滤", onClick: (ev) => { ev.stopPropagation(); if (onTagClick) onTagClick(e.tag); } }, e.tag) : null,
1360
+ h("span", { className: "msg" }, highlightMsg(e.msg !== "" ? e.msg : e.raw, keyword)),
1281
1361
  ),
1282
1362
  );
1283
1363
  }
1284
1364
 
1285
1365
  return h("div",
1286
1366
  {
1287
- className: "lc-log",
1367
+ className: "lc-log" + (rowDensity === "compact" ? " compact" : ""),
1288
1368
  ref: bodyRef,
1289
1369
  onScroll: (e) => { onScrollTop(e.target.scrollTop); },
1290
1370
  },
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.3",
4
+ "version": "0.6.5",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {