mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 19:42:15 +02:00
feat(demo): support multi-select in logcat and packet log
related to #425
This commit is contained in:
parent
055da71f6c
commit
52abdd146b
8 changed files with 1075 additions and 835 deletions
|
@ -33,7 +33,7 @@
|
||||||
"@yume-chan/struct": "workspace:^0.0.18",
|
"@yume-chan/struct": "workspace:^0.0.18",
|
||||||
"mobx": "^6.7.0",
|
"mobx": "^6.7.0",
|
||||||
"mobx-react-lite": "^3.4.0",
|
"mobx-react-lite": "^3.4.0",
|
||||||
"next": "13.1.1",
|
"next": "13.1.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"webm-muxer": "^1.1.0",
|
"webm-muxer": "^1.1.0",
|
||||||
|
@ -46,9 +46,9 @@
|
||||||
"@mdx-js/loader": "^2.2.1",
|
"@mdx-js/loader": "^2.2.1",
|
||||||
"@mdx-js/react": "^2.2.1",
|
"@mdx-js/react": "^2.2.1",
|
||||||
"@next/mdx": "^13.1.1",
|
"@next/mdx": "^13.1.1",
|
||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.27",
|
||||||
"eslint": "^8.31.0",
|
"eslint": "^8.31.0",
|
||||||
"eslint-config-next": "13.1.1",
|
"eslint-config-next": "13.1.5",
|
||||||
"source-map-loader": "^4.0.1",
|
"source-map-loader": "^4.0.1",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,22 @@ import { withDisplayName } from "../utils";
|
||||||
|
|
||||||
const useClasses = makeStyles({
|
const useClasses = makeStyles({
|
||||||
root: {
|
root: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
overflowY: 'auto',
|
overflowY: "auto",
|
||||||
},
|
},
|
||||||
flex: {
|
flex: {
|
||||||
|
display: "flex",
|
||||||
display: 'flex',
|
|
||||||
},
|
},
|
||||||
cell: {
|
cell: {
|
||||||
fontFamily: '"Cascadia Code", Consolas, monospace',
|
fontFamily: '"Cascadia Code", Consolas, monospace',
|
||||||
},
|
},
|
||||||
lineNumber: {
|
lineNumber: {
|
||||||
textAlign: 'right',
|
textAlign: "right",
|
||||||
},
|
},
|
||||||
hex: {
|
hex: {
|
||||||
marginLeft: '40px',
|
marginLeft: "40px",
|
||||||
fontVariantLigatures: 'none',
|
fontVariantLigatures: "none",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -32,20 +31,19 @@ const PRINTABLE_CHARACTERS: [number, number][] = [
|
||||||
|
|
||||||
export function isPrintableCharacter(code: number) {
|
export function isPrintableCharacter(code: number) {
|
||||||
return PRINTABLE_CHARACTERS.some(
|
return PRINTABLE_CHARACTERS.some(
|
||||||
([start, end]) =>
|
([start, end]) => code >= start && code <= end
|
||||||
code >= start &&
|
|
||||||
code <= end
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toCharacter(code: number) {
|
export function toCharacter(code: number) {
|
||||||
if (isPrintableCharacter(code))
|
if (isPrintableCharacter(code)) {
|
||||||
return String.fromCharCode(code);
|
return String.fromCharCode(code);
|
||||||
return '.';
|
}
|
||||||
|
return ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toText(data: Uint8Array) {
|
export function toText(data: Uint8Array) {
|
||||||
let result = '';
|
let result = "";
|
||||||
for (const code of data) {
|
for (const code of data) {
|
||||||
result += toCharacter(code);
|
result += toCharacter(code);
|
||||||
}
|
}
|
||||||
|
@ -59,63 +57,57 @@ export interface HexViewerProps {
|
||||||
data: Uint8Array;
|
data: Uint8Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HexViewer = withDisplayName('HexViewer')(({
|
export const HexViewer = withDisplayName("HexViewer")(
|
||||||
className,
|
({ className, data }: HexViewerProps) => {
|
||||||
data
|
const classes = useClasses();
|
||||||
}: HexViewerProps) => {
|
|
||||||
const classes = useClasses();
|
|
||||||
|
|
||||||
// Because ADB packets are usually small,
|
// Because ADB packets are usually small,
|
||||||
// so don't add virtualization now.
|
// so don't add virtualization now.
|
||||||
|
|
||||||
const children = useMemo(() => {
|
const children = useMemo(() => {
|
||||||
const lineNumbers: ReactNode[] = [];
|
const lineNumbers: ReactNode[] = [];
|
||||||
const hexRows: ReactNode[] = [];
|
const hexRows: ReactNode[] = [];
|
||||||
const textRows: ReactNode[] = [];
|
const textRows: ReactNode[] = [];
|
||||||
for (let i = 0; i < data.length; i += PER_ROW) {
|
for (let i = 0; i < data.length; i += PER_ROW) {
|
||||||
lineNumbers.push(
|
lineNumbers.push(<div key={i}>{i.toString(16)}</div>);
|
||||||
<div>
|
|
||||||
{i.toString(16)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
let hex = '';
|
let hex = "";
|
||||||
for (let j = i; j < i + PER_ROW && j < data.length; j++) {
|
for (let j = i; j < i + PER_ROW && j < data.length; j++) {
|
||||||
hex += data[j].toString(16).padStart(2, '0') + ' ';
|
hex += data[j].toString(16).padStart(2, "0") + " ";
|
||||||
|
}
|
||||||
|
hexRows.push(<div key={i}>{hex}</div>);
|
||||||
|
|
||||||
|
textRows.push(
|
||||||
|
<div key={i}>{toText(data.slice(i, i + PER_ROW))}</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
hexRows.push(
|
|
||||||
<div>
|
|
||||||
{hex}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
textRows.push(
|
return {
|
||||||
<div>
|
lineNumbers,
|
||||||
{toText(data.slice(i, i + PER_ROW))}
|
hexRows,
|
||||||
</div>
|
textRows,
|
||||||
);
|
};
|
||||||
}
|
}, [data]);
|
||||||
|
|
||||||
return {
|
return (
|
||||||
lineNumbers,
|
<div className={mergeClasses(classes.root, className)}>
|
||||||
hexRows,
|
<div className={classes.flex}>
|
||||||
textRows,
|
<div
|
||||||
};
|
className={mergeClasses(
|
||||||
}, [data]);
|
classes.cell,
|
||||||
|
classes.lineNumber
|
||||||
return (
|
)}
|
||||||
<div className={mergeClasses(classes.root, className)}>
|
>
|
||||||
<div className={classes.flex}>
|
{children.lineNumbers}
|
||||||
<div className={mergeClasses(classes.cell, classes.lineNumber)}>
|
</div>
|
||||||
{children.lineNumbers}
|
<div className={mergeClasses(classes.cell, classes.hex)}>
|
||||||
</div>
|
{children.hexRows}
|
||||||
<div className={mergeClasses(classes.cell, classes.hex)}>
|
</div>
|
||||||
{children.hexRows}
|
<div className={mergeClasses(classes.cell, classes.hex)}>
|
||||||
</div>
|
{children.textRows}
|
||||||
<div className={mergeClasses(classes.cell, classes.hex)}>
|
</div>
|
||||||
{children.textRows}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
});
|
);
|
||||||
|
|
|
@ -1,9 +1,4 @@
|
||||||
import {
|
import { ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
|
||||||
Checkbox,
|
|
||||||
ICommandBarItemProps,
|
|
||||||
Stack,
|
|
||||||
StackItem,
|
|
||||||
} from "@fluentui/react";
|
|
||||||
import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
|
import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
|
||||||
import {
|
import {
|
||||||
AndroidLogEntry,
|
AndroidLogEntry,
|
||||||
|
@ -12,7 +7,6 @@ import {
|
||||||
LogcatFormat,
|
LogcatFormat,
|
||||||
formatAndroidLogEntry,
|
formatAndroidLogEntry,
|
||||||
} from "@yume-chan/android-bin";
|
} from "@yume-chan/android-bin";
|
||||||
import { BTree } from "@yume-chan/b-tree";
|
|
||||||
import {
|
import {
|
||||||
AbortController,
|
AbortController,
|
||||||
ReadableStream,
|
ReadableStream,
|
||||||
|
@ -28,7 +22,7 @@ import {
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { FormEvent, useEffect, useState } from "react";
|
import { PointerEvent } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CommandBar,
|
CommandBar,
|
||||||
|
@ -39,6 +33,7 @@ import {
|
||||||
} from "../components";
|
} from "../components";
|
||||||
import { GLOBAL_STATE } from "../state";
|
import { GLOBAL_STATE } from "../state";
|
||||||
import { Icons, RouteStackProps, useStableCallback } from "../utils";
|
import { Icons, RouteStackProps, useStableCallback } from "../utils";
|
||||||
|
import { ObservableListSelection } from "./packet-log";
|
||||||
|
|
||||||
const LINE_HEIGHT = 32;
|
const LINE_HEIGHT = 32;
|
||||||
|
|
||||||
|
@ -85,11 +80,10 @@ const state = makeAutoObservable(
|
||||||
buffer: [] as LogRow[],
|
buffer: [] as LogRow[],
|
||||||
flushRequested: false,
|
flushRequested: false,
|
||||||
list: [] as LogRow[],
|
list: [] as LogRow[],
|
||||||
selection: new BTree(6),
|
selection: new ObservableListSelection(),
|
||||||
count: 0,
|
count: 0,
|
||||||
stream: undefined as ReadableStream<AndroidLogEntry> | undefined,
|
stream: undefined as ReadableStream<AndroidLogEntry> | undefined,
|
||||||
stopSignal: undefined as AbortController | undefined,
|
stopSignal: undefined as AbortController | undefined,
|
||||||
selectedCount: 0,
|
|
||||||
animationFrameId: undefined as number | undefined,
|
animationFrameId: undefined as number | undefined,
|
||||||
start() {
|
start() {
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
|
@ -131,7 +125,6 @@ const state = makeAutoObservable(
|
||||||
clear() {
|
clear() {
|
||||||
this.list = [];
|
this.list = [];
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
this.selectedCount = 0;
|
|
||||||
},
|
},
|
||||||
get empty() {
|
get empty() {
|
||||||
return this.list.length === 0;
|
return this.list.length === 0;
|
||||||
|
@ -162,7 +155,7 @@ const state = makeAutoObservable(
|
||||||
{
|
{
|
||||||
key: "copyAll",
|
key: "copyAll",
|
||||||
text: "Copy Rows",
|
text: "Copy Rows",
|
||||||
disabled: this.selectedCount === 0,
|
disabled: this.selection.size === 0,
|
||||||
iconProps: { iconName: Icons.Copy },
|
iconProps: { iconName: Icons.Copy },
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
let text = "";
|
let text = "";
|
||||||
|
@ -181,7 +174,7 @@ const state = makeAutoObservable(
|
||||||
{
|
{
|
||||||
key: "copyText",
|
key: "copyText",
|
||||||
text: "Copy Messages",
|
text: "Copy Messages",
|
||||||
disabled: this.selectedCount === 0,
|
disabled: this.selection.size === 0,
|
||||||
iconProps: { iconName: Icons.Copy },
|
iconProps: { iconName: Icons.Copy },
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
let text = "";
|
let text = "";
|
||||||
|
@ -197,54 +190,6 @@ const state = makeAutoObservable(
|
||||||
},
|
},
|
||||||
get columns(): Column[] {
|
get columns(): Column[] {
|
||||||
return [
|
return [
|
||||||
{
|
|
||||||
width: 40,
|
|
||||||
title: "",
|
|
||||||
CellComponent: ({
|
|
||||||
rowIndex,
|
|
||||||
columnIndex,
|
|
||||||
className,
|
|
||||||
...rest
|
|
||||||
}) => {
|
|
||||||
const [checked, setChecked] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
setChecked(this.selection.has(rowIndex));
|
|
||||||
}, [rowIndex]);
|
|
||||||
|
|
||||||
const handleChange = useStableCallback(
|
|
||||||
(e?: FormEvent<EventTarget>, checked?: boolean) => {
|
|
||||||
if (checked === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (checked) {
|
|
||||||
this.selection.add(rowIndex);
|
|
||||||
setChecked(true);
|
|
||||||
} else {
|
|
||||||
this.selection.delete(rowIndex);
|
|
||||||
setChecked(false);
|
|
||||||
}
|
|
||||||
runInAction(() => {
|
|
||||||
// Trigger mobx
|
|
||||||
this.selectedCount = this.selection.size;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
className={className}
|
|
||||||
verticalAlign="center"
|
|
||||||
horizontalAlign="center"
|
|
||||||
{...rest}
|
|
||||||
>
|
|
||||||
<Checkbox
|
|
||||||
checked={checked}
|
|
||||||
onChange={handleChange}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
width: 200,
|
width: 200,
|
||||||
title: "Time",
|
title: "Time",
|
||||||
|
@ -445,22 +390,35 @@ const Header = observer(function Header({
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const Row = function Row({ className, rowIndex, ...rest }: GridRowProps) {
|
const Row = observer(function Row({
|
||||||
const item = state.list[rowIndex];
|
className,
|
||||||
|
rowIndex,
|
||||||
|
...rest
|
||||||
|
}: GridRowProps) {
|
||||||
const classes = useClasses();
|
const classes = useClasses();
|
||||||
|
|
||||||
const handleClick = useStableCallback(() => {
|
const handlePointerDown = useStableCallback(
|
||||||
runInAction(() => {});
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
});
|
runInAction(() => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
state.selection.select(rowIndex, e.ctrlKey, e.shiftKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={mergeClasses(className, classes.row)}
|
className={mergeClasses(
|
||||||
onClick={handleClick}
|
className,
|
||||||
|
classes.row,
|
||||||
|
state.selection.has(rowIndex) && classes.selected
|
||||||
|
)}
|
||||||
|
onPointerDown={handlePointerDown}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
const LogcatPage: NextPage = () => {
|
const LogcatPage: NextPage = () => {
|
||||||
const classes = useClasses();
|
const classes = useClasses();
|
||||||
|
|
|
@ -1,10 +1,22 @@
|
||||||
import { ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
|
import { ICommandBarItemProps, Stack, StackItem } from "@fluentui/react";
|
||||||
import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
|
import { makeStyles, mergeClasses, shorthands } from "@griffel/react";
|
||||||
import { AdbCommand, decodeUtf8 } from "@yume-chan/adb";
|
import { AdbCommand, decodeUtf8 } from "@yume-chan/adb";
|
||||||
import { autorun, makeAutoObservable, observable, runInAction } from "mobx";
|
import { BTree, BTreeNode } from "@yume-chan/b-tree";
|
||||||
|
import {
|
||||||
|
IAtom,
|
||||||
|
IObservableValue,
|
||||||
|
action,
|
||||||
|
autorun,
|
||||||
|
createAtom,
|
||||||
|
makeAutoObservable,
|
||||||
|
observable,
|
||||||
|
onBecomeUnobserved,
|
||||||
|
runInAction,
|
||||||
|
} from "mobx";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import { PointerEvent } from "react";
|
||||||
import {
|
import {
|
||||||
CommandBar,
|
CommandBar,
|
||||||
Grid,
|
Grid,
|
||||||
|
@ -15,7 +27,7 @@ import {
|
||||||
HexViewer,
|
HexViewer,
|
||||||
toText,
|
toText,
|
||||||
} from "../components";
|
} from "../components";
|
||||||
import { GLOBAL_STATE, PacketLogItem } from "../state";
|
import { GLOBAL_STATE } from "../state";
|
||||||
import {
|
import {
|
||||||
Icons,
|
Icons,
|
||||||
RouteStackProps,
|
RouteStackProps,
|
||||||
|
@ -23,6 +35,139 @@ import {
|
||||||
withDisplayName,
|
withDisplayName,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
|
export class ObservableBTree implements BTree {
|
||||||
|
data: BTree;
|
||||||
|
hasMap: Map<number, IObservableValue<boolean>>;
|
||||||
|
keys: IAtom;
|
||||||
|
|
||||||
|
constructor(order: number) {
|
||||||
|
this.data = new BTree(order);
|
||||||
|
this.hasMap = new Map();
|
||||||
|
this.keys = createAtom("ObservableBTree.keys");
|
||||||
|
}
|
||||||
|
|
||||||
|
get order(): number {
|
||||||
|
return this.data.order;
|
||||||
|
}
|
||||||
|
|
||||||
|
get root(): BTreeNode {
|
||||||
|
return this.data.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size(): number {
|
||||||
|
this.keys.reportObserved();
|
||||||
|
return this.data.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(value: number): boolean {
|
||||||
|
if (!this.hasMap.has(value)) {
|
||||||
|
const observableHasValue = observable.box(this.data.has(value));
|
||||||
|
onBecomeUnobserved(observableHasValue, () =>
|
||||||
|
this.hasMap.delete(value)
|
||||||
|
);
|
||||||
|
this.hasMap.set(value, observableHasValue);
|
||||||
|
}
|
||||||
|
return this.hasMap.get(value)!.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(value: number): boolean {
|
||||||
|
if (this.data.add(value)) {
|
||||||
|
this.hasMap.get(value)?.set(true);
|
||||||
|
this.keys.reportChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(value: number): boolean {
|
||||||
|
if (this.data.delete(value)) {
|
||||||
|
this.hasMap.get(value)?.set(false);
|
||||||
|
this.keys.reportChanged();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
if (this.data.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.data.clear();
|
||||||
|
for (const entry of this.hasMap) {
|
||||||
|
entry[1].set(false);
|
||||||
|
}
|
||||||
|
this.keys.reportChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): Generator<number, void, void> {
|
||||||
|
this.keys.reportObserved();
|
||||||
|
return this.data[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ObservableListSelection {
|
||||||
|
selected = new ObservableBTree(6);
|
||||||
|
rangeStart = 0;
|
||||||
|
selectedIndex: number | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.selected.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
has(index: number) {
|
||||||
|
return this.selected.has(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
select(index: number, ctrlKey: boolean, shiftKey: boolean) {
|
||||||
|
if (this.rangeStart !== null && shiftKey) {
|
||||||
|
if (!ctrlKey) {
|
||||||
|
this.selected.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
let [start, end] = [this.rangeStart, index];
|
||||||
|
if (start > end) {
|
||||||
|
[start, end] = [end, start];
|
||||||
|
}
|
||||||
|
for (let i = start; i <= end; i += 1) {
|
||||||
|
this.selected.add(i);
|
||||||
|
}
|
||||||
|
this.selectedIndex = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctrlKey) {
|
||||||
|
if (this.selected.has(index)) {
|
||||||
|
this.selected.delete(index);
|
||||||
|
this.selectedIndex = null;
|
||||||
|
} else {
|
||||||
|
this.selected.add(index);
|
||||||
|
this.selectedIndex = index;
|
||||||
|
}
|
||||||
|
this.rangeStart = index;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selected.clear();
|
||||||
|
this.selected.add(index);
|
||||||
|
this.rangeStart = index;
|
||||||
|
this.selectedIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.selected.clear();
|
||||||
|
this.rangeStart = 0;
|
||||||
|
this.selectedIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this.selected[Symbol.iterator]();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ADB_COMMAND_NAME = {
|
const ADB_COMMAND_NAME = {
|
||||||
[AdbCommand.Auth]: "AUTH",
|
[AdbCommand.Auth]: "AUTH",
|
||||||
[AdbCommand.Close]: "CLSE",
|
[AdbCommand.Close]: "CLSE",
|
||||||
|
@ -38,6 +183,12 @@ interface Column extends GridColumn {
|
||||||
|
|
||||||
const LINE_HEIGHT = 32;
|
const LINE_HEIGHT = 32;
|
||||||
|
|
||||||
|
function uint8ArrayToHexString(array: Uint8Array) {
|
||||||
|
return Array.from(array)
|
||||||
|
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||||
|
.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
const state = new (class {
|
const state = new (class {
|
||||||
get empty() {
|
get empty() {
|
||||||
return !GLOBAL_STATE.logs.length;
|
return !GLOBAL_STATE.logs.length;
|
||||||
|
@ -50,21 +201,60 @@ const state = new (class {
|
||||||
disabled: this.empty,
|
disabled: this.empty,
|
||||||
iconProps: { iconName: Icons.Delete },
|
iconProps: { iconName: Icons.Delete },
|
||||||
text: "Clear",
|
text: "Clear",
|
||||||
onClick: () => GLOBAL_STATE.clearLog(),
|
onClick: action(() => GLOBAL_STATE.clearLog()),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "select-all",
|
||||||
|
disabled: this.empty,
|
||||||
|
iconProps: { iconName: Icons.Wand },
|
||||||
|
text: "Select All",
|
||||||
|
onClick: action(() => {
|
||||||
|
this.selection.clear();
|
||||||
|
this.selection.select(
|
||||||
|
GLOBAL_STATE.logs.length - 1,
|
||||||
|
false,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "copy",
|
||||||
|
disabled: this.selection.size === 0,
|
||||||
|
iconProps: { iconName: Icons.Copy },
|
||||||
|
text: "Copy",
|
||||||
|
onClick: () => {
|
||||||
|
let text = "";
|
||||||
|
for (const index of this.selection) {
|
||||||
|
const entry = GLOBAL_STATE.logs[index];
|
||||||
|
// prettier-ignore
|
||||||
|
text += `${
|
||||||
|
entry.timestamp!.toISOString()
|
||||||
|
}\t${
|
||||||
|
entry.direction === 'in' ? "IN" : "OUT"
|
||||||
|
}\t${
|
||||||
|
ADB_COMMAND_NAME[entry.command as keyof typeof ADB_COMMAND_NAME]
|
||||||
|
}\t${
|
||||||
|
entry.arg0.toString(16).padStart(8,'0')
|
||||||
|
}\t${
|
||||||
|
entry.arg1.toString(16).padStart(8,'0')
|
||||||
|
}\t${
|
||||||
|
uint8ArrayToHexString(entry.payload)
|
||||||
|
}\n`;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedPacket: PacketLogItem | undefined = undefined;
|
selection = new ObservableListSelection();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
makeAutoObservable(this, {
|
makeAutoObservable(this, {});
|
||||||
selectedPacket: observable.ref,
|
|
||||||
});
|
|
||||||
|
|
||||||
autorun(() => {
|
autorun(() => {
|
||||||
if (GLOBAL_STATE.logs.length === 0) {
|
if (GLOBAL_STATE.logs.length === 0) {
|
||||||
this.selectedPacket = undefined;
|
runInAction(() => this.selection.clear());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -244,21 +434,24 @@ const Row = observer(function Row({
|
||||||
}: GridRowProps) {
|
}: GridRowProps) {
|
||||||
const classes = useClasses();
|
const classes = useClasses();
|
||||||
|
|
||||||
const handleClick = useStableCallback(() => {
|
const handlePointerDown = useStableCallback(
|
||||||
runInAction(() => {
|
(e: PointerEvent<HTMLDivElement>) => {
|
||||||
state.selectedPacket = GLOBAL_STATE.logs[rowIndex];
|
runInAction(() => {
|
||||||
});
|
e.preventDefault();
|
||||||
});
|
e.stopPropagation();
|
||||||
|
state.selection.select(rowIndex, e.ctrlKey, e.shiftKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={mergeClasses(
|
className={mergeClasses(
|
||||||
className,
|
className,
|
||||||
classes.row,
|
classes.row,
|
||||||
state.selectedPacket === GLOBAL_STATE.logs[rowIndex] &&
|
state.selection.has(rowIndex) && classes.selected
|
||||||
classes.selected
|
|
||||||
)}
|
)}
|
||||||
onClick={handleClick}
|
onPointerDown={handlePointerDown}
|
||||||
{...rest}
|
{...rest}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -286,12 +479,16 @@ const PacketLog: NextPage = () => {
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
|
|
||||||
{state.selectedPacket &&
|
{state.selection.selectedIndex !== null &&
|
||||||
state.selectedPacket.payload.length > 0 && (
|
GLOBAL_STATE.logs[state.selection.selectedIndex].payload
|
||||||
|
.length > 0 && (
|
||||||
<StackItem className={classes.grow} grow>
|
<StackItem className={classes.grow} grow>
|
||||||
<HexViewer
|
<HexViewer
|
||||||
className={classes.hexViewer}
|
className={classes.hexViewer}
|
||||||
data={state.selectedPacket.payload}
|
data={
|
||||||
|
GLOBAL_STATE.logs[state.selection.selectedIndex]
|
||||||
|
.payload
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</StackItem>
|
</StackItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -6,6 +6,7 @@ export type PacketLogItemDirection = "in" | "out";
|
||||||
export interface PacketLogItem extends AdbPacketData {
|
export interface PacketLogItem extends AdbPacketData {
|
||||||
direction: PacketLogItemDirection;
|
direction: PacketLogItemDirection;
|
||||||
|
|
||||||
|
timestamp?: Date;
|
||||||
commandString?: string;
|
commandString?: string;
|
||||||
arg0String?: string;
|
arg0String?: string;
|
||||||
arg1String?: string;
|
arg1String?: string;
|
||||||
|
@ -48,11 +49,12 @@ export class GlobalState {
|
||||||
|
|
||||||
appendLog(direction: PacketLogItemDirection, packet: AdbPacketData) {
|
appendLog(direction: PacketLogItemDirection, packet: AdbPacketData) {
|
||||||
(packet as PacketLogItem).direction = direction;
|
(packet as PacketLogItem).direction = direction;
|
||||||
|
(packet as PacketLogItem).timestamp = new Date();
|
||||||
this.logs.push(packet as PacketLogItem);
|
this.logs.push(packet as PacketLogItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearLog() {
|
clearLog() {
|
||||||
this.logs = [];
|
this.logs.length = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
1423
common/config/rush/pnpm-lock.yaml
generated
1423
common/config/rush/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
|
||||||
{
|
{
|
||||||
"pnpmShrinkwrapHash": "cc0a83ec8632b8d767ce54866717a4770246d3dd",
|
"pnpmShrinkwrapHash": "a4548759bf683ee48a45a7367030c615f7b33683",
|
||||||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// cspell: ignore logcat
|
// cspell: ignore logcat
|
||||||
|
// cspell: ignore usec
|
||||||
|
|
||||||
import { AdbCommandBase, AdbSubprocessNoneProtocol } from "@yume-chan/adb";
|
import { AdbCommandBase, AdbSubprocessNoneProtocol } from "@yume-chan/adb";
|
||||||
import {
|
import {
|
||||||
|
@ -124,12 +125,21 @@ export function formatAndroidLogEntry(
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
// TODO: implement other formats
|
// TODO: implement other formats
|
||||||
default:
|
default: {
|
||||||
return `${
|
// prettier-ignore
|
||||||
|
const text=`${
|
||||||
AndroidLogPriorityToCharacter[entry.priority]
|
AndroidLogPriorityToCharacter[entry.priority]
|
||||||
}/${entry.tag.padEnd(8)}(${uid}${entry.pid
|
}/${
|
||||||
.toString()
|
entry.tag.padEnd(8)
|
||||||
.padStart(5)}): ${entry.message}`;
|
}(${
|
||||||
|
uid
|
||||||
|
}${
|
||||||
|
entry.pid.toString().padStart(5)
|
||||||
|
}): ${
|
||||||
|
entry.message
|
||||||
|
}`;
|
||||||
|
return text;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue