@windypro-rourou/dsh-logcat 0.6.6 → 0.8.0
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/README.en.md +3 -2
- package/README.md +13 -3
- package/lib/client.js +618 -66
- package/lib/index.js +590 -0
- package/lib/injector-jar.js +6 -0
- package/package.json +2 -4
package/lib/client.js
CHANGED
|
@@ -73,6 +73,87 @@ window.__ModuleLoader__.load({
|
|
|
73
73
|
.lc-line.lw-E { border-left: 3px solid #ef5350; }
|
|
74
74
|
.lc-line.lw-F { border-left: 3px solid #ab47bc; }
|
|
75
75
|
.lc-screen-img:fullscreen { background: #000; }
|
|
76
|
+
|
|
77
|
+
/* ---------------------------------------------------------------- v2 look */
|
|
78
|
+
/* Layered surface + tighter rhythm; appended last so it overrides the base. */
|
|
79
|
+
.dsh-logcat-panel {
|
|
80
|
+
background: light-dark(#fafbfc, #14171c);
|
|
81
|
+
}
|
|
82
|
+
.lc-header {
|
|
83
|
+
padding: 11px 14px; gap: 10px;
|
|
84
|
+
background: light-dark(rgba(0,0,0,.025), rgba(255,255,255,.025));
|
|
85
|
+
border-bottom: 1px solid light-dark(rgba(0,0,0,.09), rgba(255,255,255,.09));
|
|
86
|
+
}
|
|
87
|
+
.lc-title { font-size: 15px; font-weight: 700; letter-spacing: .2px; margin-right: 2px; }
|
|
88
|
+
.lc-version { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 999px; background: light-dark(rgba(66,133,244,.14), rgba(66,133,244,.2)); color: #4285f4; }
|
|
89
|
+
.lc-close { width: 26px; height: 26px; border-radius: 8px; border: 1px solid light-dark(rgba(0,0,0,.12), rgba(255,255,255,.14)); background: transparent; color: inherit; cursor: pointer; font-size: 13px; line-height: 1; display: inline-flex; align-items: center; justify-content: center; }
|
|
90
|
+
.lc-close:hover { background: light-dark(rgba(0,0,0,.06), rgba(255,255,255,.1)); }
|
|
91
|
+
.lc-dot.on { animation: lc-pulse 2s ease-in-out infinite; }
|
|
92
|
+
@keyframes lc-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(76,175,80,.55); } 50% { box-shadow: 0 0 0 5px rgba(76,175,80,0); } }
|
|
93
|
+
|
|
94
|
+
/* chips → soft cards */
|
|
95
|
+
.lc-stats { padding: 7px 14px; gap: 7px; }
|
|
96
|
+
.lc-chip {
|
|
97
|
+
padding: 3px 9px; border-radius: 9px;
|
|
98
|
+
background: light-dark(rgba(255,255,255,.9), rgba(255,255,255,.055));
|
|
99
|
+
border: 1px solid light-dark(rgba(0,0,0,.07), rgba(255,255,255,.08));
|
|
100
|
+
box-shadow: 0 1px 2px rgba(0,0,0,.05);
|
|
101
|
+
}
|
|
102
|
+
.lc-chip:hover { border-color: light-dark(rgba(0,0,0,.14), rgba(255,255,255,.16)); }
|
|
103
|
+
|
|
104
|
+
/* segmented tabs */
|
|
105
|
+
.lc-tabs { padding: 8px 12px 0; gap: 4px; border-bottom: 1px solid light-dark(rgba(0,0,0,.07), rgba(255,255,255,.08)); }
|
|
106
|
+
.lc-tab { border-radius: 999px; padding: 5px 13px; font-size: 12.5px; font-weight: 600; opacity: .68; transition: background .14s, opacity .14s, color .14s; }
|
|
107
|
+
.lc-tab[data-on] {
|
|
108
|
+
opacity: 1; color: light-dark(#fff, #fff);
|
|
109
|
+
background: linear-gradient(135deg, #4285f4, #5b8def);
|
|
110
|
+
box-shadow: 0 2px 8px rgba(66,133,244,.35);
|
|
111
|
+
}
|
|
112
|
+
.lc-tab[data-on]:hover { background: linear-gradient(135deg, #3b78e0, #4f83e4); }
|
|
113
|
+
|
|
114
|
+
/* toolbar grouping */
|
|
115
|
+
.lc-toolgroup { display: inline-flex; align-items: center; gap: 4px; }
|
|
116
|
+
.lc-toolsep { width: 1px; height: 18px; margin: 0 3px; background: light-dark(rgba(0,0,0,.12), rgba(255,255,255,.15)); }
|
|
117
|
+
.lc-icobtn { transition: background .12s, border-color .12s, transform .08s; }
|
|
118
|
+
.lc-icobtn:active { transform: scale(.93); }
|
|
119
|
+
.lc-btn:active, .lc-key:active { transform: scale(.96); }
|
|
120
|
+
|
|
121
|
+
/* live screen: device-like framing, badge, tap ripple */
|
|
122
|
+
.lc-screen-img {
|
|
123
|
+
background: radial-gradient(120% 90% at 50% 0%, light-dark(#eef1f6, #1b2027) 0%, light-dark(#e4e8ef, #0d1014) 100%);
|
|
124
|
+
padding: 10px;
|
|
125
|
+
}
|
|
126
|
+
.lc-screen-img canvas {
|
|
127
|
+
border-radius: 12px;
|
|
128
|
+
box-shadow: 0 10px 30px rgba(0,0,0,.42), 0 0 0 1px rgba(255,255,255,.06);
|
|
129
|
+
background: #000;
|
|
130
|
+
}
|
|
131
|
+
.lc-screen-badge {
|
|
132
|
+
position: absolute; top: 14px; left: 14px; z-index: 3;
|
|
133
|
+
font-size: 10px; font-weight: 600; padding: 3px 9px; border-radius: 999px;
|
|
134
|
+
background: rgba(8,10,14,.62); color: #dfe6f2; backdrop-filter: blur(6px);
|
|
135
|
+
border: 1px solid rgba(255,255,255,.12); pointer-events: none;
|
|
136
|
+
}
|
|
137
|
+
.lc-screen-badge.warn { color: #ffd479; border-color: rgba(251,192,45,.4); }
|
|
138
|
+
.lc-tap-ripple {
|
|
139
|
+
position: fixed; z-index: 4; width: 30px; height: 30px; margin: -15px 0 0 -15px;
|
|
140
|
+
border-radius: 50%; background: rgba(66,133,244,.5); pointer-events: none;
|
|
141
|
+
animation: lc-ripple .42s ease-out forwards;
|
|
142
|
+
}
|
|
143
|
+
@keyframes lc-ripple { from { transform: scale(.35); opacity: .95; } to { transform: scale(1.7); opacity: 0; } }
|
|
144
|
+
|
|
145
|
+
/* friendlier empty states */
|
|
146
|
+
.lc-empty { flex-direction: column; gap: 10px; text-align: center; padding: 0 24px; }
|
|
147
|
+
.lc-empty-icon { font-size: 30px; opacity: .45; }
|
|
148
|
+
|
|
149
|
+
/* log line polish */
|
|
150
|
+
.lc-line { transition: background .1s; }
|
|
151
|
+
.lc-line .tag { cursor: pointer; }
|
|
152
|
+
.lc-line .tag:hover { text-decoration: underline; }
|
|
153
|
+
.lc-hit { background: linear-gradient(180deg, rgba(66,133,244,.42), rgba(66,133,244,.28)); border-radius: 3px; padding: 0 1px; }
|
|
154
|
+
|
|
155
|
+
/* RE workbench headers */
|
|
156
|
+
.lc-re-head { letter-spacing: .2px; background: light-dark(rgba(0,0,0,.025), rgba(255,255,255,.03)); }
|
|
76
157
|
.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; }
|
|
77
158
|
.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; }
|
|
78
159
|
.lc-select:focus, .lc-search:focus { outline: 2px solid rgba(66,133,244,.35); border-color: rgba(66,133,244,.5); }
|
|
@@ -157,6 +238,53 @@ window.__ModuleLoader__.load({
|
|
|
157
238
|
.lc-pausedbar .lc-btn { padding: 2px 10px; }
|
|
158
239
|
.lc-fab { position: absolute; right: 14px; bottom: 14px; z-index: 4; border: 1px solid rgba(66,133,244,.45); background: light-dark(rgba(66,133,244,.14), rgba(66,133,244,.24)); color: #4285f4; font: inherit; font-size: 12px; font-weight: 700; padding: 8px 14px; border-radius: 999px; cursor: pointer; box-shadow: 0 4px 14px rgba(0,0,0,.22); }
|
|
159
240
|
.lc-fab:hover { background: light-dark(rgba(66,133,244,.22), rgba(66,133,244,.34)); }
|
|
241
|
+
/* ---- design pass: token layer + refinements ------------------------------
|
|
242
|
+
Colours resolve from the DSH design tokens when the host exposes them and
|
|
243
|
+
fall back to a hand-tuned pair otherwise, so the panel reads native on 0.2
|
|
244
|
+
and still looks right on older clients. One accent (blue); severity hues are
|
|
245
|
+
data encoding, not decoration. */
|
|
246
|
+
.lc-panel {
|
|
247
|
+
--lc-accent: var(--dsw-alias-brand-primary, #3b82f6);
|
|
248
|
+
--lc-accent-soft: light-dark(rgba(59,130,246,.10), rgba(59,130,246,.20));
|
|
249
|
+
--lc-raised: var(--dsw-alias-bg-layer-2, light-dark(#f7f8fa, #1c1f26));
|
|
250
|
+
--lc-fg: var(--dsw-alias-label-primary, light-dark(#16181d, #e6e9ef));
|
|
251
|
+
--lc-fg-2: var(--dsw-alias-label-secondary, light-dark(#4a4f57, #a8afba));
|
|
252
|
+
--lc-dim: var(--dsw-alias-label-caption, light-dark(#6b7078, #8b93a0));
|
|
253
|
+
--lc-border: var(--dsw-alias-border-l2, light-dark(rgba(0,0,0,.10), rgba(255,255,255,.12)));
|
|
254
|
+
--lc-border-strong: var(--dsw-alias-border-l4, light-dark(rgba(0,0,0,.20), rgba(255,255,255,.22)));
|
|
255
|
+
--lc-hover: var(--dsw-alias-interactive-bg-hover, light-dark(rgba(0,0,0,.045), rgba(255,255,255,.07)));
|
|
256
|
+
background: var(--dsw-alias-bg-layer-1, light-dark(#ffffff, #16181d));
|
|
257
|
+
color: var(--lc-fg);
|
|
258
|
+
font-family: system-ui, -apple-system, "Segoe UI Variable Text", "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
259
|
+
}
|
|
260
|
+
.lc-header { padding: 11px 14px; gap: 9px; border-bottom-color: var(--lc-border); }
|
|
261
|
+
.lc-title { font-size: 14px; font-weight: 650; letter-spacing: -.01em; }
|
|
262
|
+
.lc-tabs { gap: 2px; padding: 0 10px; border-bottom-color: var(--lc-border); }
|
|
263
|
+
.lc-tab { font-size: 13px; font-weight: 500; padding: 8px 12px; border-radius: 0; box-shadow: none; opacity: .62; transition: opacity .12s ease, color .12s ease; }
|
|
264
|
+
.lc-tab:hover { opacity: .95; background: transparent; }
|
|
265
|
+
.lc-tab[data-on] { opacity: 1; background: transparent; color: var(--lc-accent); box-shadow: inset 0 -2px 0 var(--lc-accent); }
|
|
266
|
+
.lc-chip { background: light-dark(rgba(0,0,0,.035), rgba(255,255,255,.05)); font-variant-numeric: tabular-nums; }
|
|
267
|
+
.lc-icobtn { width: 28px; height: 26px; border-radius: 8px; border-color: transparent; color: var(--lc-fg-2); transition: background .12s ease, color .12s ease, border-color .12s ease; }
|
|
268
|
+
.lc-icobtn:hover { background: var(--lc-hover); color: var(--lc-fg); }
|
|
269
|
+
.lc-icobtn[data-on] { background: var(--lc-accent-soft); border-color: var(--lc-accent); color: var(--lc-accent); }
|
|
270
|
+
.lc-btn { border-color: transparent; border-radius: 8px; color: var(--lc-fg-2); transition: background .12s ease, color .12s ease; }
|
|
271
|
+
.lc-btn:hover { background: var(--lc-hover); color: var(--lc-fg); }
|
|
272
|
+
.lc-btn[data-on] { background: var(--lc-accent-soft); color: var(--lc-accent); }
|
|
273
|
+
.lc-icobtn:focus-visible, .lc-btn:focus-visible, .lc-key:focus-visible, .lc-tab:focus-visible { outline: 2px solid var(--lc-accent); outline-offset: 1px; }
|
|
274
|
+
.lc-key { display: inline-flex; align-items: center; gap: 4px; border-radius: 7px; border-color: transparent; background: light-dark(rgba(0,0,0,.035), rgba(255,255,255,.05)); }
|
|
275
|
+
.lc-key:hover { background: var(--lc-hover); }
|
|
276
|
+
.lc-search, .lc-select { border-radius: 8px; border-color: var(--lc-border); background: var(--lc-raised); transition: border-color .12s ease, background .12s ease; }
|
|
277
|
+
.lc-search:focus, .lc-select:focus { border-color: var(--lc-accent); background: light-dark(#ffffff, #16181d); outline: none; }
|
|
278
|
+
.lc-line { font-family: ui-monospace, "Cascadia Mono", "JetBrains Mono", Consolas, monospace; font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
279
|
+
.lc-line:hover { background: var(--lc-hover); }
|
|
280
|
+
.lc-log.compact .lc-line { font-size: 11px; }
|
|
281
|
+
.lc-status { background: light-dark(rgba(0,0,0,.015), rgba(255,255,255,.022)); font-variant-numeric: tabular-nums; border-top-color: var(--lc-border); }
|
|
282
|
+
.lc-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; color: var(--lc-dim); font-size: 12.5px; text-align: center; }
|
|
283
|
+
.lc-empty-icon { color: light-dark(rgba(0,0,0,.20), rgba(255,255,255,.22)); }
|
|
284
|
+
.lc-overlay { background: light-dark(rgba(15,23,42,.28), rgba(0,0,0,.48)); backdrop-filter: blur(2px); }
|
|
285
|
+
.lc-modal { border-radius: 14px; border-color: var(--lc-border-strong); box-shadow: 0 24px 64px light-dark(rgba(15,23,42,.20), rgba(0,0,0,.55)); }
|
|
286
|
+
.lc-hit { background: var(--lc-accent-soft); }
|
|
287
|
+
.lc-re-head { text-transform: none; letter-spacing: 0; }
|
|
160
288
|
`;
|
|
161
289
|
//#endregion
|
|
162
290
|
|
|
@@ -190,9 +318,11 @@ window.__ModuleLoader__.load({
|
|
|
190
318
|
const API_BASE = "/api/dsh-logcat";
|
|
191
319
|
const LEVELS = ["V", "D", "I", "W", "E", "F"];
|
|
192
320
|
const LEVEL_TITLES = { V: "详细", D: "调试", I: "信息", W: "警告", E: "错误", F: "致命" };
|
|
321
|
+
/** H.264 screen-stream tiers: encoded size → bitrate (bps). */
|
|
322
|
+
const VIDEO_TIERS = { "360x800": 1200000, "540x1200": 2500000, "720x1600": 4000000, "1080x2400": 6000000 };
|
|
193
323
|
|
|
194
324
|
/** The Logcat panel view. */
|
|
195
|
-
function LogcatPanel({ controller }) {
|
|
325
|
+
function LogcatPanel({ controller, embedded }) {
|
|
196
326
|
const [adbPath, setAdbPath] = useState("");
|
|
197
327
|
const [adbReady, setAdbReady] = useState(false);
|
|
198
328
|
const [devices, setDevices] = useState([]);
|
|
@@ -253,6 +383,16 @@ window.__ModuleLoader__.load({
|
|
|
253
383
|
const [screenFps, setScreenFps] = useState(0);
|
|
254
384
|
const [screenHasFrame, setScreenHasFrame] = useState(false);
|
|
255
385
|
const [hostScreenCapable, setHostScreenCapable] = useState(false);
|
|
386
|
+
// null = unknown (status not fetched yet); false = the running GUI host predates the
|
|
387
|
+
// H.264 engine, so the video stream cannot work until that process is restarted.
|
|
388
|
+
const [hostVideoCapable, setHostVideoCapable] = useState(null);
|
|
389
|
+
// serial -> { width, height, rotation }: the device's current oriented display
|
|
390
|
+
// geometry, used to translate touch positions out of frame space.
|
|
391
|
+
const [displays, setDisplays] = useState({});
|
|
392
|
+
const [screenMode, setScreenMode] = useState(() => { try { return localStorage.getItem("dsh-logcat-screenmode-v2") === "png" ? "png" : "video"; } catch { return "video"; } });
|
|
393
|
+
const [videoTier, setVideoTier] = useState(() => { try { const t = localStorage.getItem("dsh-logcat-videotier"); return t !== null && VIDEO_TIERS[t] !== undefined ? t : "540x1200"; } catch { return "540x1200"; } });
|
|
394
|
+
const [videoInfo, setVideoInfo] = useState(null);
|
|
395
|
+
const [ripples, setRipples] = useState([]);
|
|
256
396
|
const panelOpen = useSyncExternalStore(controller.subscribe, controller.getSnapshot).panelOpen;
|
|
257
397
|
|
|
258
398
|
const wsRef = useRef(null);
|
|
@@ -276,6 +416,12 @@ window.__ModuleLoader__.load({
|
|
|
276
416
|
const screenDecodingRef = useRef(false);
|
|
277
417
|
const screenPendingRef = useRef(null);
|
|
278
418
|
const screenFpsTsRef = useRef([]);
|
|
419
|
+
const decoderRef = useRef(null);
|
|
420
|
+
const screenModeRef = useRef(screenMode);
|
|
421
|
+
const videoTierRef = useRef(videoTier);
|
|
422
|
+
const videoInfoRef = useRef(null);
|
|
423
|
+
const videoFallbackTimerRef = useRef(null);
|
|
424
|
+
videoInfoRef.current = videoInfo;
|
|
279
425
|
|
|
280
426
|
useEffect(() => { serialRef.current = serial; }, [serial]);
|
|
281
427
|
useEffect(() => { pausedRef.current = paused; }, [paused]);
|
|
@@ -339,6 +485,22 @@ window.__ModuleLoader__.load({
|
|
|
339
485
|
}
|
|
340
486
|
};
|
|
341
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Re-assert the live-screen intent. `ready` also arrives after a reconnect, and the host
|
|
490
|
+
* may have been restarted in the meantime — without this the panel would sit on a dead
|
|
491
|
+
* stream until the user happens to toggle something.
|
|
492
|
+
*/
|
|
493
|
+
const reassertStream = () => {
|
|
494
|
+
const ws = wsRef.current;
|
|
495
|
+
const target = serialRef.current;
|
|
496
|
+
if (ws === null || ws.readyState !== WebSocket.OPEN || target === "" || !screenWantRef.current) return;
|
|
497
|
+
const useVideo = screenModeRef.current === "video" && typeof VideoDecoder !== "undefined";
|
|
498
|
+
const tier = videoTierRef.current;
|
|
499
|
+
ws.send(JSON.stringify({ type: "video", serial: target, on: useVideo, size: tier, bitRate: VIDEO_TIERS[tier] ?? 2500000 }));
|
|
500
|
+
ws.send(JSON.stringify({ type: "screen", serial: target, on: !useVideo }));
|
|
501
|
+
setScreenOn(true);
|
|
502
|
+
};
|
|
503
|
+
|
|
342
504
|
const applyFrame = (frame) => {
|
|
343
505
|
if (frame.type === "ready") {
|
|
344
506
|
setAdbPath(frame.adbPath ?? "");
|
|
@@ -351,6 +513,7 @@ window.__ModuleLoader__.load({
|
|
|
351
513
|
const first = (frame.devices ?? []).find((d) => d.state === "device");
|
|
352
514
|
if (first !== undefined) pickSerial(first.serial);
|
|
353
515
|
}
|
|
516
|
+
reassertStream();
|
|
354
517
|
} else if (frame.type === "devices") {
|
|
355
518
|
setDevices(frame.devices ?? []);
|
|
356
519
|
devicesRef.current = frame.devices ?? [];
|
|
@@ -390,6 +553,19 @@ window.__ModuleLoader__.load({
|
|
|
390
553
|
}
|
|
391
554
|
} else if (frame.type === "crash") {
|
|
392
555
|
setLastCrash(frame.crash ?? null);
|
|
556
|
+
} else if (frame.type === "svideo-init") {
|
|
557
|
+
if (frame.serial === serialRef.current) setupVideoDecoder(frame);
|
|
558
|
+
} else if (frame.type === "video-state") {
|
|
559
|
+
if (frame.serial !== serialRef.current) return;
|
|
560
|
+
if (frame.display !== undefined && frame.display !== null) setDisplays((prev) => ({ ...prev, [frame.serial]: frame.display }));
|
|
561
|
+
setVideoInfo((prev) => ({
|
|
562
|
+
...(prev ?? {}),
|
|
563
|
+
running: frame.running === true,
|
|
564
|
+
idle: frame.idle === true,
|
|
565
|
+
size: frame.size ?? prev?.size,
|
|
566
|
+
bitRate: frame.bitRate ?? prev?.bitRate,
|
|
567
|
+
error: frame.running === true ? (frame.error ?? prev?.error) : (frame.error ?? prev?.error ?? "视频流已停止"),
|
|
568
|
+
}));
|
|
393
569
|
}
|
|
394
570
|
};
|
|
395
571
|
applyFrameRef.current = applyFrame;
|
|
@@ -447,6 +623,8 @@ window.__ModuleLoader__.load({
|
|
|
447
623
|
setStreaming(body.streaming ?? []);
|
|
448
624
|
setEventsStreaming(body.eventsStreaming ?? []);
|
|
449
625
|
setHostScreenCapable(body.screenCapable === true);
|
|
626
|
+
setHostVideoCapable(body.videoCapable === true || Array.isArray(body.videoStreaming));
|
|
627
|
+
setDisplays(body.displays ?? {});
|
|
450
628
|
setCurrentPackage(body.currentPackage ?? "");
|
|
451
629
|
setCurrentVersion(body.currentVersion ?? "");
|
|
452
630
|
setLatestVersion(body.latestVersion ?? "");
|
|
@@ -825,20 +1003,109 @@ window.__ModuleLoader__.load({
|
|
|
825
1003
|
reDumpAt("0x" + addr.toLowerCase(), reDumpLen);
|
|
826
1004
|
};
|
|
827
1005
|
|
|
1006
|
+
// Physical keyboard → device (while the Screen tab is focused and no text field is active).
|
|
1007
|
+
useEffect(() => {
|
|
1008
|
+
if (tab !== "screen" || !panelOpen || serial === "") return undefined;
|
|
1009
|
+
const KEYS = { Enter: 66, Backspace: 67, Escape: 4, Tab: 61, Delete: 112, ArrowUp: 19, ArrowDown: 20, ArrowLeft: 21, ArrowRight: 22, Home: 3 };
|
|
1010
|
+
const onKeyDown = (ev) => {
|
|
1011
|
+
const el = document.activeElement;
|
|
1012
|
+
if (el !== null && (el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.isContentEditable === true)) return;
|
|
1013
|
+
if (ev.ctrlKey || ev.metaKey || ev.altKey) return;
|
|
1014
|
+
if (KEYS[ev.key] !== undefined) {
|
|
1015
|
+
ev.preventDefault();
|
|
1016
|
+
fetch(API_BASE + "/keyevent", {
|
|
1017
|
+
method: "POST",
|
|
1018
|
+
headers: { "content-type": "application/json" },
|
|
1019
|
+
body: JSON.stringify({ serial, code: KEYS[ev.key] }),
|
|
1020
|
+
}).catch(() => { /* offline */ });
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
if (ev.key.length === 1) {
|
|
1024
|
+
ev.preventDefault();
|
|
1025
|
+
// Single printable char: ASCII only (use the text box for IME/Chinese input).
|
|
1026
|
+
const ch = ev.key === " " ? "%s" : ev.key.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1027
|
+
fetch(API_BASE + "/exec", {
|
|
1028
|
+
method: "POST",
|
|
1029
|
+
headers: { "content-type": "application/json" },
|
|
1030
|
+
body: JSON.stringify({ serial, command: `input text "${ch}"` }),
|
|
1031
|
+
}).catch(() => { /* offline */ });
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
window.addEventListener("keydown", onKeyDown);
|
|
1035
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
1036
|
+
}, [tab, panelOpen, serial]);
|
|
1037
|
+
|
|
1038
|
+
// Esc closes the panel — except on the Screen tab, where Esc is the device BACK key.
|
|
1039
|
+
useEffect(() => {
|
|
1040
|
+
if (!panelOpen || tab === "screen") return undefined;
|
|
1041
|
+
const onKey = (ev) => {
|
|
1042
|
+
if (ev.key !== "Escape") return;
|
|
1043
|
+
const el = document.activeElement;
|
|
1044
|
+
if (el !== null && (el.tagName === "INPUT" || el.tagName === "TEXTAREA")) {
|
|
1045
|
+
if (typeof el.blur === "function") el.blur();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
controller.close();
|
|
1049
|
+
};
|
|
1050
|
+
window.addEventListener("keydown", onKey);
|
|
1051
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1052
|
+
}, [panelOpen, tab]);
|
|
1053
|
+
|
|
828
1054
|
// Live screen stream: on while the 屏幕 tab is open, the panel is open, and a device is selected.
|
|
1055
|
+
// Preferred path is the H.264 stream (30fps+); the PNG screencap path is the fallback.
|
|
829
1056
|
useEffect(() => {
|
|
830
1057
|
const prev = prevSerialRef.current;
|
|
831
1058
|
prevSerialRef.current = serial;
|
|
1059
|
+
screenModeRef.current = screenMode;
|
|
1060
|
+
videoTierRef.current = videoTier;
|
|
832
1061
|
const want = tab === "screen" && serial !== "" && panelOpen;
|
|
833
1062
|
screenWantRef.current = want;
|
|
834
1063
|
const ws = wsRef.current;
|
|
1064
|
+
const useVideo = want && screenMode === "video" && typeof VideoDecoder !== "undefined" && hostVideoCapable !== false;
|
|
835
1065
|
if (ws !== null && ws.readyState === WebSocket.OPEN) {
|
|
836
|
-
if (prev !== "" && prev !== serial)
|
|
837
|
-
|
|
1066
|
+
if (prev !== "" && prev !== serial) {
|
|
1067
|
+
ws.send(JSON.stringify({ type: "screen", serial: prev, on: false }));
|
|
1068
|
+
ws.send(JSON.stringify({ type: "video", serial: prev, on: false }));
|
|
1069
|
+
}
|
|
1070
|
+
ws.send(JSON.stringify({ type: "video", serial, on: useVideo, size: videoTier, bitRate: VIDEO_TIERS[videoTier] ?? 2500000 }));
|
|
1071
|
+
ws.send(JSON.stringify({ type: "screen", serial, on: want && !useVideo }));
|
|
838
1072
|
}
|
|
839
1073
|
setScreenOn(want);
|
|
840
|
-
if (!want)
|
|
841
|
-
|
|
1074
|
+
if (!want) {
|
|
1075
|
+
setScreenHasFrame(false);
|
|
1076
|
+
setVideoInfo(null);
|
|
1077
|
+
closeVideoDecoder();
|
|
1078
|
+
}
|
|
1079
|
+
if (videoFallbackTimerRef.current !== null) { clearTimeout(videoFallbackTimerRef.current); videoFallbackTimerRef.current = null; }
|
|
1080
|
+
if (useVideo) {
|
|
1081
|
+
// If no avcC init arrives in 6s, the device/browser cannot do H.264 → PNG mode.
|
|
1082
|
+
videoFallbackTimerRef.current = setTimeout(() => {
|
|
1083
|
+
videoFallbackTimerRef.current = null;
|
|
1084
|
+
if (videoInfoRef.current?.codec) return;
|
|
1085
|
+
// A stale GUI host simply ignores the `video` request. Do NOT fall back to
|
|
1086
|
+
// PNG mode here and do NOT persist anything: this is a host-version problem,
|
|
1087
|
+
// and it must heal by itself once the host is restarted.
|
|
1088
|
+
if (hostVideoCapable === false) {
|
|
1089
|
+
setVideoInfo({ error: "宿主进程仍是旧版(v" + currentVersion + "),H.264 视频流不可用 —— 重启 GUI 后刷新即恢复" });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
setVideoInfo({ error: "视频流启动超时,已回退到截图模式" });
|
|
1093
|
+
setScreenMode("png");
|
|
1094
|
+
// Deliberately not persisted: an automatic fallback must never become sticky,
|
|
1095
|
+
// otherwise a transient failure would disable the video path forever.
|
|
1096
|
+
}, 6000);
|
|
1097
|
+
}
|
|
1098
|
+
// Keep the oriented display geometry fresh while the screen tab is open: touch
|
|
1099
|
+
// mapping depends on it and the device can rotate at any moment.
|
|
1100
|
+
if (!want) return;
|
|
1101
|
+
const displayTimer = setInterval(() => {
|
|
1102
|
+
fetch(API_BASE + "/status")
|
|
1103
|
+
.then((res) => res.json())
|
|
1104
|
+
.then((body) => { if (body.displays !== undefined) setDisplays(body.displays); })
|
|
1105
|
+
.catch(() => { /* offline */ });
|
|
1106
|
+
}, 4000);
|
|
1107
|
+
return () => clearInterval(displayTimer);
|
|
1108
|
+
}, [tab, serial, panelOpen, screenMode, videoTier, hostVideoCapable]);
|
|
842
1109
|
|
|
843
1110
|
// Binary WS frames: 4-byte JSON header length + header + PNG payload.
|
|
844
1111
|
// Decoding goes through createImageBitmap into a canvas (no base64
|
|
@@ -860,6 +1127,11 @@ window.__ModuleLoader__.load({
|
|
|
860
1127
|
c2d.drawImage(bmp, 0, 0);
|
|
861
1128
|
bmp.close();
|
|
862
1129
|
setScreenHasFrame(true);
|
|
1130
|
+
const now = performance.now();
|
|
1131
|
+
const ts = screenFpsTsRef.current;
|
|
1132
|
+
ts.push(now);
|
|
1133
|
+
while (ts.length > 0 && now - ts[0] > 1500) ts.shift();
|
|
1134
|
+
setScreenFps(Math.min(Math.round(ts.length / 1.5), 120));
|
|
863
1135
|
} catch { /* undecodable frame — skip */ }
|
|
864
1136
|
}
|
|
865
1137
|
} finally {
|
|
@@ -872,14 +1144,14 @@ window.__ModuleLoader__.load({
|
|
|
872
1144
|
const view = new DataView(buffer);
|
|
873
1145
|
const hlen = view.getUint32(0);
|
|
874
1146
|
const header = JSON.parse(new TextDecoder().decode(new Uint8Array(buffer, 4, hlen)));
|
|
875
|
-
if (header.
|
|
1147
|
+
if (header.serial !== serialRef.current) return;
|
|
876
1148
|
const bytes = new Uint8Array(buffer, 4 + hlen);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1149
|
+
if (header.type === "svideo") {
|
|
1150
|
+
decodeVideoFrame(header, bytes);
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
if (header.type !== "simage") return;
|
|
1154
|
+
// PNG fallback path: keep only the newest pending frame.
|
|
883
1155
|
screenPendingRef.current = bytes;
|
|
884
1156
|
void pumpScreenFrame();
|
|
885
1157
|
} catch { /* malformed frame */ }
|
|
@@ -887,19 +1159,55 @@ window.__ModuleLoader__.load({
|
|
|
887
1159
|
handleBinaryRef.current = handleBinary;
|
|
888
1160
|
|
|
889
1161
|
const screenToDevice = (e) => {
|
|
890
|
-
const
|
|
891
|
-
if (
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
const
|
|
895
|
-
|
|
896
|
-
|
|
1162
|
+
const canvas = screenCanvasRef.current;
|
|
1163
|
+
if (canvas === null || canvas.width <= 1) return null;
|
|
1164
|
+
const rect = canvas.getBoundingClientRect();
|
|
1165
|
+
if (rect.width <= 0 || rect.height <= 0) return null;
|
|
1166
|
+
const u = (e.clientX - rect.left) / rect.width;
|
|
1167
|
+
const v = (e.clientY - rect.top) / rect.height;
|
|
1168
|
+
if (u < 0 || u > 1 || v < 0 || v > 1) return null;
|
|
1169
|
+
// Touch coordinates live in the device's CURRENT oriented display space, which is
|
|
1170
|
+
// not the encoded frame space (a landscape device encodes a portrait frame unless
|
|
1171
|
+
// the stream size follows the rotation).
|
|
1172
|
+
const disp = displays[serial];
|
|
1173
|
+
const w = disp !== undefined && disp.width > 0 ? disp.width : canvas.width;
|
|
1174
|
+
const h = disp !== undefined && disp.height > 0 ? disp.height : canvas.height;
|
|
1175
|
+
return { x: Math.round(u * w), y: Math.round(v * h) };
|
|
897
1176
|
};
|
|
898
1177
|
|
|
899
1178
|
const screenMouseDown = (e) => {
|
|
1179
|
+
if (e.button === 2) return; // right-click handled by contextmenu (back key)
|
|
900
1180
|
const pt = screenToDevice(e);
|
|
901
1181
|
if (pt === null) return;
|
|
902
|
-
|
|
1182
|
+
const now = Date.now();
|
|
1183
|
+
const last = screenDragRef.current;
|
|
1184
|
+
// double-click → two taps
|
|
1185
|
+
if (last !== null && last.tapOnly === true && now - last.endedAt < 320 && Math.abs(last.x - pt.x) < 48 && Math.abs(last.y - pt.y) < 48) {
|
|
1186
|
+
screenDragRef.current = null;
|
|
1187
|
+
tapAt(pt.x, pt.y);
|
|
1188
|
+
tapAt(pt.x, pt.y);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
showRipple(e);
|
|
1192
|
+
screenDragRef.current = { ...pt, clientX: e.clientX, clientY: e.clientY, startedAt: now, held: false };
|
|
1193
|
+
};
|
|
1194
|
+
|
|
1195
|
+
/** Send a tap at device coordinates. */
|
|
1196
|
+
const tapAt = (x, y) => {
|
|
1197
|
+
fetch(API_BASE + "/exec", {
|
|
1198
|
+
method: "POST",
|
|
1199
|
+
headers: { "content-type": "application/json" },
|
|
1200
|
+
body: JSON.stringify({ serial, command: `input tap ${x} ${y}` }),
|
|
1201
|
+
}).catch(() => { /* offline */ });
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1204
|
+
/** Send a swipe (used by drag, wheel and long-press). */
|
|
1205
|
+
const swipeAt = (x1, y1, x2, y2, ms) => {
|
|
1206
|
+
fetch(API_BASE + "/exec", {
|
|
1207
|
+
method: "POST",
|
|
1208
|
+
headers: { "content-type": "application/json" },
|
|
1209
|
+
body: JSON.stringify({ serial, command: `input swipe ${x1} ${y1} ${x2} ${y2} ${ms}` }),
|
|
1210
|
+
}).catch(() => { /* offline */ });
|
|
903
1211
|
};
|
|
904
1212
|
|
|
905
1213
|
const screenMouseUp = (e) => {
|
|
@@ -911,13 +1219,43 @@ window.__ModuleLoader__.load({
|
|
|
911
1219
|
const dx = pt.x - drag.x;
|
|
912
1220
|
const dy = pt.y - drag.y;
|
|
913
1221
|
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
:
|
|
917
|
-
|
|
1222
|
+
const heldMs = Date.now() - (drag.startedAt ?? Date.now());
|
|
1223
|
+
if (dist < 24 && heldMs > 620) {
|
|
1224
|
+
// long press: swipe in place
|
|
1225
|
+
swipeAt(drag.x, drag.y, drag.x, drag.y, 800);
|
|
1226
|
+
screenDragRef.current = { x: pt.x, y: pt.y, tapOnly: true, endedAt: Date.now() };
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (dist < 24) {
|
|
1230
|
+
tapAt(drag.x, drag.y);
|
|
1231
|
+
screenDragRef.current = { x: pt.x, y: pt.y, tapOnly: true, endedAt: Date.now() };
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
swipeAt(drag.x, drag.y, pt.x, pt.y, 120);
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
/** Mouse wheel over the live screen → vertical swipe (natural scrolling). */
|
|
1238
|
+
const lastWheelRef = { current: 0 };
|
|
1239
|
+
const screenWheel = (e) => {
|
|
1240
|
+
const pt = screenToDevice(e);
|
|
1241
|
+
if (pt === null) return;
|
|
1242
|
+
e.preventDefault();
|
|
1243
|
+
const now = Date.now();
|
|
1244
|
+
if (now - lastWheelRef.current < 220) return;
|
|
1245
|
+
lastWheelRef.current = now;
|
|
1246
|
+
const step = Math.round((e.deltaY > 0 ? 1 : -1) * Math.min(Math.abs(e.deltaY), 420));
|
|
1247
|
+
const disp = displays[serial];
|
|
1248
|
+
const dh = disp !== undefined && disp.height > 0 ? disp.height : 2408;
|
|
1249
|
+
swipeAt(pt.x, pt.y, pt.x, Math.max(4, Math.min(dh - 12, pt.y - step)), 140);
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
/** Right click → Android BACK key. */
|
|
1253
|
+
const screenContextMenu = (e) => {
|
|
1254
|
+
e.preventDefault();
|
|
1255
|
+
fetch(API_BASE + "/keyevent", {
|
|
918
1256
|
method: "POST",
|
|
919
1257
|
headers: { "content-type": "application/json" },
|
|
920
|
-
body: JSON.stringify({ serial,
|
|
1258
|
+
body: JSON.stringify({ serial, key: "back" }),
|
|
921
1259
|
}).catch(() => { /* offline */ });
|
|
922
1260
|
};
|
|
923
1261
|
|
|
@@ -933,6 +1271,18 @@ window.__ModuleLoader__.load({
|
|
|
933
1271
|
}).catch(() => { /* offline */ });
|
|
934
1272
|
};
|
|
935
1273
|
|
|
1274
|
+
/** Show a short ripple where the user tapped the live screen. */
|
|
1275
|
+
const showRipple = (e) => {
|
|
1276
|
+
const wrap = screenWrapRef.current;
|
|
1277
|
+
if (wrap === null) return;
|
|
1278
|
+
const id = Date.now() + Math.random();
|
|
1279
|
+
// Viewport-fixed: the ripple must sit under the cursor no matter how the canvas is
|
|
1280
|
+
// scaled, letterboxed or rotated inside the wrapper.
|
|
1281
|
+
const ripple = { id, x: e.clientX, y: e.clientY };
|
|
1282
|
+
setRipples((prev) => [...prev.slice(-3), ripple]);
|
|
1283
|
+
setTimeout(() => setRipples((prev) => prev.filter((r) => r.id !== id)), 430);
|
|
1284
|
+
};
|
|
1285
|
+
|
|
936
1286
|
const downloadScreen = () => {
|
|
937
1287
|
const canvas = screenCanvasRef.current;
|
|
938
1288
|
if (canvas === null || canvas.width <= 1) return;
|
|
@@ -953,27 +1303,92 @@ window.__ModuleLoader__.load({
|
|
|
953
1303
|
try { el.requestFullscreen?.(); } catch { /* denied */ }
|
|
954
1304
|
};
|
|
955
1305
|
|
|
1306
|
+
// ---- H.264 path (WebCodecs): screenrecord stream → VideoDecoder → canvas ----
|
|
1307
|
+
|
|
1308
|
+
const closeVideoDecoder = () => {
|
|
1309
|
+
const decoder = decoderRef.current;
|
|
1310
|
+
decoderRef.current = null;
|
|
1311
|
+
if (decoder !== null) { try { decoder.close(); } catch { /* already closed */ } }
|
|
1312
|
+
};
|
|
1313
|
+
|
|
1314
|
+
/** Paint one decoded VideoFrame and update the fps readout. */
|
|
1315
|
+
const drawVideoFrame = (frame) => {
|
|
1316
|
+
try {
|
|
1317
|
+
const canvas = screenCanvasRef.current;
|
|
1318
|
+
if (canvas !== null) {
|
|
1319
|
+
if (canvas.width !== frame.displayWidth) canvas.width = frame.displayWidth;
|
|
1320
|
+
if (canvas.height !== frame.displayHeight) canvas.height = frame.displayHeight;
|
|
1321
|
+
const c2d = canvas.getContext("2d");
|
|
1322
|
+
c2d.drawImage(frame, 0, 0, canvas.width, canvas.height);
|
|
1323
|
+
setScreenHasFrame(true);
|
|
1324
|
+
const now = performance.now();
|
|
1325
|
+
const ts = screenFpsTsRef.current;
|
|
1326
|
+
ts.push(now);
|
|
1327
|
+
while (ts.length > 0 && now - ts[0] > 1500) ts.shift();
|
|
1328
|
+
setScreenFps(Math.min(Math.round(ts.length / 1.5), 120));
|
|
1329
|
+
}
|
|
1330
|
+
} finally {
|
|
1331
|
+
try { frame.close(); } catch { /* already closed */ }
|
|
1332
|
+
}
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
/** Configure a fresh VideoDecoder from the host's avcC description. */
|
|
1336
|
+
const setupVideoDecoder = (init) => {
|
|
1337
|
+
closeVideoDecoder();
|
|
1338
|
+
if (typeof VideoDecoder === "undefined") {
|
|
1339
|
+
setVideoInfo((prev) => ({ ...(prev ?? {}), error: "此浏览器不支持 WebCodecs,已回退截图模式" }));
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
const raw = atob(init.avcC);
|
|
1344
|
+
const description = new Uint8Array(raw.length);
|
|
1345
|
+
for (let i = 0; i < raw.length; i++) description[i] = raw.charCodeAt(i);
|
|
1346
|
+
const decoder = new VideoDecoder({
|
|
1347
|
+
output: drawVideoFrame,
|
|
1348
|
+
error: (err) => setVideoInfo((prev) => ({ ...(prev ?? {}), error: String(err?.message ?? err) })),
|
|
1349
|
+
});
|
|
1350
|
+
decoder.configure({ codec: init.codec, description, optimizeForLatency: true, hardwareAcceleration: "prefer-hardware" });
|
|
1351
|
+
decoderRef.current = decoder;
|
|
1352
|
+
setVideoInfo((prev) => ({ ...(prev ?? {}), codec: init.codec, size: init.size ?? prev?.size, running: true, error: undefined }));
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
setVideoInfo((prev) => ({ ...(prev ?? {}), error: String(error?.message ?? error) }));
|
|
1355
|
+
closeVideoDecoder();
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
/** Feed one AVCC access unit from the host into the decoder. */
|
|
1360
|
+
const decodeVideoFrame = (header, bytes) => {
|
|
1361
|
+
const decoder = decoderRef.current;
|
|
1362
|
+
if (decoder === null || decoder.state !== "configured") return;
|
|
1363
|
+
try {
|
|
1364
|
+
decoder.decode(new EncodedVideoChunk({
|
|
1365
|
+
type: header.key === true ? "key" : "delta",
|
|
1366
|
+
timestamp: (Number(header.ts) || Date.now()) * 1000,
|
|
1367
|
+
data: bytes,
|
|
1368
|
+
}));
|
|
1369
|
+
} catch { /* dropped frame */ }
|
|
1370
|
+
};
|
|
1371
|
+
|
|
956
1372
|
return h("div", { className: "lc-panel" },
|
|
957
1373
|
h("div", { className: "lc-header" },
|
|
958
|
-
h("button", { type: "button", className: "lc-back", onClick: () => controller.close() },
|
|
959
|
-
h("span", { "aria-hidden": true }, "‹"),
|
|
960
|
-
h("span", null, "关闭"),
|
|
961
|
-
),
|
|
962
1374
|
h("h2", { className: "lc-title" }, "Logcat"),
|
|
1375
|
+
currentVersion !== "" ? h("span", { className: "lc-version" }, "v" + currentVersion) : null,
|
|
963
1376
|
h("span", { className: "lc-dot " + (live ? "on" : connected ? "warn" : "off") }),
|
|
964
|
-
h("span", { style: { fontSize: 12, color: "var(--lc-dim, #9e9e9e)" } },
|
|
965
|
-
live ? "
|
|
1377
|
+
h("span", { style: { fontSize: 12, color: "var(--lc-dim, #9e9e9e)", flex: "none" } },
|
|
1378
|
+
live ? "流中" : devices.length === 0 ? "无设备" : connected ? "未选设备" : "未连接"),
|
|
966
1379
|
h("select", {
|
|
967
1380
|
className: "lc-select",
|
|
968
1381
|
value: serial,
|
|
969
1382
|
onChange: (e) => pickSerial(e.target.value),
|
|
970
1383
|
title: "选择设备",
|
|
1384
|
+
style: { flex: 1, minWidth: 0 },
|
|
971
1385
|
},
|
|
972
1386
|
devices.length === 0
|
|
973
1387
|
? h("option", { value: "" }, "无设备 — 请连接并开启 USB 调试")
|
|
974
1388
|
: devices.map((d) =>
|
|
975
1389
|
h("option", { key: d.serial, value: d.serial },
|
|
976
1390
|
(d.model !== "" ? d.model + " · " : "") + d.serial + " [" + d.state + "]"))),
|
|
1391
|
+
embedded === true ? null : h("button", { type: "button", className: "lc-close", onClick: () => controller.close(), title: "关闭面板" }, h(Icon, { name: "close", size: 14 })),
|
|
977
1392
|
),
|
|
978
1393
|
h("div", { className: "lc-stats" },
|
|
979
1394
|
stats?.model
|
|
@@ -1052,11 +1467,11 @@ window.__ModuleLoader__.load({
|
|
|
1052
1467
|
style: { maxWidth: 180 },
|
|
1053
1468
|
}),
|
|
1054
1469
|
h("button", { type: "button", className: "lc-btn", onClick: () => setPackageFilter(pkgInput), title: "按包名过滤日志(agent 侧 logcat_recent 同步生效)" }, "包名"),
|
|
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)" }, "
|
|
1470
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: takeScreenshot, title: "截取真机屏幕并下载 PNG" }, h(Icon, { name: "camera" })),
|
|
1471
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: loadHistory, title: "从磁盘加载历史日志(重启不丢)" }, h(Icon, { name: "history" })),
|
|
1472
|
+
h("button", { type: "button", className: "lc-icobtn", "data-on": eventsOn ? "" : undefined, onClick: toggleEvents, title: "logcat -b events 事件缓冲(应用启动/崩溃/生命周期)" }, h(Icon, { name: "events" })),
|
|
1473
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: openCrashModal, title: "崩溃自动快照历史(截图+上下文)" }, h(Icon, { name: "crash" })),
|
|
1474
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: () => setWifiOpen(true), title: "WiFi 无线调试(adb pair + connect)" }, h(Icon, { name: "wifi" })),
|
|
1060
1475
|
h("input", {
|
|
1061
1476
|
ref: fileInputRef,
|
|
1062
1477
|
type: "file",
|
|
@@ -1064,25 +1479,25 @@ window.__ModuleLoader__.load({
|
|
|
1064
1479
|
style: { display: "none" },
|
|
1065
1480
|
onChange: (e) => { installApkFile(e.target.files?.[0] ?? null); e.target.value = ""; },
|
|
1066
1481
|
}),
|
|
1067
|
-
h("button", { type: "button", className: "lc-icobtn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, "
|
|
1482
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: () => fileInputRef.current?.click(), title: "选择本地 APK 安装到当前设备", disabled: installingApk ? true : undefined }, h(Icon, { name: "package" })),
|
|
1068
1483
|
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" }, "
|
|
1484
|
+
paused ? h(Icon, { name: "play" }) : h(Icon, { name: "pause" })),
|
|
1485
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: clearLog, title: "清空当前视图" }, h(Icon, { name: "trash" })),
|
|
1486
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: copyLog, title: "复制过滤后日志" }, h(Icon, { name: "copy" })),
|
|
1487
|
+
h("button", { type: "button", className: "lc-icobtn", onClick: exportLog, title: "导出过滤后日志 .txt" }, h(Icon, { name: "download" })),
|
|
1073
1488
|
h("button", {
|
|
1074
1489
|
type: "button",
|
|
1075
1490
|
className: "lc-icobtn",
|
|
1076
1491
|
"data-on": autoScroll ? "" : undefined,
|
|
1077
1492
|
title: "新日志自动滚到底部",
|
|
1078
1493
|
onClick: () => setAutoScroll(!autoScroll),
|
|
1079
|
-
}, "
|
|
1494
|
+
}, h(Icon, { name: "pin" })),
|
|
1080
1495
|
)
|
|
1081
1496
|
: null,
|
|
1082
1497
|
tab === "log" || tab === "screen"
|
|
1083
1498
|
? h("div", { className: "lc-keys" },
|
|
1084
|
-
[["home", "
|
|
1085
|
-
h("button", { key, type: "button", className: "lc-key", title: "发送按键 " + key, onClick: () => sendKey(key) }, label)))
|
|
1499
|
+
[["home", "home", "Home"], ["back", "back", "返回"], ["recents", "grid", "最近"], ["wakeup", "sun", "唤醒"], ["power", "power", "电源"], ["volume_up", "volumeUp", ""], ["volume_down", "volumeDown", ""]].map(([key, icon, label]) =>
|
|
1500
|
+
h("button", { key, type: "button", className: "lc-key", title: "发送按键 " + key, onClick: () => sendKey(key) }, h(Icon, { name: icon, size: 14 }), label)))
|
|
1086
1501
|
: null,
|
|
1087
1502
|
lastCrash !== null
|
|
1088
1503
|
? h("div", { className: "lc-crashbar" },
|
|
@@ -1101,7 +1516,7 @@ window.__ModuleLoader__.load({
|
|
|
1101
1516
|
: null,
|
|
1102
1517
|
paused && tab === "log" && !eventsOn
|
|
1103
1518
|
? h("div", { className: "lc-pausedbar" },
|
|
1104
|
-
h("span", { style: { flex: 1 } }, "
|
|
1519
|
+
h("span", { style: { flex: 1 } }, h(Icon, { name: "pause", size: 14 }), "已暂停 — 新日志缓冲中(" + pendingCount + " 条),列表已冻结"),
|
|
1105
1520
|
h("button", { type: "button", className: "lc-btn", onClick: togglePause, title: "恢复实时,回放缓冲日志" }, "继续"),
|
|
1106
1521
|
)
|
|
1107
1522
|
: null,
|
|
@@ -1155,18 +1570,61 @@ window.__ModuleLoader__.load({
|
|
|
1155
1570
|
: tab === "screen"
|
|
1156
1571
|
? h("div", { className: "lc-screen" },
|
|
1157
1572
|
h("div", { className: "lc-screen-bar" },
|
|
1158
|
-
h("span", { className: "lc-dot " + (screenOn &&
|
|
1159
|
-
h("span",
|
|
1160
|
-
|
|
1573
|
+
h("span", { className: "lc-dot " + (screenOn ? (videoInfo?.error === undefined && videoInfo?.idle !== true ? "on" : "warn") : "off") }),
|
|
1574
|
+
h("span", { title: videoInfo?.error ?? undefined },
|
|
1575
|
+
hostVideoCapable === false ? "宿主版本过旧"
|
|
1576
|
+
: screenOn
|
|
1577
|
+
? (screenMode === "video"
|
|
1578
|
+
? "流畅 " + screenFps + "fps · " + (videoInfo?.size ?? videoTier)
|
|
1579
|
+
: "高清 " + screenFps + "fps · " + (videoInfo?.size ?? "PNG"))
|
|
1580
|
+
: "投屏未开启"),
|
|
1581
|
+
h("select", {
|
|
1582
|
+
className: "lc-select",
|
|
1583
|
+
value: screenMode,
|
|
1584
|
+
onChange: (e) => { const m = e.target.value; setScreenMode(m); try { localStorage.setItem("dsh-logcat-screenmode-v2", m); } catch { /* private */ } },
|
|
1585
|
+
title: "流畅=H.264 视频流(30fps+);高清=PNG 截图(静帧清晰)",
|
|
1586
|
+
style: { padding: "2px 6px" },
|
|
1587
|
+
},
|
|
1588
|
+
h("option", { value: "video" }, "流畅"),
|
|
1589
|
+
h("option", { value: "png" }, "高清")),
|
|
1590
|
+
h("select", {
|
|
1591
|
+
className: "lc-select",
|
|
1592
|
+
value: videoTier,
|
|
1593
|
+
onChange: (e) => { const t = e.target.value; setVideoTier(t); try { localStorage.setItem("dsh-logcat-videotier", t); } catch { /* private */ } },
|
|
1594
|
+
title: "投屏分辨率档位(越低越省带宽/CPU)",
|
|
1595
|
+
style: { padding: "2px 6px" },
|
|
1596
|
+
},
|
|
1597
|
+
Object.keys(VIDEO_TIERS).map((t) => h("option", { key: t, value: t }, t))),
|
|
1598
|
+
h("input", { placeholder: "输入文字后回车发送…", value: screenText, onChange: (e) => setScreenText(e.target.value), onKeyDown: (e) => { if (e.key === "Enter") sendScreenText(); }, style: { flex: 1, minWidth: 120 } }),
|
|
1599
|
+
h("button", { type: "button", className: "lc-btn", onClick: () => sendKey("wakeup"), title: "唤醒设备屏幕(等效按电源键点亮)" }, "唤醒"),
|
|
1161
1600
|
h("button", { type: "button", className: "lc-btn", onClick: downloadScreen, title: "下载当前帧 PNG" }, "下载"),
|
|
1162
|
-
h("button", { type: "button", className: "lc-btn", onClick: toggleScreenFullscreen, title: "全屏查看(Esc 退出)" }, "
|
|
1163
|
-
h("span", { className: "lc-screen-hint" }, "点击=点按 · 拖动=滑动"),
|
|
1601
|
+
h("button", { type: "button", className: "lc-btn", onClick: toggleScreenFullscreen, title: "全屏查看(Esc 退出)" }, h(Icon, { name: "expand", size: 14 })),
|
|
1164
1602
|
),
|
|
1165
|
-
|
|
1166
|
-
? h("div", { className: "lc-empty" },
|
|
1167
|
-
|
|
1168
|
-
h("
|
|
1169
|
-
|
|
1603
|
+
hostVideoCapable === false
|
|
1604
|
+
? h("div", { className: "lc-empty" },
|
|
1605
|
+
h("span", { className: "lc-empty-icon" }, h(Icon, { name: "crash", size: 26 })),
|
|
1606
|
+
h("span", null, "运行中的 GUI 宿主版本过旧 —— 重启 GUI(dsh web)后刷新,投屏才会生效"))
|
|
1607
|
+
: h("div", {
|
|
1608
|
+
className: "lc-screen-img",
|
|
1609
|
+
ref: screenWrapRef,
|
|
1610
|
+
onMouseDown: screenMouseDown,
|
|
1611
|
+
onMouseUp: screenMouseUp,
|
|
1612
|
+
onMouseLeave: screenMouseUp,
|
|
1613
|
+
onWheel: screenWheel,
|
|
1614
|
+
onContextMenu: screenContextMenu,
|
|
1615
|
+
},
|
|
1616
|
+
h("canvas", { ref: screenCanvasRef, width: 540, height: 1200 }),
|
|
1617
|
+
screenOn
|
|
1618
|
+
? h("div", { className: "lc-screen-badge" + (videoInfo?.error !== undefined && videoInfo?.error !== null ? " warn" : videoInfo?.idle === true ? " warn" : "") },
|
|
1619
|
+
(videoInfo?.idle === true ? "设备休眠 · 点「唤醒」" : (screenMode === "video" ? "H.264" : "PNG") + " · " + screenFps + "fps · " + (videoInfo?.size ?? videoTier))
|
|
1620
|
+
+ (videoInfo?.error ? " · " + videoInfo.error : ""))
|
|
1621
|
+
: null,
|
|
1622
|
+
ripples.map((r) => h("span", { key: r.id, className: "lc-tap-ripple", style: { left: r.x, top: r.y } })),
|
|
1623
|
+
!screenHasFrame
|
|
1624
|
+
? h("div", { className: "lc-empty" },
|
|
1625
|
+
h("span", { className: "lc-empty-icon" }, h(Icon, { name: "plug", size: 26 })),
|
|
1626
|
+
h("span", null, devices.length === 0 ? "连接设备并开启 USB 调试后自动开始投屏" : "正在建立投屏流…"))
|
|
1627
|
+
: null,
|
|
1170
1628
|
),
|
|
1171
1629
|
)
|
|
1172
1630
|
: eventsOn
|
|
@@ -1174,10 +1632,12 @@ window.__ModuleLoader__.load({
|
|
|
1174
1632
|
? h("div", { className: "lc-empty" }, "events 事件缓冲(已开启流,等待事件…)")
|
|
1175
1633
|
: h(VirtualLog, { entries: filteredEvents, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine, rowDensity }))
|
|
1176
1634
|
: (filtered.length === 0
|
|
1177
|
-
? h("div", { className: "lc-empty" },
|
|
1635
|
+
? h("div", { className: "lc-empty" },
|
|
1636
|
+
h("span", { className: "lc-empty-icon" }, h(Icon, { name: paused ? "pause" : devices.length === 0 ? "plug" : "file", size: 26 })),
|
|
1637
|
+
h("span", null, paused ? "已暂停(" + entries.length + " 条已缓冲)" : (devices.length === 0 ? "连接设备并开启 USB 调试后自动开始…" : "暂无日志")))
|
|
1178
1638
|
: h(VirtualLog, { entries: filtered, scrollTop, onScrollTop: setScrollTop, bodyRef, keyword, onTagClick: addKeyword, onLineDblClick: copyLine, rowDensity })),
|
|
1179
1639
|
tab === "log" && !eventsOn && live && !paused
|
|
1180
|
-
? h("button", { type: "button", className: "lc-fab", onClick: togglePause, title: "暂停:冻结当前视图(新日志进入缓冲)" }, "
|
|
1640
|
+
? h("button", { type: "button", className: "lc-fab", onClick: togglePause, title: "暂停:冻结当前视图(新日志进入缓冲)" }, h(Icon, { name: "pause", size: 14 }), "暂停")
|
|
1181
1641
|
: null,
|
|
1182
1642
|
),
|
|
1183
1643
|
crashModal
|
|
@@ -1193,7 +1653,7 @@ window.__ModuleLoader__.load({
|
|
|
1193
1653
|
? h("span", null, "暂无崩溃快照")
|
|
1194
1654
|
: crashList.map((c) =>
|
|
1195
1655
|
h("div", { key: c.id, className: "lc-crash-item", onClick: () => toggleCrashDetail(c.id) },
|
|
1196
|
-
h("div", { className: "t" }, c.ts + " [" + c.kind + "]"
|
|
1656
|
+
h("div", { className: "t" }, c.ts + " [" + c.kind + "]", c.hasScreenshot ? h(Icon, { name: "camera", size: 12 }) : null),
|
|
1197
1657
|
h("div", { className: "s" }, c.summary),
|
|
1198
1658
|
crashOpenId === c.id && crashDetail !== null
|
|
1199
1659
|
? h("div", { className: "lc-crash-detail" },
|
|
@@ -1570,6 +2030,91 @@ window.__ModuleLoader__.load({
|
|
|
1570
2030
|
//#endregion
|
|
1571
2031
|
|
|
1572
2032
|
//#region entry
|
|
2033
|
+
//#region icons
|
|
2034
|
+
/**
|
|
2035
|
+
* Minimal geometric glyphs on one 24px grid, stroked with currentColor —
|
|
2036
|
+
* deliberately dependency-free (the client bundle may only require shell
|
|
2037
|
+
* seeds), so the panel ships its own small, consistent icon set instead of
|
|
2038
|
+
* emoji or an external icon package.
|
|
2039
|
+
*/
|
|
2040
|
+
const ICONS = {
|
|
2041
|
+
camera: '<path d="M4 8.8h3.2L8.7 6.4h6.6l1.5 2.4H20v9.2H4z"/><circle cx="12" cy="13" r="3.1"/>',
|
|
2042
|
+
history: '<path d="M4.4 12a7.6 7.6 0 1 1 2.2 5.4"/><path d="M4 4.6v3.2h3.2"/><path d="M12 8.4V12l2.6 1.8"/>',
|
|
2043
|
+
events: '<path d="M4 6.5h16M4 12h16M4 17.5h10"/>',
|
|
2044
|
+
crash: '<path d="M12 4 20.5 19H3.5z"/><path d="M12 9.5v4.2"/><path d="M12 16.6h.01"/>',
|
|
2045
|
+
wifi: '<path d="M4 9.2a12 12 0 0 1 16 0"/><path d="M7 12.6a8 8 0 0 1 10 0"/><path d="M10 16a4 4 0 0 1 4 0"/><path d="M12 19h.01"/>',
|
|
2046
|
+
package: '<path d="M12 3.4 20 7.8v8.4L12 20.6 4 16.2V7.8z"/><path d="M4 7.8 12 12l8-4.2"/><path d="M12 12v8.6"/>',
|
|
2047
|
+
pause: '<path d="M9.2 6v12M14.8 6v12"/>',
|
|
2048
|
+
play: '<path d="M8.4 5.4 18.6 12 8.4 18.6z"/>',
|
|
2049
|
+
trash: '<path d="M5 7h14"/><path d="M10 7V5h4v2"/><path d="M6.6 7l1 13h8.8l1-13"/><path d="M10.5 10.5v6M13.5 10.5v6"/>',
|
|
2050
|
+
copy: '<rect x="9" y="9" width="11" height="11" rx="2"/><path d="M15 5.5H5.5V15"/>',
|
|
2051
|
+
download: '<path d="M12 4v11"/><path d="M7.6 10.8 12 15.2l4.4-4.4"/><path d="M5 19h14"/>',
|
|
2052
|
+
pin: '<path d="M12 3.6c2.9 0 4.8 2 4.8 4.4 0 2.9-2.4 4.8-3.4 7.6h-2.8C9.6 12.8 7.2 10.9 7.2 8 7.2 5.6 9.1 3.6 12 3.6z"/><path d="M12 15.6V20.4"/>',
|
|
2053
|
+
expand: '<path d="M9.5 4.5H4.5v5"/><path d="M14.5 4.5h5v5"/><path d="M14.5 19.5h5v-5"/><path d="M9.5 19.5h-5v-5"/>',
|
|
2054
|
+
home: '<path d="M4.5 11 12 4.6 19.5 11v8.4h-15z"/><path d="M10 19.4v-4.6h4v4.6"/>',
|
|
2055
|
+
back: '<path d="M14.6 5.6 8.2 12l6.4 6.4"/>',
|
|
2056
|
+
grid: '<rect x="4.5" y="4.5" width="6" height="6" rx="1.4"/><rect x="13.5" y="4.5" width="6" height="6" rx="1.4"/><rect x="4.5" y="13.5" width="6" height="6" rx="1.4"/><rect x="13.5" y="13.5" width="6" height="6" rx="1.4"/>',
|
|
2057
|
+
sun: '<circle cx="12" cy="12" r="4"/><path d="M12 3.4v2.2M12 18.4v2.2M4.6 12H6.8M17.2 12h2.2M6.8 6.8l1.6 1.6M15.6 15.6l1.6 1.6M17.2 6.8l-1.6 1.6M8.4 15.6l-1.6 1.6"/>',
|
|
2058
|
+
power: '<path d="M12 4v8"/><path d="M7.6 7.2a7 7 0 1 0 8.8 0"/>',
|
|
2059
|
+
volumeUp: '<path d="M4.5 9.8h3l4-3v10.4l-4-3h-3z"/><path d="M15 9.6a3.6 3.6 0 0 1 0 4.8"/><path d="M17.6 7.6a6.8 6.8 0 0 1 0 8.8"/>',
|
|
2060
|
+
volumeDown: '<path d="M4.5 9.8h3l4-3v10.4l-4-3h-3z"/><path d="M15 10.4a3 3 0 0 1 0 3.2"/>',
|
|
2061
|
+
plug: '<path d="M9.5 3.6v4.8M14.5 3.6v4.8"/><path d="M6.4 8.4h11.2v2.6a5.6 5.6 0 0 1-11.2 0z"/><path d="M12 16.6v3.8"/>',
|
|
2062
|
+
file: '<path d="M6.6 3.6h7.2l4 4v12.8H6.6z"/><path d="M13.8 3.6v4h4"/><path d="M9.4 12.4h5.2M9.4 15.6h5.2"/>',
|
|
2063
|
+
close: '<path d="M6.4 6.4 17.6 17.6M17.6 6.4 6.4 17.6"/>',
|
|
2064
|
+
search: '<circle cx="11" cy="11" r="5.6"/><path d="M15.2 15.2 19.4 19.4"/>',
|
|
2065
|
+
};
|
|
2066
|
+
|
|
2067
|
+
/** One glyph from the set above, at the requested square edge. */
|
|
2068
|
+
function Icon({ name, size = 16 }) {
|
|
2069
|
+
const body = ICONS[name];
|
|
2070
|
+
if (body === undefined) return null;
|
|
2071
|
+
return h("svg", {
|
|
2072
|
+
width: size,
|
|
2073
|
+
height: size,
|
|
2074
|
+
viewBox: "0 0 24 24",
|
|
2075
|
+
fill: "none",
|
|
2076
|
+
stroke: "currentColor",
|
|
2077
|
+
strokeWidth: 1.7,
|
|
2078
|
+
strokeLinecap: "round",
|
|
2079
|
+
strokeLinejoin: "round",
|
|
2080
|
+
"aria-hidden": true,
|
|
2081
|
+
dangerouslySetInnerHTML: { __html: body },
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
//#endregion
|
|
2085
|
+
|
|
2086
|
+
//#region dsh 0.2 native integration
|
|
2087
|
+
/** The glyph the left sidebar shows for this panel. */
|
|
2088
|
+
function SidebarLogcatIcon({ size = 16 }) {
|
|
2089
|
+
return h(Icon, { name: "file", size: size ?? 16 });
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* Register the panel as a first-class 0.2 surface: the central `main` panel
|
|
2094
|
+
* (selected by the sidebar entry id) plus its `sidebar.panellist` row.
|
|
2095
|
+
* @returns whether the host accepted the native seats synchronously — when
|
|
2096
|
+
* it did, the legacy drawer is not mounted at all.
|
|
2097
|
+
*/
|
|
2098
|
+
function registerNative(ctx, makePanel) {
|
|
2099
|
+
if (ctx?.slots === undefined || typeof ctx.slots.inject !== "function") return false;
|
|
2100
|
+
let active = false;
|
|
2101
|
+
try {
|
|
2102
|
+
ctx.slots.inject("main", () => {
|
|
2103
|
+
active = true;
|
|
2104
|
+
return ctx.slots.register({ name: "main", key: "logcat" }, makePanel);
|
|
2105
|
+
});
|
|
2106
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register(
|
|
2107
|
+
{ name: "sidebar.panellist", id: "logcat", order: 100, label: "Logcat" },
|
|
2108
|
+
SidebarLogcatIcon,
|
|
2109
|
+
));
|
|
2110
|
+
} catch (error) {
|
|
2111
|
+
console.warn("[dsh-logcat] native slot registration unavailable:", error);
|
|
2112
|
+
return false;
|
|
2113
|
+
}
|
|
2114
|
+
return active;
|
|
2115
|
+
}
|
|
2116
|
+
//#endregion
|
|
2117
|
+
|
|
1573
2118
|
/** Required services (fiber inject waiting — the runtime must be up first). */
|
|
1574
2119
|
const inject = ["slots"];
|
|
1575
2120
|
|
|
@@ -1590,16 +2135,23 @@ window.__ModuleLoader__.load({
|
|
|
1590
2135
|
styleGuard.observe(document.head, { childList: true });
|
|
1591
2136
|
|
|
1592
2137
|
const controller = new PanelController();
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
2138
|
+
const legacy = [];
|
|
2139
|
+
const mountLegacy = () => {
|
|
2140
|
+
try {
|
|
2141
|
+
legacy.push(mountSidebarEntry(controller));
|
|
2142
|
+
legacy.push(mountPanel(controller));
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
console.warn("[dsh-logcat] legacy mount failed:", error);
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
// dsh 0.2 exposes `main` + `sidebar.panellist`: the panel becomes a native
|
|
2148
|
+
// central surface with its own sidebar row. Clients that do not offer those
|
|
2149
|
+
// seats keep the self-healing sidebar entry and the right-hand drawer.
|
|
2150
|
+
const native = registerNative(ctx, () => h(LogcatPanel, { controller, embedded: true }));
|
|
2151
|
+
if (!native) mountLegacy();
|
|
1600
2152
|
ctx.effect(() => () => {
|
|
1601
2153
|
styleGuard.disconnect();
|
|
1602
|
-
for (const dispose of
|
|
2154
|
+
for (const dispose of legacy.splice(0)) dispose();
|
|
1603
2155
|
style.remove();
|
|
1604
2156
|
}, "dsh-logcat: ui mounts");
|
|
1605
2157
|
}
|