mirror of
https://github.com/yume-chan/ya-webadb.git
synced 2025-10-05 19:42:15 +02:00
feat(scrcpy): mouse and touch control
This commit is contained in:
parent
dc073a5d34
commit
1f6caf9604
23 changed files with 487 additions and 148 deletions
|
@ -2,6 +2,7 @@ import AsyncOperationManager from '@yume-chan/async-operation-manager';
|
|||
import { AutoDisposable, EventEmitter } from '@yume-chan/event';
|
||||
import { AdbBackend } from '../backend';
|
||||
import { AdbCommand, AdbPacket } from '../packet';
|
||||
import { AutoResetEvent } from '../utils';
|
||||
import { AdbStreamController } from './controller';
|
||||
import { AdbStream } from './stream';
|
||||
|
||||
|
@ -24,6 +25,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
// (0 means open failed)
|
||||
private readonly initializers = new AsyncOperationManager(1);
|
||||
private readonly streams = new Map<number, AdbStreamController>();
|
||||
private readonly sendLock = new AutoResetEvent();
|
||||
|
||||
public readonly backend: AdbBackend;
|
||||
|
||||
|
@ -170,7 +172,7 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
arg1: number,
|
||||
payload?: string | ArrayBuffer
|
||||
): Promise<void>;
|
||||
public sendPacket(
|
||||
public async sendPacket(
|
||||
packetOrCommand: AdbPacket | AdbCommand,
|
||||
arg0?: number,
|
||||
arg1?: number,
|
||||
|
@ -192,7 +194,12 @@ export class AdbPacketDispatcher extends AutoDisposable {
|
|||
}, this.backend);
|
||||
}
|
||||
|
||||
return AdbPacket.write(packet, this.backend);
|
||||
try {
|
||||
await this.sendLock.wait();
|
||||
await AdbPacket.write(packet, this.backend);
|
||||
} finally {
|
||||
this.sendLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
|
|
66
packages/demo/package-lock.json
generated
66
packages/demo/package-lock.json
generated
|
@ -82,9 +82,9 @@
|
|||
}
|
||||
},
|
||||
"@microsoft/load-themed-styles": {
|
||||
"version": "1.10.101",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.101.tgz",
|
||||
"integrity": "sha512-TERGxGXEmQ6lIQqgR/1MMMJCMXwk5sTN/Nt4PJnw28ZKa7T6mGAzxRDp6QLCxJCqST1YquTJoayv6ELhQSPFlQ=="
|
||||
"version": "1.10.105",
|
||||
"resolved": "https://registry.npmjs.org/@microsoft/load-themed-styles/-/load-themed-styles-1.10.105.tgz",
|
||||
"integrity": "sha512-0X0WXDFrdSFMKkGNZ7e9iti1znGOAlCpg5HEWwDmA+ma+KMd2rUkDXWDySdESRYdJQ6W2/r0Zu2UGxtomK90aQ=="
|
||||
},
|
||||
"@nodelib/fs.scandir": {
|
||||
"version": "2.1.3",
|
||||
|
@ -2143,20 +2143,20 @@
|
|||
}
|
||||
},
|
||||
"es-abstract": {
|
||||
"version": "1.17.6",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz",
|
||||
"integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==",
|
||||
"version": "1.17.7",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz",
|
||||
"integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-callable": "^1.2.0",
|
||||
"is-regex": "^1.1.0",
|
||||
"object-inspect": "^1.7.0",
|
||||
"is-callable": "^1.2.2",
|
||||
"is-regex": "^1.1.1",
|
||||
"object-inspect": "^1.8.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.0",
|
||||
"object.assign": "^4.1.1",
|
||||
"string.prototype.trimend": "^1.0.1",
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
|
@ -4555,13 +4555,35 @@
|
|||
"dev": true
|
||||
},
|
||||
"object-is": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz",
|
||||
"integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz",
|
||||
"integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"define-properties": "^1.1.3",
|
||||
"es-abstract": "^1.17.5"
|
||||
"es-abstract": "^1.18.0-next.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.0-next.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
|
||||
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-callable": "^1.2.2",
|
||||
"is-negative-zero": "^2.0.0",
|
||||
"is-regex": "^1.1.1",
|
||||
"object-inspect": "^1.8.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.1",
|
||||
"string.prototype.trimend": "^1.0.1",
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"object-keys": {
|
||||
|
@ -4592,21 +4614,21 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"es-abstract": {
|
||||
"version": "1.18.0-next.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.0.tgz",
|
||||
"integrity": "sha512-elZXTZXKn51hUBdJjSZGYRujuzilgXo8vSPQzjGYXLvSlGiCo8VO8ZGV3kjo9a0WNJJ57hENagwbtlRuHuzkcQ==",
|
||||
"version": "1.18.0-next.1",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz",
|
||||
"integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.1",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1",
|
||||
"is-callable": "^1.2.0",
|
||||
"is-callable": "^1.2.2",
|
||||
"is-negative-zero": "^2.0.0",
|
||||
"is-regex": "^1.1.1",
|
||||
"object-inspect": "^1.8.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"object.assign": "^4.1.0",
|
||||
"object.assign": "^4.1.1",
|
||||
"string.prototype.trimend": "^1.0.1",
|
||||
"string.prototype.trimstart": "^1.0.1"
|
||||
}
|
||||
|
@ -4639,9 +4661,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"office-ui-fabric-react": {
|
||||
"version": "7.144.1",
|
||||
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.144.1.tgz",
|
||||
"integrity": "sha512-lsJgKXEM4t16nuR4IVoYX5p8I4myV5p4NHMddnBeBlh/vEL8hdqRqAEnm8f7kgwZymXVxurD0FDHmU02VRfR0g==",
|
||||
"version": "7.144.2",
|
||||
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.144.2.tgz",
|
||||
"integrity": "sha512-okn1iubHJJmSETIZW89pV4xMVRiVeD9k7qxH+xlNmMDgofE2NAv6XwxdeaR0rXMYMVkRNGgc/B+2iSM24rEXhg==",
|
||||
"requires": {
|
||||
"@fluentui/date-time-utilities": "^7.9.0",
|
||||
"@fluentui/react-focus": "^7.16.9",
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
"@uifabric/react-hooks": "7.13.5",
|
||||
"@yume-chan/adb": "^0.0.1",
|
||||
"@yume-chan/adb-backend-web": "^0.0.1",
|
||||
"@yume-chan/event": "^0.0.1",
|
||||
"@yume-chan/struct": "^0.0.0",
|
||||
"jmuxer": "1.2.0",
|
||||
"react": "16.13.1",
|
||||
|
|
|
@ -13,7 +13,7 @@ interface ConnectProps {
|
|||
onDeviceChange: (device: Adb | undefined) => void;
|
||||
}
|
||||
|
||||
export default withDisplayName('Connect', ({
|
||||
export default withDisplayName('Connect')(({
|
||||
device,
|
||||
onDeviceChange,
|
||||
}: ConnectProps): JSX.Element | null => {
|
||||
|
@ -154,7 +154,7 @@ export default withDisplayName('Connect', ({
|
|||
<Dialog
|
||||
hidden={!connecting}
|
||||
dialogContentProps={{
|
||||
title: 'Connecting',
|
||||
title: 'Connecting...',
|
||||
subText: 'Please authorize the connection on your device'
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -11,7 +11,7 @@ export const ErrorDialogContext = React.createContext<ErrorDialogContext>({
|
|||
show() { }
|
||||
});
|
||||
|
||||
export default withDisplayName('ErrorDialogProvider', (props: PropsWithChildren<{}>) => {
|
||||
export default withDisplayName('ErrorDialogProvider')((props: PropsWithChildren<{}>) => {
|
||||
const [errorDialogVisible, { setTrue: showErrorDialog, setFalse: hideErrorDialog }] = useBoolean(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ export interface CacheRouteProps extends RouteProps {
|
|||
noCache?: boolean;
|
||||
}
|
||||
|
||||
export const CacheRoute = withDisplayName('CacheRoute', (props: CacheRouteProps) => {
|
||||
export const CacheRoute = withDisplayName('CacheRoute')((props: CacheRouteProps) => {
|
||||
const match = useRouteMatch(props);
|
||||
|
||||
const everMatched = useRef(!!match);
|
||||
|
@ -53,7 +53,7 @@ export interface CacheSwitchProps {
|
|||
children: React.ReactNodeArray;
|
||||
}
|
||||
|
||||
export const CacheSwitch = withDisplayName('CacheSwitch', (props: CacheSwitchProps) => {
|
||||
export const CacheSwitch = withDisplayName('CacheSwitch')((props: CacheSwitchProps) => {
|
||||
const location = useLocation();
|
||||
let contextMatch = useRouteMatch();
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import path from 'path';
|
|||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import StreamSaver from 'streamsaver';
|
||||
import { ErrorDialogContext } from '../error-dialog';
|
||||
import { CommandBar, withDisplayName } from '../utils';
|
||||
import { CommandBar, delay, formatSize, formatSpeed, useSpeed, withDisplayName } from '../utils';
|
||||
import { RouteProps } from './type';
|
||||
|
||||
initializeFileTypeIcons();
|
||||
|
@ -29,25 +29,6 @@ const classNames = mergeStyleSets({
|
|||
},
|
||||
});
|
||||
|
||||
const units = [' B', ' KB', ' MB', ' GB'];
|
||||
function formatSize(value: number): string {
|
||||
let index = 0;
|
||||
while (index < units.length && value > 1024) {
|
||||
index += 1;
|
||||
value /= 1024;
|
||||
}
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: 2 }) + units[index];
|
||||
}
|
||||
|
||||
function extensionName(fileName: string): string {
|
||||
const index = fileName.lastIndexOf('.');
|
||||
if (index === -1) {
|
||||
return '';
|
||||
} else {
|
||||
return fileName.slice(index);
|
||||
}
|
||||
}
|
||||
|
||||
const renderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props?, defaultRender?) => {
|
||||
if (!props || !defaultRender) {
|
||||
return null;
|
||||
|
@ -59,12 +40,6 @@ const renderDetailsHeader: IRenderFunction<IDetailsHeaderProps> = (props?, defau
|
|||
});
|
||||
};
|
||||
|
||||
function delay(time: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
}
|
||||
|
||||
function createReadableStreamFromBufferIterator(
|
||||
iterator: AsyncIterator<ArrayBuffer>
|
||||
): ReadableStream<Uint8Array> {
|
||||
|
@ -96,7 +71,7 @@ async function* chunkFile(file: File): AsyncGenerator<ArrayBuffer, void, void> {
|
|||
}
|
||||
}
|
||||
|
||||
export const FileManager = withDisplayName('FileManager', ({
|
||||
export const FileManager = withDisplayName('FileManager')(({
|
||||
device,
|
||||
}: RouteProps): JSX.Element | null => {
|
||||
const { show: showErrorDialog } = useContext(ErrorDialogContext);
|
||||
|
@ -246,7 +221,7 @@ export const FileManager = withDisplayName('FileManager', ({
|
|||
case LinuxFileType.Directory:
|
||||
return <Icon {...getFileTypeIconProps({ size: 20, type: FileIconType.folder })} />;
|
||||
case LinuxFileType.File:
|
||||
return <Icon {...getFileTypeIconProps({ size: 20, extension: extensionName(item.name!) })} />;
|
||||
return <Icon {...getFileTypeIconProps({ size: 20, extension: path.extname(item.name!) })} />;
|
||||
default:
|
||||
return <Icon {...getFileTypeIconProps({ size: 20, extension: 'txt' })} />;
|
||||
}
|
||||
|
@ -337,7 +312,7 @@ export const FileManager = withDisplayName('FileManager', ({
|
|||
setCurrentPath(path.resolve(currentPath, item.name!));
|
||||
break;
|
||||
case LinuxFileType.File:
|
||||
switch (extensionName(item.name!)) {
|
||||
switch (path.extname(item.name!)) {
|
||||
case '.jpg':
|
||||
case '.png':
|
||||
case '.svg':
|
||||
|
@ -359,19 +334,10 @@ export const FileManager = withDisplayName('FileManager', ({
|
|||
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadPath, setUploadPath] = useState('');
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadedSize, setUploadedSize] = useState(0);
|
||||
const [uploadTotalSize, setUploadTotalSize] = useState(0);
|
||||
const [uploadSpeed, setUploadSpeed] = useState(0);
|
||||
const [debouncedUploadedSize, uploadSpeed] = useSpeed(uploadedSize, uploadTotalSize);
|
||||
const upload = useCallback(async (file: File) => {
|
||||
let lastSecondUploadedSize = 0;
|
||||
let currentUploadedSize = 0;
|
||||
const intervalId = window.setInterval(() => {
|
||||
setUploadedSize(currentUploadedSize);
|
||||
setUploadSpeed(currentUploadedSize - lastSecondUploadedSize);
|
||||
lastSecondUploadedSize = currentUploadedSize;
|
||||
}, 1000);
|
||||
|
||||
const sync = await device!.sync();
|
||||
try {
|
||||
const itemPath = path.resolve(currentPath, file.name);
|
||||
|
@ -383,10 +349,7 @@ export const FileManager = withDisplayName('FileManager', ({
|
|||
chunkFile(file),
|
||||
(LinuxFileType.File << 12) | 0o666,
|
||||
file.lastModified / 1000,
|
||||
(uploadedSize) => {
|
||||
setUploadProgress(uploadedSize / file.size);
|
||||
currentUploadedSize = uploadedSize;
|
||||
},
|
||||
setUploadedSize,
|
||||
);
|
||||
} catch (e) {
|
||||
showErrorDialog(e.message);
|
||||
|
@ -394,7 +357,6 @@ export const FileManager = withDisplayName('FileManager', ({
|
|||
sync.dispose();
|
||||
load();
|
||||
setUploading(false);
|
||||
window.clearInterval(intervalId);
|
||||
}
|
||||
}, [currentPath, device]);
|
||||
|
||||
|
@ -565,8 +527,8 @@ export const FileManager = withDisplayName('FileManager', ({
|
|||
}}
|
||||
>
|
||||
<ProgressIndicator
|
||||
description={`${formatSize(uploadedSize)} / ${formatSize(uploadTotalSize)} at ${formatSize(uploadSpeed)}/s`}
|
||||
percentComplete={uploadProgress}
|
||||
description={formatSpeed(debouncedUploadedSize, uploadTotalSize, uploadSpeed)}
|
||||
percentComplete={uploadedSize / uploadTotalSize}
|
||||
/>
|
||||
</Dialog>
|
||||
</StackItem>
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|||
import { CommandBar, DeviceView, withDisplayName } from '../utils';
|
||||
import { RouteProps } from './type';
|
||||
|
||||
export const FrameBuffer = withDisplayName('FrameBuffer', ({
|
||||
export const FrameBuffer = withDisplayName('FrameBuffer')(({
|
||||
device
|
||||
}: RouteProps): JSX.Element | null => {
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
|
|
|
@ -15,7 +15,7 @@ interface CopyLinkProps {
|
|||
href: string;
|
||||
}
|
||||
|
||||
const CopyLink = withDisplayName('CopyLink', ({
|
||||
const CopyLink = withDisplayName('CopyLink')(({
|
||||
href,
|
||||
}: CopyLinkProps) => {
|
||||
const calloutTarget = useRef<HTMLButtonElement | null>(null);
|
||||
|
@ -44,7 +44,7 @@ const CopyLink = withDisplayName('CopyLink', ({
|
|||
);
|
||||
});
|
||||
|
||||
export const Intro = withDisplayName('Intro', () => {
|
||||
export const Intro = withDisplayName('Intro')(() => {
|
||||
return (
|
||||
<>
|
||||
<Text block>
|
||||
|
|
55
packages/demo/src/routes/scrcpy/control.ts
Normal file
55
packages/demo/src/routes/scrcpy/control.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
import { Struct, placeholder, StructInitType } from '@yume-chan/struct';
|
||||
|
||||
export const enum ScrcpyControlMessageType {
|
||||
InjectKeycode,
|
||||
InjectText,
|
||||
InjectTouch,
|
||||
InjectScroll,
|
||||
BackOrScreenOn,
|
||||
ExpandNotificationPanel,
|
||||
CollapseNotificationPanel,
|
||||
GetClipboard,
|
||||
SetClipboard,
|
||||
SetScreenPowerMode,
|
||||
RotateDevice,
|
||||
}
|
||||
|
||||
export const ScrcpySimpleControlMessage =
|
||||
new Struct()
|
||||
.uint8('type', undefined, placeholder<ScrcpyControlMessageType.BackOrScreenOn>());
|
||||
|
||||
export type ScrcpySimpleControlMessage = StructInitType<typeof ScrcpySimpleControlMessage>;
|
||||
|
||||
export const enum AndroidMotionEventAction {
|
||||
Down,
|
||||
Up,
|
||||
Move,
|
||||
Cancel,
|
||||
Outside,
|
||||
PointerDown,
|
||||
PointerUp,
|
||||
HoverMove,
|
||||
Scroll,
|
||||
HoverEnter,
|
||||
HoverExit,
|
||||
ButtonPress,
|
||||
ButtonRelease,
|
||||
}
|
||||
|
||||
export const ScrcpyInjectTouchControlMessage =
|
||||
new Struct()
|
||||
.uint8('type', undefined, ScrcpyControlMessageType.InjectTouch as const)
|
||||
.uint8('action', undefined, placeholder<AndroidMotionEventAction>())
|
||||
.uint64('pointerId')
|
||||
.uint32('pointerX')
|
||||
.uint32('pointerY')
|
||||
.uint16('screenWidth')
|
||||
.uint16('screenHeight')
|
||||
.uint16('pressure')
|
||||
.uint32('buttons');
|
||||
|
||||
export type ScrcpyInjectTouchControlMessage = StructInitType<typeof ScrcpyInjectTouchControlMessage>;
|
||||
|
||||
export type ScrcpyControlMessage =
|
||||
ScrcpySimpleControlMessage |
|
||||
ScrcpyInjectTouchControlMessage;
|
60
packages/demo/src/routes/scrcpy/fetch-server.ts
Normal file
60
packages/demo/src/routes/scrcpy/fetch-server.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { EventEmitter } from "@yume-chan/event";
|
||||
import serverUrl from 'file-loader!./scrcpy-server';
|
||||
|
||||
class FetchWithProgress {
|
||||
public readonly promise: Promise<ArrayBuffer>;
|
||||
|
||||
private _downloaded = 0;
|
||||
public get downloaded() { return this._downloaded; }
|
||||
|
||||
private _total = 0;
|
||||
public get total() { return this._total; }
|
||||
|
||||
private progressEvent = new EventEmitter<[download: number, total: number]>();
|
||||
public get onProgress() { return this.progressEvent.event; }
|
||||
|
||||
public constructor(url: string) {
|
||||
this.promise = this.fetch(url);
|
||||
}
|
||||
|
||||
private async fetch(url: string) {
|
||||
const response = await window.fetch(url);
|
||||
this._total = Number.parseInt(response.headers.get('Content-Length') ?? '0', 10);
|
||||
this.progressEvent.fire([this._downloaded, this._total]);
|
||||
|
||||
const reader = response.body!.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
while (true) {
|
||||
const result = await reader.read();
|
||||
if (result.done) {
|
||||
break;
|
||||
}
|
||||
chunks.push(result.value);
|
||||
this._downloaded += result.value.byteLength;
|
||||
this.progressEvent.fire([this._downloaded, this._total]);
|
||||
}
|
||||
|
||||
this._total = chunks.reduce((result, item) => result + item.byteLength, 0);
|
||||
const result = new Uint8Array(this._total);
|
||||
let position = 0;
|
||||
for (const chunk of chunks) {
|
||||
result.set(chunk, position);
|
||||
position += chunk.byteLength;
|
||||
}
|
||||
return result.buffer;
|
||||
}
|
||||
}
|
||||
|
||||
let cachedValue: FetchWithProgress | undefined;
|
||||
export function fetchServer(onProgress?: (e: [downloaded: number, total: number]) => void) {
|
||||
if (!cachedValue) {
|
||||
cachedValue = new FetchWithProgress(serverUrl);
|
||||
}
|
||||
|
||||
if (onProgress) {
|
||||
cachedValue.onProgress(onProgress);
|
||||
onProgress([cachedValue.downloaded, cachedValue.total]);
|
||||
}
|
||||
|
||||
return cachedValue.promise;
|
||||
}
|
|
@ -1,19 +1,13 @@
|
|||
import { ICommandBarItemProps } from '@fluentui/react';
|
||||
import { Dialog, ICommandBarItemProps, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
|
||||
import { AdbBufferedStream, AdbStream, EventQueue } from '@yume-chan/adb';
|
||||
import { Struct } from '@yume-chan/struct';
|
||||
import serverUrl from 'file-loader!./scrcpy-server';
|
||||
import JMuxer from 'jmuxer';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { CommandBar, DeviceView, ExternalLink, withDisplayName } from '../../utils';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { CommonStackTokens } from '../../styles';
|
||||
import { CommandBar, DeviceView, DeviceViewRef, ExternalLink, formatSpeed, useSpeed, withDisplayName } from '../../utils';
|
||||
import { RouteProps } from '../type';
|
||||
|
||||
let cachedServerBinary: Promise<ArrayBuffer> | undefined;
|
||||
function getServerBinary() {
|
||||
if (!cachedServerBinary) {
|
||||
cachedServerBinary = fetch(serverUrl).then(response => response.arrayBuffer());
|
||||
}
|
||||
return cachedServerBinary;
|
||||
}
|
||||
import { AndroidMotionEventAction, ScrcpyControlMessage, ScrcpyControlMessageType, ScrcpyInjectTouchControlMessage, ScrcpySimpleControlMessage } from './control';
|
||||
import { fetchServer } from './fetch-server';
|
||||
|
||||
const DeviceServerPath = '/data/local/tmp/scrcpy-server.jar';
|
||||
|
||||
|
@ -59,6 +53,7 @@ async function receiveVideo(stream: AdbBufferedStream, jmuxer: JMuxer) {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
jmuxer.destroy();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -86,6 +81,26 @@ async function receiveControl(stream: AdbBufferedStream) {
|
|||
}
|
||||
}
|
||||
|
||||
async function sendControl(stream: AdbBufferedStream, queue: EventQueue<ScrcpyControlMessage>) {
|
||||
try {
|
||||
while (true) {
|
||||
const message = await queue.next();
|
||||
let buffer: ArrayBuffer;
|
||||
switch (message.type) {
|
||||
case ScrcpyControlMessageType.InjectTouch:
|
||||
buffer = ScrcpyInjectTouchControlMessage.serialize(message, stream);
|
||||
break;
|
||||
default:
|
||||
buffer = ScrcpySimpleControlMessage.serialize(message, stream);
|
||||
break;
|
||||
}
|
||||
await stream.write(buffer);
|
||||
}
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export const enum ScrcpyLogLevel {
|
||||
Debug = 'debug',
|
||||
Info = 'info',
|
||||
|
@ -101,7 +116,7 @@ export const enum ScrcpyScreenOrientation {
|
|||
LandscapeFlipped = 3,
|
||||
}
|
||||
|
||||
export const Scrcpy = withDisplayName('Scrcpy', ({
|
||||
export const Scrcpy = withDisplayName('Scrcpy')(({
|
||||
device
|
||||
}: RouteProps): JSX.Element | null => {
|
||||
const [running, setRunning] = useState(false);
|
||||
|
@ -109,18 +124,38 @@ export const Scrcpy = withDisplayName('Scrcpy', ({
|
|||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||
const [width, setWidth] = useState(0);
|
||||
const [height, setHeight] = useState(0);
|
||||
const handleVideoRef = useCallback((value: HTMLVideoElement | null) => {
|
||||
videoRef.current = value;
|
||||
if (value) {
|
||||
value.onresize = () => {
|
||||
setWidth(value.videoWidth);
|
||||
setHeight(value.videoHeight);
|
||||
const handleVideoRef = useCallback((video: HTMLVideoElement | null) => {
|
||||
videoRef.current = video;
|
||||
if (video) {
|
||||
video.onresize = () => {
|
||||
setWidth(video.videoWidth);
|
||||
setHeight(video.videoHeight);
|
||||
};
|
||||
|
||||
video.addEventListener('touchmove', e => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
video.addEventListener('pause', () => {
|
||||
console.log('???');
|
||||
video.play();
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const serverRef = useRef<AdbStream | undefined>();
|
||||
const controlStreamRef = useRef<AdbBufferedStream | undefined>();
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
|
||||
const [serverTotalSize, setServerTotalSize] = useState(0);
|
||||
|
||||
const [serverDownloadedSize, setServerDownloadedSize] = useState(0);
|
||||
const [debouncedServerDownloadedSize, serverDownloadSpeed] = useSpeed(serverDownloadedSize, serverTotalSize);
|
||||
|
||||
const [serverUploadedSize, setServerUploadedSize] = useState(0);
|
||||
const [debouncedServerUploadedSize, serverUploadSpeed] = useSpeed(serverUploadedSize, serverTotalSize);
|
||||
|
||||
const serverRef = useRef<AdbStream>();
|
||||
const eventQueueRef = useRef<EventQueue<ScrcpyControlMessage>>();
|
||||
const controlStreamRef = useRef<AdbBufferedStream>();
|
||||
|
||||
const start = useCallback(() => {
|
||||
if (!device) {
|
||||
|
@ -128,10 +163,24 @@ export const Scrcpy = withDisplayName('Scrcpy', ({
|
|||
}
|
||||
|
||||
(async () => {
|
||||
const serverBuffer = await getServerBinary();
|
||||
setServerTotalSize(0);
|
||||
setServerDownloadedSize(0);
|
||||
setServerUploadedSize(0);
|
||||
setConnecting(true);
|
||||
|
||||
const serverBuffer = await fetchServer(([downloaded, total]) => {
|
||||
setServerDownloadedSize(downloaded);
|
||||
setServerTotalSize(total);
|
||||
});
|
||||
|
||||
const sync = await device.sync();
|
||||
await sync.write(DeviceServerPath, serverBuffer);
|
||||
await sync.write(
|
||||
DeviceServerPath,
|
||||
serverBuffer,
|
||||
undefined,
|
||||
undefined,
|
||||
setServerUploadedSize
|
||||
);
|
||||
|
||||
const listener = new EventQueue<AdbStream>();
|
||||
const reverseDeviceAddress = await device.reverse.add('localabstract:scrcpy', 27183, {
|
||||
|
@ -187,41 +236,65 @@ export const Scrcpy = withDisplayName('Scrcpy', ({
|
|||
});
|
||||
|
||||
serverRef.current = server;
|
||||
|
||||
setConnecting(false);
|
||||
setRunning(true);
|
||||
|
||||
eventQueueRef.current = new EventQueue<ScrcpyControlMessage>();
|
||||
|
||||
await Promise.all([
|
||||
receiveVideo(videoStream, jmuxer),
|
||||
receiveControl(controlStream),
|
||||
sendControl(controlStream, eventQueueRef.current),
|
||||
]);
|
||||
|
||||
jmuxer.destroy();
|
||||
await server.close();
|
||||
serverRef.current = undefined;
|
||||
setRunning(false);
|
||||
stop();
|
||||
})();
|
||||
}, [device]);
|
||||
|
||||
const stop = useCallback(() => {
|
||||
serverRef.current!.close();
|
||||
if (!serverRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventQueueRef.current!.end();
|
||||
|
||||
serverRef.current.close();
|
||||
serverRef.current = undefined;
|
||||
|
||||
setRunning(false);
|
||||
}, []);
|
||||
|
||||
const deviceViewRef = useRef<DeviceViewRef | null>(null);
|
||||
const commandBarItems = useMemo((): ICommandBarItemProps[] => {
|
||||
const reuslt: ICommandBarItemProps[] = [];
|
||||
|
||||
if (running) {
|
||||
return [{
|
||||
reuslt.push({
|
||||
key: 'stop',
|
||||
iconProps: { iconName: 'Stop' },
|
||||
text: 'Stop',
|
||||
onClick: stop,
|
||||
}];
|
||||
});
|
||||
} else {
|
||||
return [{
|
||||
reuslt.push({
|
||||
key: 'start',
|
||||
disabled: !device,
|
||||
iconProps: { iconName: 'Play' },
|
||||
text: 'Start',
|
||||
onClick: start,
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
reuslt.push({
|
||||
key: 'fullscreen',
|
||||
disabled: !running,
|
||||
iconProps: { iconName: 'Fullscreen' },
|
||||
text: 'Fullscreen',
|
||||
onClick: () => { deviceViewRef.current?.enterFullscreen(); },
|
||||
});
|
||||
|
||||
return reuslt;
|
||||
}, [device, running, start]);
|
||||
|
||||
const commandBarFarItems = useMemo((): ICommandBarItemProps[] => [
|
||||
|
@ -248,41 +321,121 @@ export const Scrcpy = withDisplayName('Scrcpy', ({
|
|||
}
|
||||
], []);
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent<HTMLVideoElement>) => {
|
||||
controlStreamRef.current!.write(new ArrayBuffer(10));
|
||||
}, []);
|
||||
const injectTouch = useCallback((
|
||||
action: AndroidMotionEventAction,
|
||||
e: React.PointerEvent<HTMLVideoElement>
|
||||
) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const handleTouchMove = useCallback((e: React.TouchEvent<HTMLVideoElement>) => {
|
||||
const view = e.currentTarget.getBoundingClientRect();
|
||||
const pointerViewX = e.clientX - view.x;
|
||||
const pointerViewY = e.clientY - view.y;
|
||||
const pointerScreenX = pointerViewX / view.width * width;
|
||||
const pointerScreenY = pointerViewY / view.height * height;
|
||||
|
||||
}, []);
|
||||
eventQueueRef.current!.push({
|
||||
type: ScrcpyControlMessageType.InjectTouch,
|
||||
action,
|
||||
buttons: 0,
|
||||
pointerId: BigInt(e.pointerId),
|
||||
pointerX: pointerScreenX,
|
||||
pointerY: pointerScreenY,
|
||||
pressure: e.pressure * 65535,
|
||||
screenWidth: width,
|
||||
screenHeight: height,
|
||||
});
|
||||
}, [width, height]);
|
||||
|
||||
const handleTouchEnd = useCallback((e: React.TouchEvent<HTMLVideoElement>) => {
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent<HTMLVideoElement>) => {
|
||||
injectTouch(AndroidMotionEventAction.Down, e);
|
||||
}, [injectTouch]);
|
||||
|
||||
}, []);
|
||||
const handlePointerMove = useCallback((e: React.PointerEvent<HTMLVideoElement>) => {
|
||||
if (e.pressure > 0) {
|
||||
injectTouch(AndroidMotionEventAction.Move, e);
|
||||
}
|
||||
}, [injectTouch]);
|
||||
|
||||
const handlePointerUp = useCallback((e: React.PointerEvent<HTMLVideoElement>) => {
|
||||
injectTouch(AndroidMotionEventAction.Up, e);
|
||||
}, [injectTouch]);
|
||||
|
||||
const handleKeyPress = useCallback((e: React.KeyboardEvent<HTMLVideoElement>) => {
|
||||
|
||||
}, []);
|
||||
|
||||
const handleCanPlay = useCallback(() => {
|
||||
videoRef.current!.play();
|
||||
}, []);
|
||||
// useEffect(() => {
|
||||
// function handlePageShow() {
|
||||
// if (document.visibilityState !== 'visible') {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const video = videoRef.current;
|
||||
// if (!video || video.buffered.length === 0) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// video.currentTime = video.buffered.end(0);
|
||||
// }
|
||||
|
||||
// document.addEventListener('visibilitychange', handlePageShow);
|
||||
|
||||
// return () => {
|
||||
// document.removeEventListener('visibilitychange', handlePageShow);
|
||||
// };
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandBar items={commandBarItems} farItems={commandBarFarItems} />
|
||||
<DeviceView width={width} height={height}>
|
||||
|
||||
<DeviceView ref={deviceViewRef} width={width} height={height}>
|
||||
<video
|
||||
ref={handleVideoRef}
|
||||
autoPlay
|
||||
width={width}
|
||||
height={height}
|
||||
onCanPlay={handleCanPlay}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
</DeviceView>
|
||||
|
||||
<Dialog
|
||||
hidden={!connecting}
|
||||
dialogContentProps={{
|
||||
title: 'Connecting...'
|
||||
}}
|
||||
>
|
||||
<Stack tokens={CommonStackTokens}>
|
||||
<StackItem>
|
||||
<ProgressIndicator
|
||||
label="1. Downloading scrcpy server..."
|
||||
progressHidden={serverTotalSize === 0}
|
||||
percentComplete={serverDownloadedSize / serverTotalSize}
|
||||
description={formatSpeed(debouncedServerDownloadedSize, serverTotalSize, serverDownloadSpeed)}
|
||||
/>
|
||||
</StackItem>
|
||||
|
||||
<StackItem>
|
||||
<ProgressIndicator
|
||||
label="2. Pushing scrcpy server to device..."
|
||||
progressHidden={serverTotalSize === 0 || serverDownloadedSize !== serverTotalSize}
|
||||
percentComplete={serverUploadedSize / serverTotalSize}
|
||||
description={formatSpeed(debouncedServerUploadedSize, serverTotalSize, serverUploadSpeed)}
|
||||
/>
|
||||
</StackItem>
|
||||
|
||||
<StackItem>
|
||||
<ProgressIndicator
|
||||
label="3. Starting scrcpy server on device..."
|
||||
progressHidden={serverTotalSize === 0 || serverUploadedSize !== serverTotalSize}
|
||||
/>
|
||||
</StackItem>
|
||||
</Stack>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ const ResizeObserverStyle: CSSProperties = {
|
|||
const UpIconProps = { iconName: 'ChevronUp' };
|
||||
const DownIconProps = { iconName: 'ChevronDown' };
|
||||
|
||||
export const Shell = withDisplayName('Shell', ({
|
||||
export const Shell = withDisplayName('Shell')(({
|
||||
device,
|
||||
}: RouteProps): JSX.Element | null => {
|
||||
const [findKeyword, setFindKeyword] = useState('');
|
||||
|
|
|
@ -3,7 +3,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|||
import { CommandBar, withDisplayName } from '../utils';
|
||||
import { RouteProps } from './type';
|
||||
|
||||
export const TcpIp = withDisplayName('TcpIp', ({
|
||||
export const TcpIp = withDisplayName('TcpIp')(({
|
||||
device
|
||||
}: RouteProps): JSX.Element | null => {
|
||||
const [serviceListenAddrs, setServiceListenAddrs] = useState<string[] | undefined>();
|
||||
|
|
|
@ -9,7 +9,7 @@ const ContainerStyles = {
|
|||
}
|
||||
} as const;
|
||||
|
||||
export const CommandBar = withDisplayName('CommandBar', (props: ICommandBarProps) => {
|
||||
export const CommandBar = withDisplayName('CommandBar')((props: ICommandBarProps) => {
|
||||
return (
|
||||
<StackItem styles={ContainerStyles}>
|
||||
<FluentCommandBar {...props} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { StackItem } from '@fluentui/react';
|
||||
import React, { ReactNode, useCallback, useEffect, useState } from 'react';
|
||||
import React, { ReactNode, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
import { ResizeObserver } from './resize-observer';
|
||||
import { withDisplayName } from './with-display-name';
|
||||
import { forwardRef } from './with-display-name';
|
||||
|
||||
export interface DeviceViewProps {
|
||||
width: number;
|
||||
|
@ -11,7 +11,15 @@ export interface DeviceViewProps {
|
|||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const DeviceView = withDisplayName('DeviceView', ({ width, height, children }: DeviceViewProps) => {
|
||||
export interface DeviceViewRef {
|
||||
enterFullscreen(): void;
|
||||
}
|
||||
|
||||
export const DeviceView = forwardRef<DeviceViewRef>('DeviceView')(({
|
||||
width,
|
||||
height,
|
||||
children,
|
||||
}: DeviceViewProps, ref) => {
|
||||
const [containerWidth, setContainerWidth] = useState(0);
|
||||
const [containerHeight, setContainerHeight] = useState(0);
|
||||
const [scale, setScale] = useState(1);
|
||||
|
@ -36,9 +44,15 @@ export const DeviceView = withDisplayName('DeviceView', ({ width, height, childr
|
|||
}
|
||||
}, [width, height, containerWidth, containerHeight]);
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
useImperativeHandle(ref, () => ({
|
||||
enterFullscreen() { containerRef.current!.requestFullscreen(); },
|
||||
}), []);
|
||||
|
||||
return (
|
||||
<StackItem grow>
|
||||
<ResizeObserver
|
||||
ref={containerRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
|
|
|
@ -12,7 +12,7 @@ export interface ExternalLinkProps {
|
|||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const ExternalLink = withDisplayName('ExternalLink', ({
|
||||
export const ExternalLink = withDisplayName('ExternalLink')(({
|
||||
href,
|
||||
spaceBefore,
|
||||
spaceAfter,
|
||||
|
|
48
packages/demo/src/utils/file-size.ts
Normal file
48
packages/demo/src/utils/file-size.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { useRef } from 'react';
|
||||
|
||||
const units = [' B', ' KB', ' MB', ' GB'];
|
||||
|
||||
export function formatSize(value: number): string {
|
||||
let index = 0;
|
||||
while (index < units.length && value > 1024) {
|
||||
index += 1;
|
||||
value /= 1024;
|
||||
}
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: 2 }) + units[index];
|
||||
}
|
||||
|
||||
export function formatSpeed(completed: number, total: number, speed: number): string | undefined {
|
||||
if (total === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return `${formatSize(completed)} of ${formatSize(total)} (${formatSize(speed)}/s)`;
|
||||
}
|
||||
|
||||
export function useDebounced<T>(value: T, interval = 1000): T {
|
||||
const startTime = useRef(0);
|
||||
const startValue = useRef(value);
|
||||
|
||||
const now = Date.now();
|
||||
if (now - startTime.current > interval) {
|
||||
startTime.current = now;
|
||||
startValue.current = value;
|
||||
}
|
||||
|
||||
return startValue.current;
|
||||
}
|
||||
|
||||
export function useSpeed(completed: number, total: number): [completed: number, speed: number] {
|
||||
const debouncedCompleted = useDebounced(completed);
|
||||
|
||||
if (completed === total) {
|
||||
return [completed, completed - debouncedCompleted];
|
||||
} else {
|
||||
return [debouncedCompleted, completed - debouncedCompleted];
|
||||
}
|
||||
}
|
||||
|
||||
export function delay(time: number): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
window.setTimeout(resolve, time);
|
||||
});
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
export * from './command-bar';
|
||||
export * from './device-view';
|
||||
export * from './external-link';
|
||||
export * from './file-size';
|
||||
export * from './resize-observer';
|
||||
export * from './with-display-name';
|
||||
|
|
|
@ -15,7 +15,7 @@ const iframeStyle: CSSProperties = {
|
|||
visibility: 'hidden',
|
||||
};
|
||||
|
||||
export const ResizeObserver = forwardRef('ResizeObserver', ({
|
||||
export const ResizeObserver = forwardRef<HTMLDivElement>('ResizeObserver')(({
|
||||
onResize,
|
||||
style,
|
||||
children,
|
||||
|
@ -25,7 +25,7 @@ export const ResizeObserver = forwardRef('ResizeObserver', ({
|
|||
onResizeRef.current = onResize;
|
||||
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = createMergedRef()(ref, containerRef);
|
||||
const mergedRef = createMergedRef<HTMLDivElement | null>()(ref, containerRef);
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
const { width, height } = containerRef.current!.getBoundingClientRect();
|
||||
|
|
|
@ -1,18 +1,14 @@
|
|||
import React from 'react';
|
||||
import { memo, NamedExoticComponent } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
|
||||
export function withDisplayName<P extends object>(
|
||||
name: string,
|
||||
Component: React.FunctionComponent<P>
|
||||
): NamedExoticComponent<P> {
|
||||
Component.displayName = name;
|
||||
return memo(Component);
|
||||
export function withDisplayName(name: string) {
|
||||
return <P extends object>(Component: React.FunctionComponent<P>) => {
|
||||
Component.displayName = name;
|
||||
return memo(Component);
|
||||
};
|
||||
}
|
||||
|
||||
export function forwardRef<P extends object>(
|
||||
name: string,
|
||||
Component: React.ForwardRefRenderFunction<unknown, P>
|
||||
) {
|
||||
Component.displayName = name;
|
||||
return memo(React.forwardRef(Component));
|
||||
export function forwardRef<T>(name: string) {
|
||||
return <P extends object>(Component: React.ForwardRefRenderFunction<T, P>) => {
|
||||
return withDisplayName(name)(React.forwardRef(Component));
|
||||
};
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export namespace Number {
|
|||
number;
|
||||
|
||||
export const enum SubType {
|
||||
Uint8,
|
||||
Uint16,
|
||||
Int32,
|
||||
Uint32,
|
||||
|
@ -17,6 +18,7 @@ export namespace Number {
|
|||
}
|
||||
|
||||
export const SizeMap: Record<SubType, number> = {
|
||||
[SubType.Uint8]: 1,
|
||||
[SubType.Uint16]: 2,
|
||||
[SubType.Int32]: 4,
|
||||
[SubType.Uint32]: 4,
|
||||
|
@ -25,6 +27,7 @@ export namespace Number {
|
|||
};
|
||||
|
||||
export const DataViewGetterMap = {
|
||||
[SubType.Uint8]: 'getUint8',
|
||||
[SubType.Uint16]: 'getUint16',
|
||||
[SubType.Int32]: 'getInt32',
|
||||
[SubType.Uint32]: 'getUint32',
|
||||
|
@ -33,6 +36,7 @@ export namespace Number {
|
|||
} as const;
|
||||
|
||||
export const DataViewSetterMap = {
|
||||
[SubType.Uint8]: 'setUint8',
|
||||
[SubType.Uint16]: 'setUint16',
|
||||
[SubType.Int32]: 'setInt32',
|
||||
[SubType.Uint32]: 'setUint32',
|
||||
|
|
|
@ -192,6 +192,22 @@ export default class Struct<
|
|||
});
|
||||
}
|
||||
|
||||
public uint8<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint8>
|
||||
>(
|
||||
name: TName,
|
||||
options: FieldDescriptorBaseOptions = {},
|
||||
_typescriptType?: TTypeScriptType,
|
||||
) {
|
||||
return this.number(
|
||||
name,
|
||||
Number.SubType.Uint8,
|
||||
options,
|
||||
_typescriptType
|
||||
);
|
||||
}
|
||||
|
||||
public uint16<
|
||||
TName extends string,
|
||||
TTypeScriptType = Number.TypeScriptType<Number.SubType.Uint16>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue