feat(demo): add UI for manually adding websocket backends

This commit is contained in:
Simon Chan 2021-12-12 22:53:18 +08:00
parent 3492e3bf71
commit 2e38ec5ce3
27 changed files with 1360 additions and 761 deletions

View file

@ -77,5 +77,6 @@
"files.associations": {
"*.mdx": "markdown",
"*.json": "jsonc"
}
},
"prettier.tabWidth": 4
}

39
CONTRIBUTE.md Normal file
View file

@ -0,0 +1,39 @@
## Development
The repository uses [Rush](https://rushjs.io/) for monorepo management.
### Install Rush globally
```sh
$ npm i -g @microsoft/rush
```
### Install dependencies
```sh
$ rush update
```
### Everyday commands
1. Build all packages:
```sh
$ rush build
```
2. Watch and rebuild all libraries:
```sh
$ rush build:watch
```
3. Start demo dev-server:
```sh
$ cd apps/demo
$ npm run dev
```
Usually you need two terminals to run both 2 and 3.

View file

@ -1,68 +1,51 @@
# Android Debug Bridge (ADB) for Web Browsers
# 📱 Android Debug Bridge (ADB) for Web Browsers
[![GitHub license](https://img.shields.io/github/license/yume-chan/ya-webadb)](https://github.com/yume-chan/ya-webadb/blob/master/LICENSE)
Manipulate Android devices from any (supported) web browsers, even from another Android device.
Online demo: https://yume-chan.github.io/ya-webadb
[🚀 Online Demo](https://yume-chan.github.io/ya-webadb)
## How does it work
## Compatibility
**I'm working on a series of [blog posts](https://chensi.moe/blog/2020/09/28/webadb-part0-overview/) explaining the ADB protocol and my implementation in details.**
| Connection | Chromium-based Browsers | Firefox | Node.js |
| ------------------------------------- | ------------------------------------------ | ------- | -------- |
| USB cable | Yes via [WebUSB] | No | Possible |
| Wireless via [WebSocket] <sup>1</sup> | Yes | Yes | Possible |
| Wireless via TCP | Possible via [Direct Sockets] <sup>2</sup> | No | Possible |
`@yume-chan/adb` is a platform-independent TypeScript implementation of the Android Debug Bridge (ADB) protocol.
[WebUSB]: https://wicg.github.io/webusb/
[WebSocket]: https://websockets.spec.whatwg.org/
[Direct Sockets]: https://wicg.github.io/raw-sockets/
`@yume-chan/adb-backend-webusb` is a backend for `@yume-chan/adb` that uses WebUSB API.
<sup>1</sup> Requires WebSockify softwares, see [instruction](https://github.com/yume-chan/ya-webadb/discussions/245#discussioncomment-384030) for detail.
See README in each package for details.
<sup>2</sup> Under developemnt. Requires Chrome Canary (excluding Chrome for Android) and adding page URL to `chrome://flags/#restricted-api-origins`
## Packages
## Security concerns
This repository is a monorepo containing following packages:
Accessing USB devices (especially your phone) directly from a web page can be **very dangerous**. Firefox developers even refused to implement the WebUSB standard because they [considered it to be **harmful**](https://mozilla.github.io/standards-positions/#webusb).
| Package Name | Description |
| --------------------------------------------------------------------- | ----------------------------------------------------------------- |
| adb ([README](libraries/adb/README.md)) | TypeScript implementation of Android Debug Bridge (ADB) protocol. |
| adb-backend-webusb ([README](libraries/adb-backend-webusb/README.md)) | Backend for `@yume-chan/adb` using WebUSB API. |
| event ([README](libraries/event/README.md)) | Event/EventEmitter pattern. |
| struct ([README](libraries/struct/README.md)) | C-style structure serializer and deserializer. |
| demo ([README](apps/demo/README.md)) | Demo of `@yume-chan/adb` and `@yume-chan/adb-backend-webusb`. |
## Features
## Development
* 📁 File Management
* 📋 List
* ⬆ Upload
* ⬇ Download
* 🗑 Delete
* 📷 Screen Capture
* 📜 Interactiv Shell
* ⚙ Enable ADB over WiFi
* 📦 Install APK
* 🎥 [Scrcpy](https://github.com/Genymobile/scrcpy) compatible client (screen mirroring and controling device)
The repository uses [Rush](https://rushjs.io/) for monorepo management.
[📋 Project Roadmap](https://github.com/yume-chan/ya-webadb/issues/348)
### Install Rush globally
## Contribute
```sh
$ npm i -g @microsoft/rush
```
See [CONTRIBUTE.md](./CONTRIBUTE.md)
### Install dependencies
## Credits
```sh
$ rush update
```
### Everyday commands
Build all packages:
```sh
$ rush build
```
Watch all libraries:
```sh
$ rush build:watch
```
Start demo dev-server:
```sh
$ cd apps/demo
$ npm start
```
Usually you need two terminals to run both 2 and 3.
* Google for [ADB](https://android.googlesource.com/platform/packages/modules/adb) ([Apache License 2.0](./adb.NOTICE))
* Romain Vimont for [Scrcpy](https://github.com/Genymobile/scrcpy) ([Apache License 2.0](https://github.com/Genymobile/scrcpy/blob/master/LICENSE))

View file

@ -1,11 +1,13 @@
import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, StackItem, TooltipHost } from '@fluentui/react';
import { DefaultButton, Dialog, Dropdown, IDropdownOption, PrimaryButton, ProgressIndicator, Stack, StackItem } from '@fluentui/react';
import { Adb, AdbBackend } from '@yume-chan/adb';
import AdbWebUsbBackend, { AdbWebCredentialStore, AdbWebUsbBackendWatcher } from '@yume-chan/adb-backend-webusb';
import AdbDirectSocketsBackend from "@yume-chan/adb-backend-direct-sockets";
import AdbWebUsbBackend, { AdbWebUsbBackendWatcher } from '@yume-chan/adb-backend-webusb';
import AdbWsBackend from '@yume-chan/adb-backend-ws';
import AdbWebCredentialStore from '@yume-chan/adb-credential-web';
import { observer } from 'mobx-react-lite';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { global, logger } from '../state';
import { CommonStackTokens } from '../utils';
import { CommonStackTokens, Icons } from '../utils';
const DropdownStyles = { dropdown: { width: '100%' } };
@ -52,29 +54,65 @@ function _Connect(): JSX.Element | null {
[]
);
const [wsBackendList, setWsBackendList] = useState<AdbBackend[]>([]);
const [wsBackendList, setWsBackendList] = useState<AdbWsBackend[]>([]);
useEffect(() => {
const intervalId = setInterval(async () => {
if (connecting || global.device) {
const savedList = localStorage.getItem('ws-backend-list');
if (!savedList) {
return;
}
const wsBackend = new AdbWsBackend("ws://localhost:15555");
try {
await wsBackend.connect();
setWsBackendList([wsBackend]);
setSelectedBackend(wsBackend);
} catch {
setWsBackendList([]);
} finally {
await wsBackend.dispose();
}
}, 5000);
const parsed = JSON.parse(savedList) as { address: string; }[];
setWsBackendList(parsed.map(x => new AdbWsBackend(x.address)));
}, []);
return () => {
clearInterval(intervalId);
};
}, [connecting]);
const addWsBackend = useCallback(() => {
const address = window.prompt('Enter the address of WebSockify server');
if (!address) {
return;
}
setWsBackendList(list => {
const copy = list.slice();
copy.push(new AdbWsBackend(address));
window.localStorage.setItem('ws-backend-list', JSON.stringify(copy.map(x => ({ address: x.serial }))));
return copy;
});
}, []);
const [tcpBackendList, setTcpBackendList] = useState<AdbDirectSocketsBackend[]>([]);
useEffect(() => {
if (!AdbDirectSocketsBackend.isSupported()) {
return;
}
const savedList = localStorage.getItem('tcp-backend-list');
if (!savedList) {
return;
}
const parsed = JSON.parse(savedList) as { address: string; port: number; }[];
setTcpBackendList(parsed.map(x => new AdbDirectSocketsBackend(x.address, x.port)));
}, []);
const addTcpBackend = useCallback(() => {
const address = window.prompt('Enter the address of device');
if (!address) {
return;
}
const port = window.prompt('Enter the port of device', '5555');
if (!port) {
return;
}
const portNumber = Number.parseInt(port, 10);
setTcpBackendList(list => {
const copy = list.slice();
copy.push(new AdbDirectSocketsBackend(address, portNumber));
window.localStorage.setItem('tcp-backend-list', JSON.stringify(copy.map(x => ({ address: x.address, port: x.port }))));
return copy;
});
}, []);
const handleSelectedBackendChange = (
e: React.FormEvent<HTMLDivElement>,
@ -143,6 +181,28 @@ function _Connect(): JSX.Element | null {
});
}, [backendList]);
const addMenuProps = useMemo(() => {
const items = [];
items.push({
key: 'websocket',
text: 'WebSocket',
onClick: addWsBackend,
});
if (AdbDirectSocketsBackend.isSupported()) {
items.push({
key: 'direct-sockets',
text: 'Direct Sockets TCP',
onClick: addTcpBackend,
});
}
return {
items,
};
}, []);
return (
<Stack
tokens={{ childrenGap: 8, padding: '0 0 8px 8px' }}
@ -158,10 +218,12 @@ function _Connect(): JSX.Element | null {
onChange={handleSelectedBackendChange}
/>
{!global.device ? (
{!global.device
? (
<Stack horizontal tokens={CommonStackTokens}>
<StackItem grow shrink>
<PrimaryButton
iconProps={{ iconName: Icons.PlugConnected }}
text="Connect"
disabled={!selectedBackend}
primary={!!selectedBackend}
@ -170,21 +232,26 @@ function _Connect(): JSX.Element | null {
/>
</StackItem>
<StackItem grow shrink>
<TooltipHost
content="WebADB can't connect to anything without your explicit permission."
>
<DefaultButton
text="Add device"
iconProps={{ iconName: Icons.AddCircle }}
text="Add"
split
splitButtonAriaLabel="Add other connection type"
menuProps={addMenuProps}
disabled={!supported}
primary={!selectedBackend}
styles={{ root: { width: '100%' } }}
onClick={requestAccess}
/>
</TooltipHost>
</StackItem>
</Stack>
) : (
<DefaultButton text="Disconnect" onClick={disconnect} />
)
: (
<DefaultButton
iconProps={{ iconName: Icons.PlugDisconnected }}
text="Disconnect"
onClick={disconnect}
/>
)}
<Dialog

View file

@ -3,9 +3,9 @@ import { AdbPacketInit } from '@yume-chan/adb';
import { decodeUtf8 } from '@yume-chan/adb-backend-webusb';
import { DisposableList } from '@yume-chan/event';
import { observer } from "mobx-react-lite";
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { global, logger } from "../state";
import { withDisplayName } from '../utils';
import { Icons, withDisplayName } from '../utils';
import { CommandBar } from './command-bar';
const classNames = mergeStyleSets({
@ -59,7 +59,7 @@ export const ToggleLogView = observer(() => {
return (
<IconButton
checked={global.logVisible}
iconProps={{ iconName: 'ChangeEntitlements' }}
iconProps={{ iconName: Icons.TextGrammarError }}
title="Toggle Log"
onClick={global.toggleLog}
/>
@ -120,9 +120,7 @@ export const LogView = observer(({
{
key: 'Copy',
text: 'Copy',
iconProps: {
iconName: 'Copy',
},
iconProps: { iconName: Icons.Copy },
onClick: () => {
setPackets(lines => {
window.navigator.clipboard.writeText(lines.join('\r'));
@ -133,9 +131,7 @@ export const LogView = observer(({
{
key: 'Clear',
text: 'Clear',
iconProps: {
iconName: 'Delete',
},
iconProps: { iconName: Icons.Delete },
onClick: () => {
setPackets([]);
},

View file

@ -9,13 +9,16 @@
"lint": "next lint"
},
"dependencies": {
"@fluentui/font-icons-mdl2": "^8.1.14",
"@fluentui/react": "^8.36.3",
"@fluentui/react-file-type-icons": "^8.4.3",
"@fluentui/react-hooks": "^8.3.4",
"@fluentui/react-icons": "^2.0.154-beta.5",
"@fluentui/react-make-styles": "^9.0.0-beta.3",
"@yume-chan/adb": "^0.0.9",
"@yume-chan/adb-backend-direct-sockets": "^0.0.9",
"@yume-chan/adb-backend-webusb": "^0.0.9",
"@yume-chan/adb-backend-ws": "^0.0.9",
"@yume-chan/adb-credential-web": "^0.0.9",
"@yume-chan/async": "^2.1.4",
"@yume-chan/event": "^0.0.9",
"@yume-chan/scrcpy": "^0.0.9",

View file

@ -1,40 +1,49 @@
import { IComponentAsProps, IconButton, INavButtonProps, initializeIcons, mergeStyles, mergeStyleSets, Nav, Stack, StackItem } from "@fluentui/react";
import { IComponentAsProps, IconButton, INavButtonProps, mergeStyles, mergeStyleSets, Nav, registerIcons, Stack, StackItem } from "@fluentui/react";
import type { AppProps } from 'next/app';
import Link from 'next/link';
import { useRouter } from 'next/router';
import React, { useCallback, useEffect, useState } from "react";
import { Connect, ErrorDialogProvider, LogView, ToggleLogView } from "../components";
import { register } from '../utils/icons';
import '../styles/globals.css';
import { Icons } from "../utils";
initializeIcons();
register();
const ROUTES = [
{
url: '/',
icon: Icons.Bookmark,
name: 'README',
},
{
url: '/device-info',
icon: Icons.Phone,
name: 'Device Info',
},
{
url: '/file-manager',
icon: Icons.Folder,
name: 'File Manager',
},
{
url: '/framebuffer',
icon: Icons.Camera,
name: 'Screen Capture',
},
{
url: '/shell',
icon: Icons.WindowConsole,
name: 'Interactive Shell',
},
{
url: '/scrcpy',
icon: Icons.PhoneLaptop,
name: 'Scrcpy',
},
{
url: '/tcpip',
icon: Icons.WifiSettings,
name: 'ADB over WiFi',
},
];
@ -62,7 +71,7 @@ function MyApp({ Component, pageProps }: AppProps) {
textAlign: 'center',
},
'left-column': {
width: 250,
width: 270,
paddingRight: 8,
borderRight: '1px solid rgb(243, 242, 241)',
overflow: 'auto',
@ -89,7 +98,7 @@ function MyApp({ Component, pageProps }: AppProps) {
<IconButton
checked={leftPanelVisible}
title="Toggle Menu"
iconProps={{ iconName: 'GlobalNavButton' }}
iconProps={{ iconName: Icons.Navigation }}
onClick={toggleLeftPanel}
/>

View file

@ -6,7 +6,7 @@ import Head from 'next/head';
import React from "react";
import { ExternalLink } from "../components";
import { global } from '../state';
import { RouteStackProps } from "../utils";
import { Icons, RouteStackProps } from "../utils";
const KNOWN_FEATURES: Record<string, string> = {
'shell_v2': `"shell" command now supports separating child process's stdout and stderr, and returning exit code`,
@ -93,7 +93,7 @@ const DeviceInfo: NextPage = () => {
<span>{feature}</span>
{KNOWN_FEATURES[feature] && (
<TooltipHost content={<span>{KNOWN_FEATURES[feature]}</span>}>
<Icon style={{ marginLeft: 4 }} iconName="Unknown" />
<Icon style={{ marginLeft: 4 }} iconName={Icons.Info} />
</TooltipHost>
)}
</span>

View file

@ -2,7 +2,7 @@ import { Breadcrumb, concatStyleSets, ContextualMenu, ContextualMenuItem, Detail
import { FileIconType, getFileTypeIconProps, initializeFileTypeIcons } from '@fluentui/react-file-type-icons';
import { useConst } from '@fluentui/react-hooks';
import { AdbSyncEntryResponse, AdbSyncMaxPacketSize, LinuxFileType } from '@yume-chan/adb';
import { autorun, makeAutoObservable, observable, runInAction } from "mobx";
import { action, autorun, makeAutoObservable, observable, runInAction } from "mobx";
import { observer } from "mobx-react-lite";
import { NextPage } from "next";
import Head from "next/head";
@ -11,7 +11,7 @@ import path from 'path';
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { CommandBar } from '../components';
import { global } from '../state';
import { asyncEffect, chunkFile, formatSize, formatSpeed, pickFile, RouteStackProps, useSpeed } from '../utils';
import { asyncEffect, chunkFile, formatSize, formatSpeed, Icons, pickFile, RouteStackProps, useSpeed } from '../utils';
let StreamSaver: typeof import('streamsaver');
if (typeof window !== 'undefined') {
@ -93,6 +93,16 @@ class FileManagerState {
sortDescending = false;
startItemIndexInView = 0;
uploading = false;
uploadPath: string | undefined = undefined;
uploadedSize = 0;
uploadTotalSize = 0;
debouncedUploadedSize = 0;
uploadSpeed = 0;
selectedItems: ListItem[] = [];
contextMenuTarget: MouseEvent | undefined = undefined;
get breadcrumbItems(): IBreadcrumbItem[] {
let part = '';
const list: IBreadcrumbItem[] = this.path.split('/').filter(Boolean).map(segment => {
@ -118,6 +128,95 @@ class FileManagerState {
return list;
}
get menuItems() {
let result: IContextualMenuItem[] = [];
switch (this.selectedItems.length) {
case 0:
result.push({
key: 'upload',
text: 'Upload',
iconProps: {
iconName: Icons.CloudArrowUp,
style: { height: 20, fontSize: 20, lineHeight: 1.5 }
},
disabled: !global.device,
onClick() {
(async () => {
const files = await pickFile({ multiple: true });
for (let i = 0; i < files.length; i++) {
const file = files.item(i)!;
await state.upload(file);
}
})();
return false;
}
});
break;
case 1:
if (this.selectedItems[0].type === LinuxFileType.File) {
result.push({
key: 'download',
text: 'Download',
iconProps: {
iconName: Icons.CloudArrowDown,
style: { height: 20, fontSize: 20, lineHeight: 1.5 }
},
onClick() {
(async () => {
const sync = await global.device!.sync();
try {
const itemPath = path.resolve(state.path, this.selectedItems[0].name!);
const readableStream = createReadableStreamFromBufferIterator(sync.read(itemPath));
const writeableStream = StreamSaver!.createWriteStream(this.selectedItems[0].name!, {
size: this.selectedItems[0].size,
});
await readableStream.pipeTo(writeableStream);
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
sync.dispose();
}
})();
return false;
},
});
}
default:
result.push({
key: 'delete',
text: 'Delete',
iconProps: {
iconName: Icons.Delete,
style: { height: 20, fontSize: 20, lineHeight: 1.5 }
},
onClick() {
(async () => {
try {
for (const item of this.selectedItems) {
const output = await global.device!.rm(path.resolve(state.path, item.name!));
if (output) {
global.showErrorDialog(output);
return;
}
}
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
state.loadFiles();
}
})();
return false;
}
});
break;
}
return result;
}
get sortedList() {
const list = this.items.slice();
list.sort((a, b) => {
@ -153,7 +252,7 @@ class FileManagerState {
{
key: 'type',
name: 'File Type',
iconName: 'Page',
iconName: Icons.Document20,
isIconOnly: true,
minWidth: 20,
maxWidth: 20,
@ -342,10 +441,73 @@ class FileManagerState {
sync.dispose();
}
});
upload = async (file: File) => {
const sync = await global.device!.sync();
try {
const itemPath = path.resolve(state.path!, file.name);
runInAction(() => {
this.uploading = true;
this.uploadPath = file.name;
this.uploadedSize = 0;
this.uploadTotalSize = file.size;
this.debouncedUploadedSize = 0;
this.uploadSpeed = 0;
});
const intervalId = setInterval(action(() => {
this.uploadSpeed = this.uploadedSize - this.debouncedUploadedSize;
this.debouncedUploadedSize = this.uploadedSize;
}), 1000);
try {
await sync.write(
itemPath,
chunkFile(file, AdbSyncMaxPacketSize),
(LinuxFileType.File << 12) | 0o666,
file.lastModified / 1000,
action((uploaded) => {
this.uploadedSize = uploaded;
}),
);
runInAction(() => {
this.uploadSpeed = this.uploadedSize - this.debouncedUploadedSize;
this.debouncedUploadedSize = this.uploadedSize;
});
} finally {
clearInterval(intervalId);
}
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
sync.dispose();
state.loadFiles();
runInAction(() => {
this.uploading = false;
});
}
};
}
const state = new FileManagerState();
const UploadDialog = observer(() => {
return (
<Dialog
hidden={!state.uploading}
dialogContentProps={{
title: 'Uploading...',
subText: state.uploadPath
}}
>
<ProgressIndicator
description={formatSpeed(state.debouncedUploadedSize, state.uploadTotalSize, state.uploadSpeed)}
percentComplete={state.uploadedSize / state.uploadTotalSize}
/>
</Dialog>
);
});
const FileManager: NextPage = (): JSX.Element | null => {
useEffect(() => {
runInAction(() => {
@ -425,141 +587,34 @@ const FileManager: NextPage = (): JSX.Element | null => {
}
}, [previewImage]);
const [selectedItems, setSelectedItems] = useState<ListItem[]>([]);
const selection = useConst(() => new Selection({
onSelectionChanged() {
const selectedItems = selection.getSelection() as ListItem[];
setSelectedItems(selectedItems);
runInAction(() => {
state.selectedItems = selectedItems;
});
},
}));
const [uploading, setUploading] = useState(false);
const [uploadPath, setUploadPath] = useState('');
const [uploadedSize, setUploadedSize] = useState(0);
const [uploadTotalSize, setUploadTotalSize] = useState(0);
const [debouncedUploadedSize, uploadSpeed] = useSpeed(uploadedSize, uploadTotalSize);
const upload = useCallback(async (file: File) => {
const sync = await global.device!.sync();
try {
const itemPath = path.resolve(state.path!, file.name);
setUploading(true);
setUploadPath(file.name);
setUploadTotalSize(file.size);
await sync.write(
itemPath,
chunkFile(file, AdbSyncMaxPacketSize),
(LinuxFileType.File << 12) | 0o666,
file.lastModified / 1000,
setUploadedSize,
);
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
sync.dispose();
state.loadFiles();
setUploading(false);
}
}, []);
const [menuItems, setMenuItems] = useState<IContextualMenuItem[]>([]);
useEffect(() => {
let result: IContextualMenuItem[] = [];
switch (selectedItems.length) {
case 0:
result.push({
key: 'upload',
text: 'Upload',
iconProps: { iconName: 'Upload' },
disabled: !global.device,
onClick() {
(async () => {
const files = await pickFile({ multiple: true });
for (let i = 0; i < files.length; i++) {
const file = files.item(i)!;
await upload(file);
}
})();
return false;
}
});
break;
case 1:
if (selectedItems[0].type === LinuxFileType.File) {
result.push({
key: 'download',
text: 'Download',
iconProps: { iconName: 'Download' },
onClick() {
(async () => {
const sync = await global.device!.sync();
try {
const itemPath = path.resolve(state.path, selectedItems[0].name!);
const readableStream = createReadableStreamFromBufferIterator(sync.read(itemPath));
const writeableStream = StreamSaver!.createWriteStream(selectedItems[0].name!, {
size: selectedItems[0].size,
});
await readableStream.pipeTo(writeableStream);
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
sync.dispose();
}
})();
return false;
},
});
}
default:
result.push({
key: 'delete',
text: 'Delete',
iconProps: { iconName: 'Delete' },
onClick() {
(async () => {
try {
for (const item of selectedItems) {
const output = await global.device!.rm(path.resolve(state.path, item.name!));
if (output) {
global.showErrorDialog(output);
return;
}
}
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
} finally {
state.loadFiles();
}
})();
return false;
}
});
break;
}
setMenuItems(result);
}, [selectedItems, upload]);
const [contextMenuTarget, setContextMenuTarget] = useState<MouseEvent>();
const showContextMenu = useCallback((
_item?: AdbSyncEntryResponse,
_index?: number,
item?: AdbSyncEntryResponse,
index?: number,
e?: Event
) => {
if (!e) {
return false;
}
if (menuItems.length) {
setContextMenuTarget(e as MouseEvent);
if (state.menuItems.length) {
runInAction(() => {
state.contextMenuTarget = e as MouseEvent;
});
}
return false;
}, [menuItems]);
}, []);
const hideContextMenu = useCallback(() => {
setContextMenuTarget(undefined);
runInAction(() => state.contextMenuTarget = undefined);
}, []);
return (
@ -568,7 +623,7 @@ const FileManager: NextPage = (): JSX.Element | null => {
<title>File Manager - WebADB</title>
</Head>
<CommandBar items={menuItems} />
<CommandBar items={state.menuItems} />
<StackItem grow styles={{
root: {
@ -609,26 +664,15 @@ const FileManager: NextPage = (): JSX.Element | null => {
)}
<ContextualMenu
items={menuItems}
hidden={!contextMenuTarget}
items={state.menuItems}
hidden={!state.contextMenuTarget}
directionalHint={DirectionalHint.bottomLeftEdge}
target={contextMenuTarget}
target={state.contextMenuTarget}
onDismiss={hideContextMenu}
contextualMenuItemAs={props => <ContextualMenuItem {...props} hasIcons={false} />}
/>
<Dialog
hidden={!uploading}
dialogContentProps={{
title: 'Uploading...',
subText: uploadPath
}}
>
<ProgressIndicator
description={formatSpeed(debouncedUploadedSize, uploadTotalSize, uploadSpeed)}
percentComplete={uploadedSize / uploadTotalSize}
/>
</Dialog>
<UploadDialog />
</StackItem>
</Stack>
);

View file

@ -7,7 +7,7 @@ import Head from "next/head";
import React, { useCallback, useEffect, useRef } from 'react';
import { CommandBar, DemoMode, DeviceView } from '../components';
import { global } from "../state";
import { RouteStackProps } from "../utils";
import { Icons, RouteStackProps } from "../utils";
class FrameBufferState {
width = 0;
@ -43,9 +43,7 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
}
try {
const start = window.performance.now();
const framebuffer = await global.device.framebuffer();
const end = window.performance.now();
state.setImage(framebuffer);
} catch (e) {
global.showErrorDialog(e instanceof Error ? e.message : `${e}`);
@ -68,14 +66,14 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
{
key: 'start',
disabled: !global.device,
iconProps: { iconName: 'Camera' },
iconProps: { iconName: Icons.Camera, style: { height: 20, fontSize: 20, lineHeight: 1.5 } },
text: 'Capture',
onClick: capture,
},
{
key: 'Save',
disabled: !state.imageData,
iconProps: { iconName: 'Save' },
iconProps: { iconName: Icons.Save, style: { height: 20, fontSize: 20, lineHeight: 1.5 } },
text: 'Save',
onClick: () => {
const canvas = canvasRef.current;
@ -95,14 +93,14 @@ const FrameBuffer: NextPage = (): JSX.Element | null => {
const commandBarFarItems = computed((): ICommandBarItemProps[] => [
{
key: 'DemoMode',
iconProps: { iconName: 'Personalize' },
iconProps: { iconName: Icons.Wand, style: { height: 20, fontSize: 20, lineHeight: 1.5 } },
checked: state.demoModeVisible,
text: 'Demo Mode Settings',
onClick: state.toggleDemoModeVisible,
},
{
key: 'info',
iconProps: { iconName: 'Info' },
iconProps: { iconName: Icons.Info, style: { height: 20, fontSize: 20, lineHeight: 1.5 } },
iconOnly: true,
tooltipHostProps: {
content: 'Use ADB FrameBuffer command to capture a full-size, high-resolution screenshot.',

View file

@ -9,7 +9,7 @@ import Head from "next/head";
import React, { useEffect, useState } from "react";
import { DemoMode, DeviceView, DeviceViewRef, ExternalLink } from "../components";
import { global } from "../state";
import { CommonStackTokens, formatSpeed, RouteStackProps } from "../utils";
import { CommonStackTokens, formatSpeed, Icons, RouteStackProps } from "../utils";
const SERVER_URL = new URL('@yume-chan/scrcpy/bin/scrcpy-server?url', import.meta.url).toString();
@ -201,14 +201,14 @@ class ScrcpyPageState {
result.push({
key: 'start',
disabled: !global.device,
iconProps: { iconName: 'Play' },
iconProps: { iconName: Icons.Play },
text: 'Start',
onClick: this.start as VoidFunction,
});
} else {
result.push({
key: 'stop',
iconProps: { iconName: 'Stop' },
iconProps: { iconName: Icons.Stop },
text: 'Stop',
onClick: this.stop,
});
@ -217,7 +217,7 @@ class ScrcpyPageState {
result.push({
key: 'fullscreen',
disabled: !this.running,
iconProps: { iconName: 'Fullscreen' },
iconProps: { iconName: Icons.FullScreenMaximize },
text: 'Fullscreen',
onClick: () => { this.deviceView?.enterFullscreen(); },
});
@ -238,7 +238,7 @@ class ScrcpyPageState {
},
{
key: 'DemoMode',
iconProps: { iconName: 'Personalize' },
iconProps: { iconName: Icons.Wand },
checked: this.demoModeVisible,
text: 'Demo Mode Settings',
onClick: action(() => {
@ -247,7 +247,7 @@ class ScrcpyPageState {
},
{
key: 'info',
iconProps: { iconName: 'Info' },
iconProps: { iconName: Icons.Info },
iconOnly: true,
tooltipHostProps: {
content: (
@ -351,20 +351,50 @@ class ScrcpyPageState {
runInAction(() => {
this.serverTotalSize = 0;
this.serverDownloadedSize = 0;
this.debouncedServerDownloadedSize = 0;
this.serverUploadedSize = 0;
this.debouncedServerUploadedSize = 0;
this.connecting = true;
});
const serverBuffer = await fetchServer(action(([downloaded, total]) => {
let intervalId = setInterval(action(() => {
this.serverDownloadSpeed = this.serverDownloadedSize - this.debouncedServerDownloadedSize;
this.debouncedServerDownloadedSize = this.serverDownloadedSize;
}), 1000);
let serverBuffer: ArrayBuffer;
try {
serverBuffer = await fetchServer(action(([downloaded, total]) => {
this.serverDownloadedSize = downloaded;
this.serverTotalSize = total;
}));
runInAction(() => {
this.serverDownloadSpeed = this.serverDownloadedSize - this.debouncedServerDownloadedSize;
this.debouncedServerDownloadedSize = this.serverDownloadedSize;
});
} finally {
clearInterval(intervalId);
}
intervalId = setInterval(action(() => {
this.serverUploadSpeed = this.serverUploadedSize - this.debouncedServerUploadedSize;
this.debouncedServerUploadedSize = this.serverUploadedSize;
}), 1000);
try {
await pushServer(global.device, serverBuffer, {
onProgress: action((progress) => {
this.serverUploadedSize = progress;
}),
});
runInAction(() => {
this.serverUploadSpeed = this.serverUploadedSize - this.debouncedServerUploadedSize;
this.debouncedServerUploadedSize = this.serverUploadedSize;
});
} finally {
clearInterval(intervalId);
}
const encoders = await ScrcpyClient.getEncoders(
global.device,
@ -685,19 +715,19 @@ const Scrcpy: NextPage = () => {
<Stack verticalFill horizontalAlign="center" style={{ background: '#999' }}>
<Stack verticalFill horizontal style={{ width: '100%', maxWidth: 300 }} horizontalAlign="space-evenly" verticalAlign="center">
<IconButton
iconProps={{ iconName: 'Play' }}
iconProps={{ iconName: Icons.Play }}
style={{ transform: 'rotate(180deg)', color: 'white' }}
onPointerDown={state.handleBackPointerDown}
onPointerUp={state.handleBackPointerUp}
/>
<IconButton
iconProps={{ iconName: 'LocationCircle' }}
iconProps={{ iconName: Icons.Circle }}
style={{ color: 'white' }}
onPointerDown={state.handleHomePointerDown}
onPointerUp={state.handleHomePointerUp}
/>
<IconButton
iconProps={{ iconName: 'Stop' }}
iconProps={{ iconName: Icons.Stop }}
style={{ color: 'white' }}
onPointerDown={state.handleAppSwitchPointerDown}
onPointerUp={state.handleAppSwitchPointerUp}
@ -778,7 +808,7 @@ const Scrcpy: NextPage = () => {
<>
<span>Use forward connection{' '}</span>
<TooltipHost content="Old Android devices may not support reverse connection when using ADB over WiFi">
<Icon iconName="Info" />
<Icon iconName={Icons.Info} />
</TooltipHost>
</>
}

View file

@ -3,10 +3,10 @@ import { reaction } from "mobx";
import { observer } from "mobx-react-lite";
import { NextPage } from "next";
import Head from "next/head";
import React, { CSSProperties, useCallback, useContext, useEffect, useRef, useState } from 'react';
import React, { CSSProperties, useCallback, useEffect, useRef, useState } from 'react';
import 'xterm/css/xterm.css';
import { global } from "../state";
import { ResizeObserver, RouteStackProps } from '../utils';
import { Icons, ResizeObserver, RouteStackProps } from '../utils';
let terminal: import('../components/terminal').AdbTerminal;
if (typeof window !== 'undefined') {
@ -19,8 +19,8 @@ const ResizeObserverStyle: CSSProperties = {
height: '100%',
};
const UpIconProps = { iconName: 'ChevronUp' };
const DownIconProps = { iconName: 'ChevronDown' };
const UpIconProps = { iconName: Icons.ChevronUp };
const DownIconProps = { iconName: Icons.ChevronDown };
const Shell: NextPage = (): JSX.Element | null => {
const [searchKeyword, setSearchKeyword] = useState('');

View file

@ -6,7 +6,7 @@ import Head from "next/head";
import React, { useCallback, useEffect } from "react";
import { ExternalLink } from "../components";
import { global } from "../state";
import { asyncEffect, RouteStackProps } from "../utils";
import { asyncEffect, Icons, RouteStackProps } from "../utils";
class TcpIpState {
initial = true;
@ -42,14 +42,14 @@ class TcpIpState {
{
key: 'refresh',
disabled: !global.device,
iconProps: { iconName: 'Refresh' },
iconProps: { iconName: Icons.ArrowClockwise },
text: 'Refresh',
onClick: this.queryInfo as () => void,
onClick: this.queryInfo as VoidFunction,
},
{
key: 'apply',
disabled: !global.device,
iconProps: { iconName: 'Save' },
iconProps: { iconName: Icons.Save },
text: 'Apply',
onClick: this.applyServicePort,
}

80
apps/demo/utils/icons.tsx Normal file
View file

@ -0,0 +1,80 @@
import { registerIcons } from "@fluentui/react";
import { AddCircleRegular, ArrowClockwiseRegular, ArrowSortDownRegular, ArrowSortUpRegular, BookmarkRegular, CameraRegular, CheckmarkRegular, ChevronDownRegular, ChevronRightRegular, ChevronUpRegular, CircleRegular, CloudArrowDownRegular, CloudArrowUpRegular, CopyRegular, DeleteRegular, DocumentRegular, FolderRegular, FullScreenMaximizeRegular, InfoRegular, NavigationRegular, PhoneLaptopRegular, PhoneRegular, PlayRegular, PlugConnectedRegular, PlugDisconnectedRegular, SaveRegular, StopRegular, TextGrammarErrorRegular, WandRegular, WifiSettingsRegular, WindowConsoleRegular } from '@fluentui/react-icons';
const STYLE = {};
export function register() {
registerIcons({
icons: {
AddCircle: <AddCircleRegular style={STYLE} />,
ArrowClockwise: <ArrowClockwiseRegular style={STYLE} />,
Bookmark: <BookmarkRegular style={STYLE} />,
Camera: <CameraRegular style={STYLE} />,
ChevronDown: <ChevronDownRegular style={STYLE} />,
ChevronRight: <ChevronRightRegular style={STYLE} />,
ChevronUp: <ChevronUpRegular style={STYLE} />,
Circle: <CircleRegular style={STYLE} />,
Copy: <CopyRegular style={STYLE} />,
CloudArrowUp: <CloudArrowUpRegular style={STYLE} />,
CloudArrowDown: <CloudArrowDownRegular style={STYLE} />,
Delete: <DeleteRegular style={STYLE} />,
Document: <DocumentRegular style={STYLE} />,
Folder: <FolderRegular style={STYLE} />,
FullScreenMaximize: <FullScreenMaximizeRegular style={STYLE} />,
Info: <InfoRegular style={STYLE} />,
Navigation: <NavigationRegular style={STYLE} />,
Phone: <PhoneRegular style={STYLE} />,
PhoneLaptop: <PhoneLaptopRegular style={STYLE} />,
Play: <PlayRegular style={STYLE} />,
PlugConnected: <PlugConnectedRegular style={STYLE} />,
PlugDisconnected: <PlugDisconnectedRegular style={STYLE} />,
Save: <SaveRegular style={STYLE} />,
Stop: <StopRegular style={STYLE} />,
TextGrammarError: <TextGrammarErrorRegular style={STYLE} />,
Wand: <WandRegular style={STYLE} />,
WifiSettings: <WifiSettingsRegular style={STYLE} />,
WindowConsole: <WindowConsoleRegular style={STYLE} />,
StatusCircleCheckmark: <CheckmarkRegular style={STYLE} />,
ChevronUpSmall: <ChevronUpRegular style={STYLE} />,
ChevronDownSmall: <ChevronDownRegular style={STYLE} />,
CircleRing: <CircleRegular style={STYLE} />,
SortUp: <ArrowSortUpRegular style={STYLE} />,
SortDown: <ArrowSortDownRegular style={STYLE} />,
Document20: <DocumentRegular style={{ fontSize: 20, verticalAlign: 'middle' }} />
}
});
}
export default {
AddCircle: 'AddCircle',
ArrowClockwise: 'ArrowClockwise',
Bookmark: 'Bookmark',
Camera: 'Camera',
Copy: 'Copy',
Circle: 'Circle',
ChevronDown: 'ChevronDown',
ChevronRight: 'ChevronRight',
ChevronUp: 'ChevronUp',
CloudArrowUp: 'CloudArrowUp',
CloudArrowDown: 'CloudArrowDown',
Delete: 'Delete',
Document: 'Document',
Folder: 'Folder',
FullScreenMaximize: 'FullScreenMaximize',
Info: 'Info',
Navigation: 'Navigation',
Phone: 'Phone',
PhoneLaptop: 'PhoneLaptop',
Play: 'Play',
PlugConnected: 'PlugConnected',
PlugDisconnected: 'PlugDisconnected',
Save: 'Save',
Stop: 'Stop',
TextGrammarError: 'TextGrammarError',
Wand: 'Wand',
WifiSettings: 'WifiSettings',
WindowConsole: 'WindowConsole',
Document20: 'Document20'
};

View file

@ -1,6 +1,7 @@
export * from './async-effect';
export * from './file-size';
export * from './file';
export * from './file-size';
export { default as Icons } from './icons';
export * from './resize-observer';
export * from './with-display-name';
export * from './styles';
export * from './with-display-name';

View file

@ -1,16 +1,19 @@
dependencies:
'@docusaurus/core': 2.0.0-beta.3_react-dom@17.0.2+react@17.0.2
'@docusaurus/preset-classic': 2.0.0-beta.3_12a6012245369fd5be825566be975ff0
'@fluentui/font-icons-mdl2': 8.1.14_b094b78811fc8d2f00a90f13d0251fb6
'@fluentui/react': 8.36.3_12a6012245369fd5be825566be975ff0
'@fluentui/react-file-type-icons': 8.4.3_b094b78811fc8d2f00a90f13d0251fb6
'@fluentui/react-hooks': 8.3.4_b094b78811fc8d2f00a90f13d0251fb6
'@fluentui/react-icons': 2.0.154-beta.5_d2f1067edc0bfed1f9bdc78013a98cbb
'@fluentui/react-make-styles': 9.0.0-beta.4_12a6012245369fd5be825566be975ff0
'@mdx-js/loader': 1.6.22_react@17.0.2
'@mdx-js/react': 1.6.22_react@17.0.2
'@next/mdx': 11.1.2_f56c41adb6190c4680be4a1c0222355d
'@rush-temp/adb': file:projects/adb.tgz
'@rush-temp/adb-backend-direct-sockets': file:projects/adb-backend-direct-sockets.tgz
'@rush-temp/adb-backend-webusb': file:projects/adb-backend-webusb.tgz
'@rush-temp/adb-backend-ws': file:projects/adb-backend-ws.tgz
'@rush-temp/adb-credential-web': file:projects/adb-credential-web.tgz
'@rush-temp/demo': file:projects/demo.tgz_@mdx-js+react@1.6.22
'@rush-temp/event': file:projects/event.tgz
'@rush-temp/scrcpy': file:projects/scrcpy.tgz
@ -2079,6 +2082,10 @@ packages:
node: '>=12.13.0'
resolution:
integrity: sha512-DApc6xcb3CvvsBCfRU6Zk3KoZa4mZfCJA4XRv5zhlhaSb0GFuAo7KQ353RUu6d0eYYylY3GGRABXkxRE1SEClA==
/@emotion/hash/0.8.0:
dev: false
resolution:
integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
/@eslint/eslintrc/0.4.3:
dependencies:
ajv: 6.12.6
@ -2141,6 +2148,21 @@ packages:
dev: false
resolution:
integrity: sha512-pVY2m3IC5+LLmMzsaPApX9eKTzpOzdgQwrR3FNTE6mGx3N/+QWYM7fdF+T1ldZQt87dCRSeQnmAo5kqjtxeA/w==
/@fluentui/keyboard-keys/9.0.0-beta.1:
dev: false
resolution:
integrity: sha512-eGKEzdyIep7KZPoKwHVfcDR3hKuSnI0iDqZGYrIOVWYVr1aD9uZ3JXFK2ad8DZtOGnxgK3/pOvAfqFo61LoRjQ==
/@fluentui/make-styles/9.0.0-beta.3:
dependencies:
'@emotion/hash': 0.8.0
csstype: 2.6.19
inline-style-expand-shorthand: 1.3.0
rtl-css-js: 1.15.0
stylis: 4.0.13
tslib: 2.3.1
dev: false
resolution:
integrity: sha512-Bwe3BTLC5ATNcQdUk0U4s+eKU7OlV0UF4dlVjnlNYQefMfTWfr/ozZa3IczATOfYKyYPad1KOM3SBUojUdaoog==
/@fluentui/merge-styles/8.2.0:
dependencies:
'@fluentui/set-version': 8.1.4
@ -2191,6 +2213,69 @@ packages:
react: '>=16.8.0 <18.0.0'
resolution:
integrity: sha512-uWaNalWpgsjDaPki4rXzEJAbdOTy1HH+Kwo2Olfyxfm0geePWBnCjKikXI1xIaS/o47ew2B4TQyvR4ShvudOmA==
/@fluentui/react-icons/2.0.154-beta.5_d2f1067edc0bfed1f9bdc78013a98cbb:
dependencies:
'@fluentui/react-make-styles': 9.0.0-beta.4_12a6012245369fd5be825566be975ff0
dev: false
peerDependencies:
'@fluentui/react-make-styles': ^9.0.0-beta.3
resolution:
integrity: sha512-U+bdvX1BZELUEN5MK4BX7H35p92Kyt/M+26PWJG/gvc2KCgpfFTFedBXjABKEf1Jo8wIcQl8VWciKSkpzhUaQw==
/@fluentui/react-make-styles/9.0.0-beta.4_12a6012245369fd5be825566be975ff0:
dependencies:
'@fluentui/make-styles': 9.0.0-beta.3
'@fluentui/react-shared-contexts': 9.0.0-beta.4_12a6012245369fd5be825566be975ff0
'@fluentui/react-theme': 9.0.0-beta.4_12a6012245369fd5be825566be975ff0
'@fluentui/react-utilities': 9.0.0-beta.4_b094b78811fc8d2f00a90f13d0251fb6
'@types/react': 17.0.27
react: 17.0.2
tslib: 2.3.1
dev: false
peerDependencies:
'@types/react': '>=16.8.0 <18.0.0'
react: '>=16.8.0 <18.0.0'
react-dom: '*'
resolution:
integrity: sha512-z6yy4xKc8+4YgmsCqluTHNwVyCoG5EuQauIOaeT8kQVvV+vtxTSTqqj9nzG9uknGu29zlwqSzqfI4U5WKEFHyA==
/@fluentui/react-shared-contexts/9.0.0-beta.4_12a6012245369fd5be825566be975ff0:
dependencies:
'@fluentui/react-theme': 9.0.0-beta.4_12a6012245369fd5be825566be975ff0
'@types/react': 17.0.27
react: 17.0.2
tslib: 2.3.1
dev: false
peerDependencies:
'@types/react': '>=16.8.0 <18.0.0'
react: '>=16.8.0 <18.0.0'
react-dom: '*'
resolution:
integrity: sha512-I4Wb0KHQfDAdWMXb+Af8LvmEvsnzMZ4Oky0ubxNTlueALSdwUE4mVj7dKEasCNESdxR0wXm8YooWh1xCX9WG7w==
/@fluentui/react-theme/9.0.0-beta.4_12a6012245369fd5be825566be975ff0:
dependencies:
'@types/react': 17.0.27
react: 17.0.2
react-dom: 17.0.2_react@17.0.2
tslib: 2.3.1
dev: false
peerDependencies:
'@types/react': '>=16.8.0 <18.0.0'
'@types/react-dom': '>=16.8.0 <18.0.0'
react: '>=16.8.0 <18.0.0'
react-dom: '>=16.8.0 <18.0.0'
resolution:
integrity: sha512-dfmfZFgb03d43ZguIKbQIGuKEhyYUDNJLheLLV/1CdzZWHxayxEpHofILc6S7L7KVDIhZPniOSjgxmn+R3kzuw==
/@fluentui/react-utilities/9.0.0-beta.4_b094b78811fc8d2f00a90f13d0251fb6:
dependencies:
'@fluentui/keyboard-keys': 9.0.0-beta.1
'@types/react': 17.0.27
react: 17.0.2
tslib: 2.3.1
dev: false
peerDependencies:
'@types/react': '>=16.8.0 <18.0.0'
react: '>=16.8.0 <18.0.0'
resolution:
integrity: sha512-b5drXcG02SNHokC39/B19dt1cti0ErtyS94ymMl1ppZ/Lh02/3VYukYsTBXBpKim4zmALAxqGkx/yXm95A7UBQ==
/@fluentui/react-window-provider/2.1.4_b094b78811fc8d2f00a90f13d0251fb6:
dependencies:
'@fluentui/set-version': 8.1.4
@ -5301,6 +5386,10 @@ packages:
node: '>=8'
resolution:
integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==
/csstype/2.6.19:
dev: false
resolution:
integrity: sha512-ZVxXaNy28/k3kJg0Fou5MiYpp88j7H9hLZp8PDC3jV0WFjfH5E9xHb56L0W59cPbKbcHXeP4qyT8PrHp8t6LcQ==
/csstype/3.0.8:
dev: false
resolution:
@ -7822,6 +7911,10 @@ packages:
node: '>=10'
resolution:
integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
/inline-style-expand-shorthand/1.3.0:
dev: false
resolution:
integrity: sha512-cYW3cf2Tzi43jjHk8yyHAAnwgVXOC0jdmv7QkHMmha2zI2znhWh8LEC+Enb+PHcZi9afsbcP4JHyr5C08jDRHA==
/inline-style-parser/0.1.1:
dev: false
resolution:
@ -11957,6 +12050,12 @@ packages:
node: 6.* || >= 7.*
resolution:
integrity: sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==
/rtl-css-js/1.15.0:
dependencies:
'@babel/runtime': 7.16.3
dev: false
resolution:
integrity: sha512-99Cu4wNNIhrI10xxUaABHsdDqzalrSRTie4GeCmbGVuehm4oj+fIy8fTzB+16pmKe8Bv9rl+hxIBez6KxExTew==
/rtl-detect/1.0.3:
dev: false
resolution:
@ -12856,6 +12955,10 @@ packages:
dev: false
resolution:
integrity: sha512-8/3pSmthWM7lsPBKv7NXkzn2Uc9W7NotcwGNpJaa3k7WMM1XDCA4MgT5k/8BIexd5ydZdboXtU90XH9Ec4Bv/Q==
/stylis/4.0.13:
dev: false
resolution:
integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
/supports-color/5.5.0:
dependencies:
has-flag: 3.0.0
@ -14326,6 +14429,17 @@ packages:
dev: false
resolution:
integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==
file:projects/adb-backend-direct-sockets.tgz:
dependencies:
'@yume-chan/async': 2.1.4
tslib: 2.3.1
typescript: 4.4.3
dev: false
name: '@rush-temp/adb-backend-direct-sockets'
resolution:
integrity: sha512-To21ySnzlUUAcBspOH3tqG4KH7e8evt+K2/D3kYZOCdgQ9P/y9jGHjm/loWzFj82/9h2OGIzM9rYF9gege8nkQ==
tarball: file:projects/adb-backend-direct-sockets.tgz
version: 0.0.0
file:projects/adb-backend-webusb.tgz:
dependencies:
'@types/w3c-web-usb': 1.0.5
@ -14349,6 +14463,17 @@ packages:
integrity: sha512-gmOeTpe51r+uwYeeMuPIQ6LU2+DYXJbirNXafLgfLjpWWWH8sCblZADZlHLzI/wte6RfabrecbyTgxoTtJ6rvA==
tarball: file:projects/adb-backend-ws.tgz
version: 0.0.0
file:projects/adb-credential-web.tgz:
dependencies:
'@yume-chan/async': 2.1.4
tslib: 2.3.1
typescript: 4.4.3
dev: false
name: '@rush-temp/adb-credential-web'
resolution:
integrity: sha512-X7CGrXjmZMJZ1eodb8OPi7Gpf4Td6E82tVGmElOGyHGZQfQUAbquFZEA90Xy3fMkOEL044erE0z2K50FT1XgIA==
tarball: file:projects/adb-credential-web.tgz
version: 0.0.0
file:projects/adb.tgz:
dependencies:
'@yume-chan/async': 2.1.4
@ -14367,6 +14492,8 @@ packages:
'@fluentui/react': 8.36.3_12a6012245369fd5be825566be975ff0
'@fluentui/react-file-type-icons': 8.4.3_b094b78811fc8d2f00a90f13d0251fb6
'@fluentui/react-hooks': 8.3.4_b094b78811fc8d2f00a90f13d0251fb6
'@fluentui/react-icons': 2.0.154-beta.5_d2f1067edc0bfed1f9bdc78013a98cbb
'@fluentui/react-make-styles': 9.0.0-beta.4_12a6012245369fd5be825566be975ff0
'@mdx-js/loader': 1.6.22_react@17.0.2
'@next/mdx': 11.1.2_f56c41adb6190c4680be4a1c0222355d
'@types/dom-webcodecs': 0.1.2
@ -14398,7 +14525,7 @@ packages:
peerDependencies:
'@mdx-js/react': '*'
resolution:
integrity: sha512-tq3k2RWwrKsKzVKXGjy+/z9ROfpdF6OMYdFKonBDgfuOe9HUqke5EUlO54BVvLO68v+X8UvVu3o+5X/RW16scg==
integrity: sha512-cvSHC3mz5ixtrLSqs8IsDxT8pMx30CevOxdniisAx4zRBJPjmFNN2CH6jNY0ir5xy18pDM0NhJGKj+EGIvbWWQ==
tarball: file:projects/demo.tgz
version: 0.0.0
file:projects/event.tgz:
@ -14484,16 +14611,19 @@ packages:
specifiers:
'@docusaurus/core': ^2.0.0-beta.0
'@docusaurus/preset-classic': ^2.0.0-beta.0
'@fluentui/font-icons-mdl2': ^8.1.14
'@fluentui/react': ^8.36.3
'@fluentui/react-file-type-icons': ^8.4.3
'@fluentui/react-hooks': ^8.3.4
'@fluentui/react-icons': ^2.0.154-beta.5
'@fluentui/react-make-styles': ^9.0.0-beta.3
'@mdx-js/loader': ^1.6.22
'@mdx-js/react': ^1.6.21
'@next/mdx': ^11.1.2
'@rush-temp/adb': file:./projects/adb.tgz
'@rush-temp/adb-backend-direct-sockets': file:./projects/adb-backend-direct-sockets.tgz
'@rush-temp/adb-backend-webusb': file:./projects/adb-backend-webusb.tgz
'@rush-temp/adb-backend-ws': file:./projects/adb-backend-ws.tgz
'@rush-temp/adb-credential-web': file:./projects/adb-credential-web.tgz
'@rush-temp/demo': file:./projects/demo.tgz
'@rush-temp/event': file:./projects/event.tgz
'@rush-temp/scrcpy': file:./projects/scrcpy.tgz

View file

@ -0,0 +1,14 @@
.rush
# Test
coverage
**/*.spec.ts
**/*.spec.js
**/*.spec.js.map
**/__helpers__
jest.config.js
tsconfig.json
# Logs
*.log

View file

@ -0,0 +1,3 @@
# `@yume-chan/adb-backend-direct-sockets`
Use Direct Sockets API for plugin-free ADB over WiFi connection.

View file

@ -0,0 +1,36 @@
{
"name": "@yume-chan/adb-backend-direct-sockets",
"private": true,
"version": "0.0.9",
"description": "Backend for `@yume-chan/adb` using Direct Sockets API.",
"keywords": [
"adb"
],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"main": "cjs/index.js",
"module": "esm/index.js",
"types": "dts/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"build": "build-ts-package",
"build:watch": "build-ts-package --incremental"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"typescript": "^4.4.3",
"@yume-chan/ts-package-builder": "^1.0.0"
},
"dependencies": {
"@yume-chan/adb": "^0.0.9",
"@yume-chan/async": "^2.1.4",
"@yume-chan/event": "^0.0.9",
"tslib": "^2.3.1"
}
}

View file

@ -0,0 +1,111 @@
import { AdbBackend } from '@yume-chan/adb';
import { EventEmitter } from '@yume-chan/event';
const Utf8Encoder = new TextEncoder();
const Utf8Decoder = new TextDecoder();
declare global {
interface TCPSocket {
close(): Promise<void>;
readonly remoteAddress: string;
readonly remotePort: number;
readonly readable: ReadableStream;
readonly writable: WritableStream;
}
interface SocketOptions {
localAddress?: string | undefined;
localPort?: number | undefined;
remoteAddress: string;
remotePort: number;
sendBufferSize?: number;
receiveBufferSize?: number;
keepAlive?: number;
noDelay?: boolean;
}
interface Navigator {
openTCPSocket(options?: SocketOptions): Promise<TCPSocket>;
}
}
export function encodeUtf8(input: string): ArrayBuffer {
return Utf8Encoder.encode(input).buffer;
}
export function decodeUtf8(buffer: ArrayBuffer): string {
return Utf8Decoder.decode(buffer);
}
export default class AdbDirectSocketsBackend implements AdbBackend {
public static isSupported(): boolean {
return typeof window !== 'undefined' && !!window.navigator?.openTCPSocket;
}
public readonly serial: string;
public readonly address: string;
public readonly port: number;
public name: string | undefined;
private socket: TCPSocket | undefined;
private reader: ReadableStreamDefaultReader<ArrayBuffer> | undefined;
private writer: WritableStreamDefaultWriter<ArrayBuffer> | undefined;
private _connected = false;
public get connected() { return this._connected; }
private readonly disconnectEvent = new EventEmitter<void>();
public readonly onDisconnected = this.disconnectEvent.event;
public constructor(address: string, port: number = 5555, name?: string) {
this.address = address;
this.port = port;
this.serial = `${address}:${port}`;
this.name = name;
}
public async connect() {
const socket = await navigator.openTCPSocket({
remoteAddress: this.address,
remotePort: this.port,
noDelay: true,
});
this.socket = socket;
this.reader = this.socket.readable.getReader();
this.writer = this.socket.writable.getWriter();
this._connected = true;
}
public encodeUtf8(input: string): ArrayBuffer {
return encodeUtf8(input);
}
public decodeUtf8(buffer: ArrayBuffer): string {
return decodeUtf8(buffer);
}
public write(buffer: ArrayBuffer): Promise<void> {
return this.writer!.write(buffer);
}
public async read(length: number): Promise<ArrayBuffer> {
const result = await this.reader!.read();
if (result.value) {
return result.value;
}
throw new Error('Stream ended');
}
public dispose(): void | Promise<void> {
this.socket?.close();
}
}

View file

@ -0,0 +1,14 @@
{
"extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json",
"compilerOptions": {
"lib": [
"ESNext",
"DOM"
],
},
"references": [
{
"path": "../adb/tsconfig.json"
}
]
}

View file

@ -1,4 +1,3 @@
export * from './auth';
export * from './backend';
export { AdbWebUsbBackend as default } from './backend';
export * from './utils';

View file

@ -0,0 +1,3 @@
# `@yume-chan/adb-credential-web`
Store ADB credentials in LocalStorage.

View file

@ -0,0 +1,36 @@
{
"name": "@yume-chan/adb-credential-web",
"private": true,
"version": "0.0.9",
"description": "Credential Store for `@yume-chan/adb` using Web LocalStorage API.",
"keywords": [
"adb"
],
"author": "Simon Chan <cnsimonchan@live.com>",
"homepage": "https://github.com/yume-chan/ya-webadb#readme",
"license": "MIT",
"main": "cjs/index.js",
"module": "esm/index.js",
"types": "dts/index.d.ts",
"repository": {
"type": "git",
"url": "git+https://github.com/yume-chan/ya-webadb.git"
},
"scripts": {
"build": "build-ts-package",
"build:watch": "build-ts-package --incremental"
},
"bugs": {
"url": "https://github.com/yume-chan/ya-webadb/issues"
},
"devDependencies": {
"typescript": "^4.4.3",
"@yume-chan/ts-package-builder": "^1.0.0"
},
"dependencies": {
"@yume-chan/adb": "^0.0.9",
"@yume-chan/async": "^2.1.4",
"@yume-chan/event": "^0.0.9",
"tslib": "^2.3.1"
}
}

View file

@ -1,7 +1,17 @@
import { AdbCredentialStore, calculateBase64EncodedLength, calculatePublicKey, calculatePublicKeyLength, decodeBase64, encodeBase64 } from "@yume-chan/adb";
import { decodeUtf8 } from "./utils";
export class AdbWebCredentialStore implements AdbCredentialStore {
const Utf8Encoder = new TextEncoder();
const Utf8Decoder = new TextDecoder();
export function encodeUtf8(input: string): ArrayBuffer {
return Utf8Encoder.encode(input).buffer;
}
export function decodeUtf8(buffer: ArrayBuffer): string {
return Utf8Decoder.decode(buffer);
}
export default class AdbWebCredentialStore implements AdbCredentialStore {
public readonly localStorageKey: string;
public constructor(localStorageKey = 'private-key') {
@ -44,5 +54,4 @@ export class AdbWebCredentialStore implements AdbCredentialStore {
return privateKey;
}
}

View file

@ -0,0 +1,14 @@
{
"extends": "./node_modules/@yume-chan/ts-package-builder/tsconfig.base.json",
"compilerOptions": {
"lib": [
"ESNext",
"DOM"
],
},
"references": [
{
"path": "../adb/tsconfig.json"
}
]
}

View file

@ -4,7 +4,6 @@
*/
{
"$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush.schema.json",
/**
* (Required) This specifies the version of the Rush engine to be used in this repo.
* Rush's "version selector" feature ensures that the globally installed tool will
@ -17,7 +16,6 @@
* correct error-underlining and tab-completion for editors such as VS Code.
*/
"rushVersion": "5.55.1",
/**
* The next field selects which package manager should be installed and determines its version.
* Rush installs its own local copy of the package manager to ensure that your build process
@ -27,10 +25,8 @@
* for details about these alternatives.
*/
"pnpmVersion": "5.15.2",
// "npmVersion": "4.5.0",
// "yarnVersion": "1.9.4",
/**
* Options that are only used when the PNPM package manager is selected
*/
@ -51,7 +47,6 @@
* The default value is "local".
*/
// "pnpmStore": "local",
/**
* If true, then Rush will add the "--strict-peer-dependencies" option when invoking PNPM.
* This causes "rush install" to fail if there are unsatisfied peer dependencies, which is
@ -63,7 +58,6 @@
* It is strongly recommended to set strictPeerDependencies=true.
*/
// "strictPeerDependencies": true,
/**
* Configures the strategy used to select versions during installation.
*
@ -77,7 +71,6 @@
* will recalculate all version selections.
*/
// "resolutionStrategy": "fast",
/**
* If true, then `rush install` will report an error if manual modifications
* were made to the PNPM shrinkwrap file without running "rush update" afterwards.
@ -95,7 +88,6 @@
* The default value is false.
*/
// "preventManualShrinkwrapChanges": true,
/**
* If true, then `rush install` will use the PNPM workspaces feature to perform the
* install.
@ -112,7 +104,6 @@
*/
// "useWorkspaces": true
},
/**
* Older releases of the Node.js engine may be missing features required by your system.
* Other releases may have bugs. In particular, the "latest" version will not be a
@ -125,7 +116,6 @@
* LTS versions: https://nodejs.org/en/download/releases/
*/
"nodeSupportedVersionRange": ">=12.13.0 <13.0.0 || >=14.15.0 <15.0.0",
/**
* Odd-numbered major versions of Node.js are experimental. Even-numbered releases
* spend six months in a stabilization period before the first Long Term Support (LTS) version.
@ -138,7 +128,6 @@
* to disable Rush's warning.
*/
// "suppressNodeLtsWarning": false,
/**
* If you would like the version specifiers for your dependencies to be consistent, then
* uncomment this line. This is effectively similar to running "rush check" before any
@ -151,7 +140,6 @@
* section of the common-versions.json.
*/
"ensureConsistentVersions": true,
/**
* Large monorepos can become intimidating for newcomers if project folder paths don't follow
* a consistent and recognizable pattern. When the system allows nested folder trees,
@ -177,7 +165,6 @@
*/
// "projectFolderMinDepth": 2,
// "projectFolderMaxDepth": 2,
/**
* Today the npmjs.com registry enforces fairly strict naming rules for packages, but in the early
* days there was no standard and hardly any enforcement. A few large legacy projects are still using
@ -190,7 +177,6 @@
* The default value is false.
*/
// "allowMostlyStandardPackageNames": true,
/**
* This feature helps you to review and approve new packages before they are introduced
* to your monorepo. For example, you may be concerned about licensing, code quality,
@ -226,7 +212,6 @@
// */
// // "ignoredNpmScopes": ["@types"]
// },
/**
* If you use Git as your version control system, this section has some additional
* optional features you can use.
@ -247,14 +232,12 @@
// "[^@]+@users\\.noreply\\.github\\.com",
// "travis@example\\.org"
// ],
/**
* When Rush reports that the address is malformed, the notice can include an example
* of a recommended email. Make sure it conforms to one of the allowedEmailRegExps
* expressions.
*/
// "sampleEmail": "mrexample@users.noreply.github.com",
/**
* The commit message to use when committing changes during 'rush publish'.
*
@ -263,7 +246,6 @@
* in the commit message, and then customize Rush's message to contain that string.
*/
// "versionBumpCommitMessage": "Applying package updates. [skip-ci]",
/**
* The commit message to use when committing changes during 'rush version'.
*
@ -273,7 +255,6 @@
*/
// "changeLogUpdateCommitMessage": "Deleting change files and updating change logs for package updates. [skip-ci]"
},
"repository": {
/**
* The URL of this Git repository, used by "rush change" to determine the base branch for your PR.
@ -291,20 +272,17 @@
* to retrieve the latest activity for the remote master branch.
*/
"url": "https://github.com/yume-chan/ya-webadb"
/**
* The default branch name. This tells "rush change" which remote branch to compare against.
* The default value is "master"
*/
// "defaultBranch": "master",
/**
* The default remote. This tells "rush change" which remote to compare against if the remote URL is
* not set or if a remote matching the provided remote URL is not found.
*/
// "defaultRemote": "origin"
},
/**
* Event hooks are customized script actions that Rush executes when specific events occur
*/
@ -315,25 +293,21 @@
"preRushInstall": [
// "common/scripts/pre-rush-install.js"
],
/**
* The list of shell commands to run after the Rush installation finishes
*/
"postRushInstall": [
"rush postinstall"
],
/**
* The list of shell commands to run before the Rush build command starts
*/
"preRushBuild": [],
/**
* The list of shell commands to run after the Rush build command finishes
*/
"postRushBuild": []
},
/**
* Installation variants allow you to maintain a parallel set of configuration files that can be
* used to build the entire monorepo with an alternate set of dependencies. For example, suppose
@ -364,7 +338,6 @@
// "description": "Build this repo using the previous release of the SDK"
// }
],
/**
* Rush can collect anonymous telemetry about everyday developer activity such as
* success/failure of installs, builds, and other operations. You can use this to identify
@ -374,14 +347,12 @@
* in the "eventHooks" section.
*/
// "telemetryEnabled": false,
/**
* Allows creation of hotfix changes. This feature is experimental so it is disabled by default.
* If this is set, 'rush change' only allows a 'hotfix' change type to be specified. This change type
* will be used when publishing subsequent changes from the monorepo.
*/
// "hotfixChangeEnabled": false,
/**
* (Required) This is the inventory of projects to be managed by Rush.
*
@ -483,6 +454,14 @@
"packageName": "@yume-chan/adb-backend-ws",
"projectFolder": "libraries/adb-backend-ws"
},
{
"packageName": "@yume-chan/adb-backend-direct-sockets",
"projectFolder": "libraries/adb-backend-direct-sockets"
},
{
"packageName": "@yume-chan/adb-credential-web",
"projectFolder": "libraries/adb-credential-web"
},
{
"packageName": "@yume-chan/scrcpy",
"projectFolder": "libraries/scrcpy"