@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.
Files changed (2) hide show
  1. package/lib/client.js +80 -23
  2. 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: () => 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") }, "逆向"),
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-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"),
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-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 }, "导出"),
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-btn",
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.3",
4
+ "version": "0.6.4",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {